一、準(zhǔn)備階段
1.1 框架選型
隨著對(duì)MV*架構(gòu)模式的逐步理解,越來(lái)越發(fā)覺(jué)對(duì)于一般的業(yè)務(wù)場(chǎng)景,mvvm是前端架構(gòu)的不二選擇。在我所了解的框架范圍內(nèi),與mvvm架構(gòu)模式最貼合的框架只有angular,無(wú)論是雙向數(shù)據(jù)綁定、指令,還有強(qiáng)化的html標(biāo)簽,angular提出的很多新概念都像是為mvvm理論量身定做的一套外衣。很可惜angular對(duì)ie8的支持不太友好,兼容ie8的angular插件也非常稀缺,對(duì)于國(guó)內(nèi)環(huán)境,ie8仍然占有較大的市場(chǎng)份額,無(wú)奈之下只能轉(zhuǎn)向輕量級(jí)的backbone。
1.2 官方文檔vs電子書(shū)
通過(guò)閱讀backbone的官網(wǎng)簡(jiǎn)介,總結(jié)下backbone特點(diǎn)(并非優(yōu)點(diǎn)):
- 提供了基本的MVC結(jié)構(gòu),可以做到業(yè)務(wù)邏輯與視圖的分離(MV*框架最基本的功能)
- 內(nèi)置支持restful風(fēng)格的API(做過(guò)項(xiàng)目后會(huì)發(fā)覺(jué),內(nèi)置的那套基本沒(méi)什么用,自定義會(huì)更合適)
- 路由的支持(大部分MV*框架也都支持,也不是亮點(diǎn))
- 方便與第三方插件集成(因?yàn)閎ackbone太輕量了,對(duì)于插件沒(méi)什么要求,這勉強(qiáng)可以算優(yōu)點(diǎn))
- 兼容ie8(對(duì)比其他MV*框架,感覺(jué)這是唯一的優(yōu)點(diǎn),也是選擇它的理由)
- 粗粒度的單向數(shù)據(jù)綁定(缺點(diǎn),每次更新視圖,都只能全部更新,當(dāng)然你可以自己寫(xiě)局部更新,會(huì)比較繁瑣)
- 太輕量了(缺點(diǎn),連angular這種重量級(jí)的框架寫(xiě)法都會(huì)五花八門(mén),backbone這種就更別提了,新手根本無(wú)法駕馭它寫(xiě)出優(yōu)良的代碼)
由于backbone功能單薄,無(wú)法形成固有的mvc或是mvp模式,但是為了讓它變得好用,我們可以嘗試增強(qiáng)它的功能,讓它更接近我們想要的mvvm框架。mvvm框架最大特點(diǎn)是:雙向數(shù)據(jù)綁定,于是通過(guò)google:backbone data binding,找到了以下幾個(gè)backbone插件:Backbone.ModelBinder、Rivets.js、Backbone.Stickit。通過(guò)測(cè)試發(fā)現(xiàn)Rivets.js很好用,可惜不兼容ie8,ModelBinder配置不夠靈活,于是Stickit成為最佳選擇。
關(guān)于框架的學(xué)習(xí),我的思路是這樣的:官方文檔是一定要看的,但是沒(méi)必要從頭看到尾,官方開(kāi)頭那部分介紹是一定要看的,那里會(huì)告訴你框架是什么樣的,有什么特性,而具體的API只要看能反應(yīng)出框架特性的那幾個(gè)API就夠了,其他的都是用來(lái)查的。有些API不用查,你也知道它肯定有,就像你學(xué)完java后學(xué)C++,你知道C++里肯定有for循環(huán)。
<b>只看官網(wǎng)文檔是遠(yuǎn)遠(yuǎn)不夠的,文檔只是告訴你可以這樣用,但是沒(méi)有告訴你該不該這樣用,所以當(dāng)你大體了解了一個(gè)框架的基礎(chǔ)知識(shí)后,應(yīng)該找一本名叫《xxx框架最佳實(shí)踐》的電子書(shū),了解下怎么用才合理,對(duì)于輕量級(jí)的框架尤其如此。</b>
學(xué)計(jì)算機(jī)知識(shí),一個(gè)wiki百科就夠了(要翻墻),千萬(wàn)不要看國(guó)內(nèi)某百科,連自己貼吧的內(nèi)容都可以當(dāng)參考文獻(xiàn),實(shí)在是太不靠譜了。
二、實(shí)踐階段
準(zhǔn)備階段做得越多,實(shí)踐起來(lái)就越輕松,項(xiàng)目需求分析階段,開(kāi)發(fā)人員的時(shí)間如果用來(lái)做技術(shù)調(diào)研、框架選型、編寫(xiě)demo、測(cè)試性能、編寫(xiě)非業(yè)務(wù)組件等工作,時(shí)間還是會(huì)比較緊張的。總結(jié)了一下項(xiàng)目中的問(wèn)題及解決方案:
2.1 如何編寫(xiě)model層代碼
對(duì)于mvc新手,經(jīng)常會(huì)誤解m的含義,認(rèn)為m只是表示數(shù)據(jù)。實(shí)際上mvc是按照職責(zé)來(lái)劃分的,而非數(shù)據(jù)類(lèi)型。m表示業(yè)務(wù)層,即包含表示業(yè)務(wù)邏輯的數(shù)據(jù)模型,同時(shí)也包含操作這些數(shù)據(jù)的方法。反應(yīng)到backbone項(xiàng)目中,m層的寫(xiě)法如下:
var Product = Backbone.Model.extend({
// 業(yè)務(wù)數(shù)據(jù)模型
defaults: {
name: “”,
price: 0
},
// 操作數(shù)據(jù)的方法
create: function() {...},
remove: function() {...},
modify: function() {...},
query: function() {...}
});
不要在view中直接調(diào)用沒(méi)有業(yè)務(wù)含義的底層方法:fetch、save等,這將導(dǎo)致model與view耦合在一起,view層的代碼變得繁雜且難以維護(hù)。
2.2 事件與邏輯分離
backbone提供了View類(lèi),很多人并沒(méi)有意識(shí)到事件與邏輯的解耦,他們通常這樣寫(xiě):
var ProductView = Backbone.View.extend({
// 注冊(cè)事件
events: {
"click #saveBtn": "create",
....
},
// 創(chuàng)建
create: function() {
// 針對(duì)click準(zhǔn)備一些數(shù)據(jù),可能涉及到dom操作
var data = ....;
// 執(zhí)行創(chuàng)建的邏輯
...
}
});
當(dāng)觸發(fā)創(chuàng)建邏輯的事件不止一個(gè)時(shí),會(huì)變成這樣:
var ProductView = Backbone.View.extend({
// 注冊(cè)事件
events: {
"click #saveBtn": "createForClick",
"blur #xxInput": "createForBlur",
....
},
// 創(chuàng)建for click
createForClick: function() {
// 針對(duì)click準(zhǔn)備一些數(shù)據(jù),可能涉及到dom操作
var data = ....;
// 執(zhí)行創(chuàng)建的邏輯
...
},
// 創(chuàng)建for blur
createForBlur: function() {
// 針對(duì)blur準(zhǔn)備一些數(shù)據(jù),可能涉及到dom操作
var data = ....;
// 執(zhí)行創(chuàng)建的邏輯
...
}
});
此時(shí)你會(huì)發(fā)現(xiàn)處理邏輯的代碼重復(fù)了,所以將事件與邏輯分離的一個(gè)優(yōu)點(diǎn)是:邏輯代碼可以復(fù)用,正確的寫(xiě)法如下:
var ProductView = Backbone.View.extend({
// 注冊(cè)事件
events: {
"click #saveBtn": "createForClick",
"blur #xxInput": "createForBlur",
....
},
// 響應(yīng)click事件的創(chuàng)建
createForClick: function() {
// 針對(duì)click準(zhǔn)備一些數(shù)據(jù),可能涉及到dom操作
var data = ....;
// 執(zhí)行創(chuàng)建的邏輯
this.create(data);
},
// 響應(yīng)blur事件的創(chuàng)建
createForBlur: function() {
// 針對(duì)blur準(zhǔn)備一些數(shù)據(jù),可能涉及到dom操作
var data = ....;
// 執(zhí)行創(chuàng)建的邏輯
this.create(data);
},
// 執(zhí)行創(chuàng)建
create: function(data) {...}
});
事件與邏輯的分離最大的好處是可以方便的進(jìn)行單元測(cè)試,以上面為例,只需針對(duì)create方法進(jìn)行測(cè)試,就能驗(yàn)證邏輯的正確性,而非邏輯的dom操作是不在單元測(cè)試范圍之內(nèi)的。
2.3 凈化路由代碼
backbone提供了路由功能,可以方便的根據(jù)網(wǎng)址跳轉(zhuǎn)到指定的視圖,很多人的寫(xiě)法是這樣的:
var AppRouter = Backbone.Router.extend({
"route1": "createView1",
"route2": "createView2",
....,
createView1: function() {
// 1.操作model層方法,獲取視圖所需數(shù)據(jù)
...
// 2.new一個(gè)視圖對(duì)象,傳遞數(shù)據(jù)到視圖中
...
// 3.其他的邏輯
...
},
createView2: function() {
// 1.操作model層方法,獲取視圖所需數(shù)據(jù)
...
// 2.new一個(gè)視圖對(duì)象,傳遞數(shù)據(jù)到視圖中
...
// 3.其他的邏輯
...
}
});
隨著需求的增加,創(chuàng)建視圖的邏輯會(huì)變的越來(lái)越復(fù)雜,整個(gè)路由的代碼會(huì)顯得非常臃腫,還有一個(gè)問(wèn)題,不單單路由里需要?jiǎng)?chuàng)建視圖,其他的地方也會(huì)用到,這個(gè)時(shí)候,重復(fù)的代碼又出現(xiàn)了。解決這個(gè)問(wèn)題的方案是抽取出創(chuàng)建視圖的邏輯,實(shí)際項(xiàng)目中我抽取了一個(gè)文件夾叫作:controllers,里面以業(yè)務(wù)功能為單位,存放創(chuàng)建視圖邏輯的js文件,比如處理產(chǎn)品的controller,可以定義為productCtrl.js,代碼如下:
var productCtrl= (function() {
var create = function() {
// 1.操作model層方法,獲取視圖所需數(shù)據(jù)
...
// 2.new一個(gè)視圖對(duì)象,傳遞數(shù)據(jù)到視圖中
...
// 3.其他的邏輯
...
};
return {
create: create
};
});
此時(shí)路由的代碼就變得非常清爽了,同時(shí)其他地方需要?jiǎng)?chuàng)建視圖時(shí),只需要調(diào)用productCtrl.create()
即可,路由代碼:
var AppRouter = Backbone.Router.extend({
"route1": "createView1",
"route2": "createView2",
....,
createView1: function() {
productCtrl.create();
},
createView2: function() {
...
}
});
2.4 引入bower-installer優(yōu)化bower文件結(jié)構(gòu)
前端構(gòu)建時(shí)需要合并第三方的js、css文件,但是每個(gè)插件的目錄結(jié)構(gòu)不盡相同,導(dǎo)致grunt命令寫(xiě)起來(lái)非常繁瑣,為了盡量統(tǒng)一處理,引入了bower-installer插件,主要目的是抽取出插件的核心文件,將其放入規(guī)則統(tǒng)一的目錄中,方便進(jìn)一步處理。 bower-installer的github地址:https://github.com/blittle/bower-installer
2.4.1 bower-installer使用
- 安裝:npm install -g bower-installer
- 運(yùn)行:bower-installer
具體配置參考github上的文檔
2.4.2 基于bower-installer引入第三方插件的流程(以jquery為例)
- 配置bower.json文件:"jquery": "1.11.3"
- 執(zhí)行bower install下載jquery插件
- 執(zhí)行bower installer抽取jquery核心文件到bower_main_files目錄
- 引入文件:在index.html中引入插件的根目錄由bower_components改為bower_main_files
2.5 mvvm的缺陷
開(kāi)發(fā)過(guò)程一切都很順利,只踩到兩個(gè)坑:<b>性能</b>、<b>復(fù)雜界面的邏輯處理</b>,而這兩個(gè)坑是由mvvm的基因決定的。
mvvm的雙向數(shù)據(jù)綁定可以讓開(kāi)發(fā)變得非常便利,但同時(shí)也帶來(lái)了兩個(gè)問(wèn)題:
- 為了將model與dom中的元素一一對(duì)應(yīng)進(jìn)行綁定,即便最簡(jiǎn)單的界面,也會(huì)消耗大量性能,當(dāng)需要一次性加載大量數(shù)據(jù)時(shí),性能問(wèn)題就會(huì)凸顯。
- 數(shù)據(jù)綁定是基于觀察者模式構(gòu)建的,當(dāng)界面中存在多個(gè)組件,且組件間有復(fù)雜交互時(shí),代碼很難跟蹤,調(diào)試會(huì)變得非常困難,當(dāng)修改了某個(gè)model時(shí),你無(wú)法清晰的了解后面會(huì)發(fā)生什么。
針對(duì)mvvm的缺點(diǎn),以下業(yè)務(wù)場(chǎng)景是不適合的:
- 由于極端的用戶(hù)體驗(yàn)要求,需要一次性渲染大量數(shù)據(jù),不能分頁(yè)的場(chǎng)景。
- 組件多且交互過(guò)復(fù)雜的場(chǎng)景。
據(jù)說(shuō)angular2.0也傾向單向數(shù)據(jù)流,估計(jì)也是考慮到這個(gè)原因,react+flux也推崇單向數(shù)據(jù)流,也許這是一種趨勢(shì)。但對(duì)于一般的業(yè)務(wù)場(chǎng)景來(lái)說(shuō),雙向數(shù)據(jù)綁定還是非常實(shí)用的,所以具體采用什么方案還是要結(jié)合具體的業(yè)務(wù)場(chǎng)景。
三、總結(jié)
- backbone.js是一個(gè)功能單薄的框架,它需要其他插件的輔助才能變得好用。
- 沒(méi)有了解過(guò)xxx最佳實(shí)踐,就不要輕易在項(xiàng)目里使用,不然往往會(huì)得到xxx不好用的錯(cuò)誤結(jié)論。
- 沒(méi)有一個(gè)模式永遠(yuǎn)是最佳的,只有適合業(yè)務(wù)場(chǎng)景的模式才是最佳模式。