使用Backbone的正確姿式

首發個人博客,兩邊同時更新。html

2012年來到點樂以後,我開始投身Web應用開發。當時我選擇Backbone做爲主力框架,剛開始作不免會帶着各類舊習慣,只有一邊使用一邊摸索。最近我終於搞明白使用Backbone的正確姿式,記敘於此,但願能讓後來者少走些彎路。前端

使用框架時,個人原則是:既然使用了這個框架,就應該按照這個框架的思路解決問題,必定要把它的功能都用上,要按照它的方式組織代碼,這樣纔對得起學習使用的成本。因此下面的「正確姿式」,天然也是奔着全面使用Backbone的內建功能、儘可能符合Backbone的設計思路,這樣的目的總結的。jquery

本文假定讀者熟悉JavaScript,對Backbone有必定程度的瞭解。git

View

我比較贊成《JavaScript設計模式》中的觀點,Backbone的設計很難講是更接近MVP仍是更接近MVC,而是在Web前端這個大環境下,基於其技術特色,吸收各類設計模式的優勢作出最合適的的實現。Web應用中,視圖的實現天然應該交給HTML和CSS負責,而在數據變化時更新視圖,以及響應用戶操做的事情,就交給Backbone.Viewgithub

Backbone.View提供很是直觀的events幫助咱們註冊事件,免受使用jQuery的.on.on.on之苦。事件被委託給$el,不只節省資源,而且作列表類應用須要操做多個子節點時,無需再綁定事件。ajax

不過請注意,這裏的事件委託仍然有賴冒泡機制,因此諸如loaderror等不冒泡的事件沒法委託給$el處理(submit在早期IE下也不行,可是jQuery會代發冒泡版),只能手工偵聽具體節點。另外,Backbone會把處理函數代理給View的實例,因此函數中的this指向是實例,而再也不是觸發事件的DOM元素。後端

列表的前世此生

接着,來點代碼,看看最經常使用的UI範式——列表。Backbone中,搭配使用Backbone.CollectionBackbone.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有三個特色:

  1. model的事件會由collection向外轉播(至關於冒泡)
  2. 取得數據後,collection會逐個建立model,每次都會廣播add事件
  3. .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。

這個時候,咱們就能夠利用optionsdestroy()支持參數{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從服務器端把數據取來,而後再resetcollection或者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是一個輕量級,入侵程度很低的框架。能夠很方便的結合各類其餘庫來使用,對使用者的要求也不多,易於學習。不過若是能以正確的姿式操做,又能達到事半功倍的效果。這篇文章寫得很費勁,有些東西老是感受話到嘴邊寫不出來,反覆修改屢次對最後兩段仍不滿意。哎,之後再說吧,先發了。

相關文章
相關標籤/搜索