1. 程式人生 > >Backbone.js簡單入門範例

Backbone.js簡單入門範例

Backbone.js已經很古老了,在Angular.js大行其道的今天,我們公司居然還在用Backbone。。。【哭笑不得臉】

這篇文章寫的非常淺顯易懂,一步一步教你如何使用Backbone.js,特此轉過來收藏

11年剛開始用前端MVC框架時寫過一篇文章,當時Knockout和Backbone都在用,但之後的專案全是在用Backbone,主要因為它簡單、靈活,無論是富JS應用還是企業網站都用得上。相比React針對View和單向資料流的設計,Backbone更能體現MVC的思路,所以針對它寫一篇入門範例,說明如下:

  1. 結構上分4節,介紹Model/View/Collection,實現從遠端獲取資料顯示到表格且修改刪除;
  2. 名為“範例”,所以程式碼為主,每節的第1段程式碼都是完整程式碼,複製貼上就能用,每段程式碼都是基於前一段程式碼來寫的,因此每段程式碼的新內容不會超過20行(大括號計算在內);
  3. 每行程式碼沒有註釋,但重要內容之後有寫具體的說明;
  4. 開發環境是Chrome,使用github的API,這樣用Chrome即使在本地路徑(形如file://的路徑)也能獲取資料。

0. Introduction

幾乎所有的框架都是在做兩件事:一是幫你把程式碼寫在正確的地方;二是幫你做一些髒活累活。Backbone實現一種清晰的MVC程式碼結構,解決了資料模型和檢視對映的問題。雖然所有JS相關的專案都可以用,但Backbone最適合的還是這樣一種場景:需要用JS生成大量的頁面內容(HTML為主),使用者跟頁面元素有很多的互動行為。

Backbone物件有5個重要的函式,Model/Collection/View/Router/History。Router和History是針對Web應用程式的優化,建議先熟悉pushState的相關知識。入門階段可以只瞭解Model/Collection/View。將Model視為核心,Collection是Model的集合,View是為了實現Model的改動在前端的反映。

1. Model

Model是所有JS應用的核心,很多Backbone教程喜歡從View開始講,其實View的內容不多,而且理解了View意義不大,理解Model更重要。以下程式碼實現從github的API獲取一條gist資訊,顯示到頁面上:

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://code.jquery.com/jquery-1.11.1.js"></script>
<script type="text/javascript" src="http://underscorejs.org/underscore-min.js"></script>
<script type="text/javascript" src="http://backbonejs.org/backbone-min.js"></script>

<link href="http://cdn.bootcss.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <table id="js-id-gists" class="table">
    <thead><th>description</th><th>URL</th><th>created_at</th></thead>
    <tbody></tbody>
  </table>
  <script type="text/javascript">
  var Gist = Backbone.Model.extend({
    url: 'https://api.github.com/gists/public',
    parse: function (response) {
      return (response[0]);
    }
  }),
    gist = new Gist();

  gist.on('change', function (model) {
    var tbody = document.getElementById('js-id-gists').children[1],
      tr = document.getElementById(model.get('id'));
    if (!tr) {
      tr = document.createElement('tr');
      tr.setAttribute('id', model.get('id'));
    }
    tr.innerHTML = '<td>' + model.get('description') + '</td><td>' + model.get('url') + '</td><td>' + model.get('created_at') + '</td>';
    tbody.appendChild(tr);
  });
  gist.fetch();
  </script>
</body>
</html>
  • LINE4~8: 載入要用到的JS庫。ajax請求和部分View的功能需要jQuery支援(或者重寫ajax/View的功能);Backbone的程式碼是基於Underscore寫的(或者用Lo-Dash代替);載入bootstrap.css只是因為預設樣式太難看…
  • LINE16~22: 建立一個Model並例項化。url是資料來源(API介面)的地址,parse用來處理返回的資料。實際返回的是一個Array,這裡取第一個Object。
  • LINE24~33: 繫結change事件。還沒有使用View,所以要自己處理HTML。這10行程式碼主要是get的用法(model.get),其他的功能之後會用View來實現。
  • LINE34: 執行fetch。從遠端獲取資料,獲到資料後會觸發change事件。可以重寫sync方法
    開啟Chrome的Console,輸入gist,可以看到Model獲得的屬性:

這裡寫圖片描述

Model提供資料和與資料相關的邏輯。上圖輸出的屬性是資料,程式碼中的fetch/parse/get/set都是對資料進行操作,其他的還有escape/unset/clear/destory,從函式名字就大致可以明白它的用途。還有很常用的validate函式,在set/save操作時用來做資料驗證,驗證失敗會觸發invalid事件:

  /* 替換之前程式碼的JS部分(LINE16~34) */
  var Gist = Backbone.Model.extend({
    url: 'https://api.github.com/gists/public',
    parse: function (response) {
      return (response[0]);
    },
    defaults: {
      website: 'dmyz'
    },
    validate: function (attrs) {
      if (attrs.website == 'dmyz') {
        return 'Website Error';
      }
    }
  }),
    gist = new Gist();

  gist.on('invalid', function (model, error) {
    alert(error);
  });
  gist.on('change', function (model) {
    var tbody = document.getElementById('js-id-gists').children[1],
      tr = document.getElementById(model.get('id'));
    if (!tr) {
      tr = document.createElement('tr');
      tr.setAttribute('id', model.get('id'));
    }
    tr.innerHTML = '<td>'+ model.get('description') +'</td><td>'+ model.get('url') +'</td><td>'+ model.get('created_at') +'</td>';
    tbody.appendChild(tr);
  });
  gist.save();

跟之前的程式碼比較,有4處改動:

  • LINE7~9: 增加了defaults。如果屬性中沒有website(注意不是website值為空),會設定website值為dmyz。
  • LINE10~14: 增加validate函式。當website值為dmyz時,觸發invalid事件。
  • LINE18~20: 繫結invalid事件,alert返回的錯誤。
  • LINE31: 不做fetch,直接save操作。

因為沒有fetch,所以頁面上不會顯示資料。執行save操作,會呼叫validate函式,驗證失敗會觸發invalid事件,alert出錯誤提示。同時save操作也會向Model的URL發起一個PUT請求,github這個API沒有處理PUT,所以會返回404錯誤。

在Console中輸入gist.set('description','demo'),可以看到頁面元素也會有相應的變化。執行gist.set('description', gist.previous('description'))恢復之前的值。這就是Model和View的對映,現在還是自己實現的,下一節會用Backbone的View來實現。

2. View

用Backbone的View來改寫之前程式碼LINE24~33部分:

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://code.jquery.com/jquery-1.11.1.js"></script>
<script type="text/javascript" src="http://underscorejs.org/underscore-min.js"></script>
<script type="text/javascript" src="http://backbonejs.org/backbone-min.js"></script>

<link href="http://cdn.bootcss.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <table id="js-id-gists" class="table">
    <thead><th>description</th><th>URL</th><th>created_at</th><th></th></thead>
    <tbody></tbody>
  </table>
  <script type="text/javascript">
  var Gist = Backbone.Model.extend({
    url: 'https://api.github.com/gists/public',
    parse: function (response) {
      return response[0];
    }
  }),
    gist = new Gist();

  var GistRow = Backbone.View.extend({
    el: 'tbody',
    MODEL: gist,
    events: {
      'click a': 'replaceURL'
    },
    replaceURL: function () {
      this.MODEL.set('url', 'http://dmyz.org');
    },
    initialize: function () {
      this.listenTo(this.MODEL, 'change', this.render);
    },
    render: function () {
      var model = this.MODEL,
        tr = document.createElement('tr');
      tr.innerHTML = '<td>' + model.get('description') + '</td><td>' + model.get('url') + '</td><td>' + model.get('created_at') + '</td><td><a href="javascript:void(0)">&reg;</a></td>';
      this.el.innerHTML = tr.outerHTML;
      return this;
    }
  });
  var tr = new GistRow();
  gist.fetch();
  </script>
</body>
</html>
  • LINE25: 所有的View都是基於DOM的,指定el會選擇頁面的元素,指定tagName會建立相應的DOM,如果都沒有指定會是一個空的div。
  • LINE27~32: 繫結click事件到a標籤,replaceURL函式會修改(set)url屬性的值。
  • LINE33~35: View的初始化函式(initialize),監聽change事件,當Model資料更新時觸發render函式。
  • LINE36~42: render函式。主要是LINE41~42這兩行,把生成的HTML程式碼寫到this.el,返回this。
  • LINE44: 例項化GistRow,初始化函式(initialize)會被執行。

點選行末的a標籤,頁面顯示的這條記錄的URL會被修改成http://dmyz.org

這個View名為GistRow,選擇的卻是tbody標籤,這顯然是不合理的。接下來更改JS程式碼,顯示API返回的30條資料:

/* 替換之前程式碼的JS部分(LINE16~45) */
  var Gist = Backbone.Model.extend(),
    Gists = Backbone.Model.extend({
      url: 'https://api.github.com/gists/public',
      parse: function (response) {
        return response;
      }
    }),
    gists = new Gists();

  var GistRow = Backbone.View.extend({
    tagName: 'tr',
    render: function (object) {
      var model = new Gist(object);
      this.el.innerHTML = '<td>' + model.get('description') + '</td><td>'+ model.get('url') + '</td><td>' + model.get('created_at') + '</td><td></td>'
      return this;
    }
  });

  var GistsView = Backbone.View.extend({
    el: 'tbody',
    model: gists,
    initialize: function () {
      this.listenTo(this.model, 'change', this.render);
    },
    render: function () {
      var html = '';
      _.forEach(this.model.attributes, function (object) {
        var tr = new GistRow();
        html += tr.render(object).el.outerHTML;
      });
      this.el.innerHTML = html;
      return this;
    }
  });
  var gistsView = new GistsView();
  gists.fetch();
  • LINE2~9: 建立了兩個Model(Gist和Gists),parse現在返回完整Array而不只是第一條。
  • LINE11~18: 建立一個tr。render方法會傳一個Object來例項化一個Gist的Model,再從這個Model裡get需要的值。
  • LINE26~34: 遍歷Model中的所有屬性。現在使用的是Model而不是Collection,所以遍歷出的是Object。forEach是Underscore的函式。

Backbone的View更多的是組織程式碼的作用,它實際乾的活很少。View的model屬性在本節第一段程式碼用的是大寫,表明只是一個名字,並不是說給View傳一個Model它會替你完成什麼,控制邏輯還是要自己寫。還有View中經常會用到的template函式,也是要自己定義的,具體結合哪種模板引擎來用就看自己的需求了。

這段程式碼中的Gists比較難操作其中的每一個值,它其實應該是Gist的集合,這就是Backbone的Collection做的事了。

3. Collection

Collection是Model的集合,在這個Collection中的Model如果觸發了某個事件,可以在Collection中接收到並做處理。第2節的程式碼用Collection實現:

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://code.jquery.com/jquery-1.11.1.js"></script>
<script type="text/javascript" src="http://underscorejs.org/underscore-min.js"></script>
<script type="text/javascript" src="http://backbonejs.org/backbone-min.js"></script>

<link href="http://cdn.bootcss.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <table id="js-id-gists" class="table">
    <thead><th>description</th><th>URL</th><th>created_at</th><th></th></thead>
    <tbody></tbody>
  </table>
  <script type="text/javascript">
  var Gist = Backbone.Model.extend(),
    Gists = Backbone.Collection.extend({
      model: Gist,
      url: 'https://api.github.com/gists/public',
      parse: function (response) {
        return response;
      }
    }),
    gists = new Gists();

  var GistRow = Backbone.View.extend({
    tagName: 'tr',
    render: function (model) {
      this.el.innerHTML = '<td>' + model.get('description') + '</td><td>'+ model.get('url') + '</td><td>' + model.get('created_at') + '</td><td></td>'
      return this;
    }
  });

  var GistsView = Backbone.View.extend({
    el: 'tbody',
    collection: gists,
    initialize: function () {
      this.listenTo(this.collection, 'reset', this.render);
    },
    render: function () {
      var html = '';
      _.forEach(this.collection.models, function (model) {
        var tr = new GistRow();
        html += tr.render(model).el.outerHTML;
      });
      this.el.innerHTML = html;
      return this;
    }
  });
  var gistsView = new GistsView();
  gists.fetch({reset: true});
  </script>
</body>
</html>
  • LINE17~23: 基本跟第2節的第2段程式碼一樣。把Model改成Collection,指定Collection的Model,這樣Collectio獲得返回值會自動封裝成Model的Array。
  • LINE38: Collection和Model不同,獲取到資料也不會觸發事件,所以繫結一個reset事件,在之後的fetch操作中傳遞{reset: true}。
  • LINE42~45: 從Collection從遍歷Model,傳給GistRow這個View,生成HTML。

Collection是Backbone裡功能最多的函式(雖然其中很多是Underscore的),而且只要理解了Model和View的關係,使用Collection不會有任何障礙。給Collection繫結各種事件來實現豐富的互動功能了,以下這段JS程式碼會加入刪除/編輯的操作,可以在JSBIN上檢視原始碼和執行結果。只是增加了事件,沒有什麼新內容,所以就不做說明了,附上JSBIN的演示地址:http://jsbin.com/jevisopo/1

/* 替換之前程式碼的JS部分(LINE16~51) */
  var Gist = Backbone.Model.extend(),
    Gists = Backbone.Collection.extend({
      model: Gist,
      url: 'https://api.github.com/gists/public',
      parse: function (response) {
        return response;
      }
    }),
    gists = new Gists();

  var GistRow = Backbone.View.extend({
    tagName: 'tr',
    render: function (model) {
      this.el.id = model.cid;
      this.el.innerHTML = '<td>' + model.get('description') + '</td><td>'+ model.get('url') + '</td><td>' + model.get('created_at') + '</td><td><a href="javascript:void(0)" class="js-remove">X</a> <a href="javascript:void(0)" class="js-edit">E</a>&nbsp;</td>'
      return this;
    }
  });

  var GistsView = Backbone.View.extend({
    el: 'tbody',
    collection: gists,
    events: {
      'click a.js-remove': function (e) {
        var cid = e.currentTarget.parentElement.parentElement.id;
        gists.get(cid).destroy();
        gists.remove(cid);
      },
      'click a.js-edit': 'editRow',
      'blur td[contenteditable]': 'saveRow'
    },
    editRow: function (e) {
      var tr = e.currentTarget.parentElement.parentElement,
        i = 0;

      while (i < 3) {
        tr.children[i].setAttribute('contenteditable', true);
        i++;
      }
    },
    saveRow: function (e) {
      var tr = e.currentTarget.parentElement,
        model = gists.get(tr.id);

      model.set({
        'description' : tr.children[0].innerText,
        'url': tr.children[1].innerText,
        'created_at': tr.children[2].innerText
      });
      model.save();
    },
    initialize: function () {
      var self = this;
      _.forEach(['reset', 'remove', 'range'], function (e) {
        self.listenTo(self.collection, e, self.render);
      });
    },
    render: function () {
      var html = '';
      _.forEach(this.collection.models, function (model) {
        var tr = new GistRow();
        html += tr.render(model).el.outerHTML;
      });
      this.el.innerHTML = html;
      return this;
    }
  });
  var gistsView = new GistsView();
  gists.fetch({reset: true});

Afterword

雖然是入門範例,但因為篇幅有限,有些基本語言特徵和Backbone的功能不可能面面俱到,如果還看不懂肯定是我漏掉了需要解釋的點,請(在Google之後)評論或是郵件告知。

Backbone不是jQuery外掛,引入以後整個DOM立即實現增刪改查了,也做不到KnockoutJS/AnglarJS那樣,在DOM上做資料繫結就自動完成邏輯。它是將一些前端工作處理得更好更規範,如果學習前端MVC的目的是想輕鬆完成工作,Backbone可能不是最佳選擇。如果有一個專案,100多行HTML和1000多行JS,JS主要都在操作頁面DOM(如果討厭+號連線HTML還可以搭配React/JSX來寫),那就可以考慮用Backbone來重寫了,它比其他龐大的MVC框架要容易掌握得多,作為入門學習也是非常不錯的。