首發個人博客,兩邊同時更新。html
2012年來到點樂以後,我開始投身Web應用開發。當時我選擇Backbone做爲主力框架,剛開始作不免會帶着各類舊習慣,只有一邊使用一邊摸索。最近我終於搞明白使用Backbone的正確姿式,記敘於此,但願能讓後來者少走些彎路。前端
使用框架時,個人原則是:既然使用了這個框架,就應該按照這個框架的思路解決問題,必定要把它的功能都用上,要按照它的方式組織代碼,這樣纔對得起學習使用的成本。因此下面的「正確姿式」,天然也是奔着全面使用Backbone的內建功能、儘可能符合Backbone的設計思路,這樣的目的總結的。jquery
本文假定讀者熟悉JavaScript,對Backbone有必定程度的瞭解。git
View
我比較贊成《JavaScript設計模式》中的觀點,Backbone的設計很難講是更接近MVP仍是更接近MVC,而是在Web前端這個大環境下,基於其技術特色,吸收各類設計模式的優勢作出最合適的的實現。Web應用中,視圖的實現天然應該交給HTML和CSS負責,而在數據變化時更新視圖,以及響應用戶操做的事情,就交給Backbone.View
。github
Backbone.View
提供很是直觀的events
幫助咱們註冊事件,免受使用jQuery的.on.on.on
之苦。事件被委託給$el
,不只節省資源,而且作列表類應用須要操做多個子節點時,無需再綁定事件。ajax
不過請注意,這裏的事件委託仍然有賴冒泡機制,因此諸如load
、error
等不冒泡的事件沒法委託給$el
處理(submit
在早期IE下也不行,可是jQuery會代發冒泡版),只能手工偵聽具體節點。另外,Backbone會把處理函數代理給View
的實例,因此函數中的this
指向是實例,而再也不是觸發事件的DOM元素。後端
接着,來點代碼,看看最經常使用的UI範式——列表。Backbone中,搭配使用Backbone.Collection
和Backbone.View
能夠方便的實現列表類組件,不過由於Backbone自己的限制不多,實現方法不少。早期我習慣於經過reset
方法和reset
事件來操做列表,後來慢慢體會到,接下來的作法更合適。設計模式
HTML部分:api
// 這裏假設咱們要作一個todo列表 <ul> <script type="text/x-handlebars-template"> <li id="{{id}}"> <input type="checkbox" name="todo" value="{{id}}"> {{title}} <time datetime="{{create_time}}">{{create_time}}</time> </li> </script> </ul>
JavaScript部分(使用Handlebars做爲模板引擎):瀏覽器
var ListView = Backbone.View.extend({ fragment: '', events: { }, initialize: function () { this.template = Handlebars.compile(this.$('script').remove().html().replace(/r|n|s{2,}/g, ''); this.collection.on('add', this.collection_addHandler, this); this.collection.on('remove', this.collection_removeHandler, this); this.collection.on('sync', this.collection_syncHandler, this); this.collection.fetch(); }, collection_addHandler: function (model) { this.fragment += this.template(model.toJSON()); }, collection_removeHandler: function (model) { this.$('#' + model.id).remove(); }, collection_syncHandler: function () { if (this.fragment) { this.$el.append(this.fragment); this.fragment = ''; } } });
之因此建議這麼作,由於Backbone.Collection
有三個特色:
model
的事件會由collection
向外轉播(至關於冒泡)collection
會逐個建立model
,每次都會廣播add
事件.fetch()
後.set()
時,新數據中未曾出現的對象(以id
爲標識)會被移除列表中還有一些技巧,將在後文呈現。
options
早先取數據時爲了向服務器傳遞變量,或者使用特定的jQuery參數,我常常覆寫.sync()
方法,甚至直接使用$.ajax()
。後來發現,各函數的options
(除去少數幾個return
以外)都會傳遞到下一個函數,直至最後;期間每一步的參數都會合並進來向後傳遞。
因此咱們只須要在第一次調用函數時,將須要的值放在options
裏便可 。好比,要保存model
裏的數據,API服務器和當前服務器不在同域,就能夠這樣:
// 關於xhrFields,能夠參考jQuery文檔:http://api.jquery.com/jQuery.ajax/ model.save(null, { xhrFields: { withCredentials: true } });
其實,options
是個很巧妙的設計。Backbone做爲框架,必須給其它庫和業務邏輯留出足夠的空間,使用options
,隨便其餘開發者傳什麼值,最後都能傳回業務邏輯中。另外,當參數個數比較多的時候,使用options
也有助於閱讀代碼。
options
進階基於「從頭傳到尾」這個性質,咱們還能夠發明一些特殊用法。好比上一節的例子,我但願給每一個元素增長一個刪除按鈕,點擊後移除元素。重點是:以漸隱動畫來表現移除動做。在Backbone中,.destroy()
方法會通知服務器刪除對象(.remove()
方法只是從當前集合中移除model
,沒法知足須要),而且觸發destroy
事件,咱們能夠在這裏插入動畫;但馬上就又會觸發remove
事件,因此只是在collection_destroyHandler
的時候fadeOut
是不夠的,還要防止collection_removeHandler
在動畫結束前直接移除dom。
這個時候,咱們就能夠利用options
。destroy()
支持參數{wait: true}
,能夠等待服務器返回成功後才移除model
。因而咱們就能確保對象已經從服務器上清除後,再以視圖體現;同時,只要檢查options
裏是否包含這個屬性,就能夠知道當前觸發collection_removeHandler
的是.destroy()
仍是.remove()
,再決定是否馬上移除節點就很容易了。(其實隨便傳個什麼標記均可以,這裏使用{wait: true}
能夠得到更好的體驗。)
// 一致的代碼我就不寫了 events: { 'click .delele-button': 'deleteButton_clickHandler' }, initialize: function () { // 一致的代碼 // .... // 再也不重寫 this.collection.on('destroy', this.collection_destroyHandler, this); }, collection_destroyHandler: function (model) { this.$('#' + model.id).fadeOut(function () { $(this).remove(); }); }, collection_removeHandler: function (model, collection, options) { if (!options.wait) { // 沒有wait this.$('#' + model.id).remove(); } }, deleteButton_clickHandler: function (event) { var id = $(event.target).closest('li').attr('id'); this.collection.get(id).destroy({wait: true}); // 服務器返回確認才真正移除 }
利用options
能達成的效果還有不少。好比,有些時候咱們想往model
裏放一些特殊用途的數據,只在渲染時候用,不保存到服務器上。經過研究源碼咱們發現,一樣調用.toJSON()
,.save()
會傳入含有各類參數的options
,而手動調用則不會。因而咱們又能根據options
裏的屬性返回不一樣的數據,知足不一樣的須要。
這些實現本文再也不一一詳述,你們請自行琢磨,歡迎留言探討。
sync
& fetch
有些習慣延續自以前的項目,好比請求遠程數據。早期我總想用$.ajax
從服務器端把數據取來,而後再reset
給collection
或者model
。直到最近開發新項目:tiger-prawn + lemon-grass(也即點樂後臺V5),我開始開發RESTful的後端,配合專爲RESTful設計的Backbone,貫徹「同步」思路,因而我終於醒悟,發出文章開頭那句感慨——我終於明白使用Backbone的正確姿式了。
「同步」是Backbone很是重要的設計思路,也是用戶體驗裏很是重要的一環。咱們常常要面對多端的環境,尤爲是開發企業級應用,多人協做辦公,必須保證數據在每一個終端看起來一致。我一開始很難理解爲何.fetch()
回來數據後,除非指定{remove: false}
,不然新數據中再也不存在的對象會被移出collection
。這個疑問後來在開發集體協做的todo列表時獲得解答。
還拿前文的todo列表作例子。想象列表面向一個工做組,組內成員均從列表當中接受工做,完成後勾上checkbox表示結案;別的地方還會有需求源源不斷的塞入這個列表中。這個時候,數據同步就顯得尤其重要。某甲勾掉一條任務,須要從其餘人的列表中勾掉同一條任務。因爲咱們數據交互的主要手段仍然是Ajax,服務器沒法向瀏覽器發出指令,只能由瀏覽器經過返回值進行操做,因此,將「同步」做爲強制性要求,本地collection
根據返回值的變化,該修改的修改,該移除的移除,就是最佳選擇了。
trigger: false
未必都有用最後再說個小問題。有時候我但願改變路徑,但不要刷新頁面,好比建立文章/article/create/,
.save()後獲得文章id,跳轉到
/article/id。由於仍然處於編輯狀態,因此不須要刷新頁面。一開始我覺得
router.navigate('#/article/id', {trigger: false});,加個參數
{trigger: false}`就能夠防止路由生效,後來發現不行。
出於種種緣由,好比服務器沒作重定向,我一直沒用pushState
。此種狀態下,Backbone使用setInterval
檢查地址欄變化,因此只要地址欄有修改,就會觸發相應的路由。
下面是我作過而且在維護的一些使用了Backbone的項目。這些項目未必都作到了以上幾點,因此僅供參考。
團隊培訓項目和團隊培訓項目二期,其實從這兩個項目中就能看出我思路的變化。
遊戲寶典 手機應用,雖然項目停擺了……找時間更新移植到phonegap上。
Backbone是一個輕量級,入侵程度很低的框架。能夠很方便的結合各類其餘庫來使用,對使用者的要求也不多,易於學習。不過若是能以正確的姿式操做,又能達到事半功倍的效果。這篇文章寫得很費勁,有些東西老是感受話到嘴邊寫不出來,反覆修改屢次對最後兩段仍不滿意。哎,之後再說吧,先發了。