目前來看,團隊內部前端項目已全面實施組件化開發。組件化的好處太多,如:按需載入、可復用、易維護、可擴展、少挖坑、不改組件代碼直接切成伺服器端渲染(如 "Nuclear" 組件化可以做到,大家叫同構)... 怎麼做到這麼強大的優勢,來回憶下以前見過的坑,或者現有項目里的坑。 CSS層疊樣式?保佑不要污 ...
目前來看,團隊內部前端項目已全面實施組件化開發。組件化的好處太多,如:按需載入、可復用、易維護、可擴展、少挖坑、不改組件代碼直接切成伺服器端渲染(如Nuclear組件化可以做到,大家叫同構)...
怎麼做到這麼強大的優勢,來回憶下以前見過的坑,或者現有項目里的坑。
CSS層疊樣式?保佑不要污染別的HTML!
在web前端,一般一個組件必須要有骨架HTML和裝飾的CSS以及JS邏輯。而CSS要是可以是局部作用域那就再好不過了!就不用寫長長的首碼了,浪費帶寬不說,而且費勁。
如
.ui-popup-arrow-xx-xxxxx-xxxx-container {
}
這回夠長了吧,不會污染別的HTML了吧。真的太長了,沒有辦法,因為CSS不是局部的,怕污染其他的HTML,規劃好長長的namespace、module是以前的最佳實踐。
怎麼優雅綁定事件?只能定義在window下?
如果HTML綁定的事件是局部作用域那就再好不過了!我真的見過模版代碼里出現下麵的代碼:
<div onclick="xxx()"></div>
然後在js里找到了下麵的代碼:
<script>
window.xxx = function(){
}
</script>
要綁定的事件一多,得污染多少全局變數啊。所以還有的工程師這麼乾:
<div onclick="ns.xxx()"></div>
<div onclick="ns.xxxx()"></div>
然後在js里找到了下麵的代碼:
<script>
window.ns = {};
ns.xx = function(){
}
ns.xxx = function(){
}
</script>
這裡貌似比不設定namespace好很多,但是還是妥協的結果。一般希望能封裝成組件,組件的HTML里綁定的事件就是組件內定義的事件,內聚內聚!!
通過js動態綁定事件的壞處我以前專門寫了一篇文章來闡述,主要是lazy bind會導致用戶看到了頁面,但是頁面確無法響應用戶的交互,這裡不再闡述。
需求變更?找不到在哪改代碼?
大型項目如游戲什麼的為啥都是面向對象式的寫法?如果一個組件剛好又能是一個Class那就再好不過,Class base可以更方便地抽象現實世界的物體及其屬性或者邏輯演算法,所以甚至有些編程語言都是面向對象的(這裡逆向邏輯),如JAVA、C#...整體過程式的代碼對於大型項目幾乎沒法維護(如基於jQuery就能容易寫出整體都是過程式的組織結構),整體OO,局部過程式是可以接受的。
組件需要嵌套?只能複製粘貼原組件?
扁平無嵌套組件還是比較簡單,對模板的字元串處理下,把綁定的事件全指向組件自身定義的方法,生命周期也好處理。在真正的業務里經常需要組件嵌套,這樣也更利於復用。雖然大量模板引擎支持引用子模板、共用數據等,但是組件是有生命周期的,模板嵌套不能真正解決組件嵌套的問題。能支持組件嵌套並且聲明式嵌套就那就再好不過了!
數據變了?重新生成HTML替換一下?
怎麼替換?先查找dom?什麼?你還在查找dom?你還在背誦CSS選擇器?替換一下?不能增量更新嗎?或者diff一下吧?不要每次全部替換啊!
首屏太慢?以前抽象的組件沒法復用?
什麼?首屏太慢?改成直出(伺服器渲染)?以前代碼沒法復用?要推翻重寫?什麼?怎麼搞?排期?產品不給排期?需求沒變為什麼要給排期?
下麵來看下Nuclear怎麼解決上面問題。
install Nuclear
npm install alloynuclear
Hello,Nuclear!
var HelloNuclear = Nuclear.create({
render: function () {
return '<div>Hello , {{name}} !</div>';
}
})
new HelloNuclear({ name: "Nuclear" }, "body");
內置了mustache.js無邏輯模板。
事件綁定
var EventDemo = Nuclear.create({
clickHandler: function (evt, target, other1,other2) {
//MouseEvent {isTrusted: true, screenX: 51, screenY: 87, clientX: 51, clientY: 21…}
console.log(evt);
//<div onclick="Nuclear.instances[0].clickHandler(event,this,'otherParameter1','otherParameter2')">Click Me!</div>
console.log(target);
//otherParameter1
console.log(other1);
//otherParameter2
console.log(other2);
alert("Hello Nuclear!");
},
render: function () {
return '<div onclick="clickHandler(event,this,\'otherParameter1\',\'otherParameter2\')">Click Me!</div>'
}
})
new EventDemo({ seen: true }, "body");
條件判斷
var ConditionDemo = Nuclear.create({
render: function () {
return '{{#seen}}\
<div>\
you can see me\
</div>\
{{/seen}}\
{{^seen}}\
<div>\
yan can not see me\
</div>\
{{/seen}}'
}
})
var cd = new ConditionDemo({ seen: true }, "body");
setTimeout(function () {
cd.option.seen = false;
}, 2000);
2秒後改變seen,dom會自動變更。
迴圈
var LoopDemo = Nuclear.create({
render: function () {
return '<ul>{{#list}}<li>姓名:{{name}} 年齡:{{age}}</li>{{/list}}</ul>'
}
})
var ld = new LoopDemo({
list: [
{ name: "dntzhang", age: 18 },
{ name: "vorshen", age: 17 }
]
}, "body");
setTimeout(function () {
//增加
ld.option.list.push({ name: "lisi", age: 38 });
}, 1000);
setTimeout(function () {
//修改
ld.option.list[0].age = 19;
}, 2000);
setTimeout(function () {
//移除
ld.option.list.splice(0, 1);
}, 3000);
Array的變更也能監聽到,能夠自動觸發Dom的變更。
局部CSS
<body>
<div>I'm other div!! my color is not red!!</div>
<script src="../dist/nuclear.js"></script>
<script type="text/javascript">
var ScopedCSSDemo = Nuclear.create({
clickHandler: function () {
alert("my color is red!");
},
render: function () {
return '<div onclick="clickHandler()">my color is red!</div>'
},
style: function () {
return 'div { cursor:pointer; color:red }';
}
})
//第三個參數true代表 增量(increment)到body里,而非替換(replace)body里的
new ScopedCSSDemo ({ seen: true }, "body" ,true);
</script>
</body>
組件外的div不會被組件內的CSS污染。
討厭反斜杠?
討厭反斜杠可以使用 ES20XX template literals、或者split to js、css和html文件然後通過構建組裝使用。也可以用template標簽或者textare存放模板。
<template id="myTemplate">
<style>
h3 {
color: red;
}
button {
color: green;
}
</style>
<div>
<div>
<h3>TODO</h3>
<ul>{{#items}}<li>{{.}}</li>{{/items}}</ul>
<form onsubmit="add(event)">
<input nc-id="textBox" value="{{inputValue}}" type="text">
<button>Add #{{items.length}}</button>
</form>
</div>
</div>
</template>
<script>
var TodoApp = Nuclear.create({
install: function () {
this.todoTpl = document.querySelector("#myTemplate").innerHTML;
},
add: function (evt) {
evt.preventDefault();
this.inputValue = "";
this.option.items.push(this.textBox.value);
},
render: function () {
return this.todoTpl;
}
});
new TodoApp({ inputValue: "", items: [] }, "body");
</script>
組件嵌套
<script>
var TodoList = Nuclear.create({
render: function () {
return '<ul> {{#items}} <li>{{.}}</li> {{/items}}</ul>';
}
});
</script>
<script>
var TodoTitle = Nuclear.create({
render: function () {
return '<h3>{{title}}</h3>';
}
});
</script>
<script>
var TodoApp = Nuclear.create({
install: function () {
//pass options to children
this.childrenOptions = [{ title: "Todo" }, { items: [] }];
this.length = 0;
},
add: function (evt) {
evt.preventDefault();
//this.nulcearChildren[1].option.items.push(this.textBox.value);
//or
this.list.option.items.push(this.textBox.value);
this.length = this.list.option.items.length;
this.textBox.value = "";
},
render: function () {
//or any_namespace.xx.xxx.TodoList 對應的 nc-constructor="any_namespace.xx.xxx.TodoList"
return '<div>\
<child nc-constructor="TodoTitle"></child>\
<child nc-constructor="TodoList" nc-name="list"></child>\
<form onsubmit="add(event)" >\
<input nc-id="textBox" value="{{inputValue}}" type="text" />\
<button>Add #'+ this.length + '</button>\
</form>\
</div>';
}
});
new TodoApp({ inputValue: "" }, "body");
</script>
通過在父對象的install里設置this.childrenOptions來把option傳給子節點。
伺服器端渲染
function todo(Nuclear,server) {
var Todo = Nuclear.create({
add: function (evt) {
evt.preventDefault();
this.option.items.push(this.textBox.value);
},
render: function () {
return `<div>
<h3>TODO</h3>
<ul> {{#items}} <li>{{.}}</li> {{/items}}</ul>
<form onsubmit="add(event)" >
<input nc-id="textBox" type="text" value="" />
<button>Add #{{items.length}}</button>
</form>
</div>`;
},
style: function () {
return `h3 { color:red; }
button{ color:green;}`;
}
},{
server:server
});
return Todo;
}
if ( typeof module === "object" && typeof module.exports === "object" ) {
module.exports = todo ;
} else {
this.todo = todo;
}
通過第二個參數server來決定是伺服器端渲染還是客戶端渲染。server使用的代碼也很簡單:
var koa = require('koa');
var serve = require('koa-static');
var router = require('koa-route');
var app = koa();
var jsdom = require('jsdom');
var Nuclear = require("alloynuclear")(jsdom.jsdom().defaultView);
var Todo = require('./component/todo')(Nuclear,true);
app.use(serve(__dirname + '/component'));
app.use(router.get('/todos', function *(){
var str = require('fs').readFileSync(__dirname + '/view/index.html', 'utf8');
var todo = new Todo({ items: ["Nuclear2","koa",'ejs'] });
this.body = Nuclear.Tpl.render(str, {
todo: todo.HTML
});
Nuclear.destroy(todo);
}));
app.listen(3000);
瀏覽器端使用的代碼:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
{{{todo}}}
<script src="./nuclear.js"></script>
<script src="./todo.js"></script>
<script>
new todo(Nuclear)('body');
</script>
</body>
</html>
這樣,組件的代碼不需要任何變更就可以在server和client同時使用。
Nuclear如何做到同構的?
內置三條管線如下所示:
比如一般前後端分離的開發方式,僅僅會走中間那條管線。而同構的管線如下所示:
這裡前後後端會共用option,所以不僅僅需要直出HTML,option也會一併支持給前端用來二次渲染初始一些東西。
Nuclear優勢
1.節約流量
2.提升用戶體驗
3.載入更加靈活
4.Dom查找幾乎絕跡
5.搭積木一樣寫頁面
6.提升代碼復用性
7.可插拔的模板引擎
8.Lazy CSS首屏更輕鬆
9.Nuclear文件大小6KB (gzip)
10.零行代碼修改無縫切到同構直出
...
...
Nuclear Github
https://github.com/AlloyTeam/Nuclear