Backbone.js的技巧和模式

本文由白牙根據Phillip Whisenhunt的《Backbone.js Tips And Patterns》所譯,整個譯文帶有我本身的理解與思想,若是譯得很差或不對之處還請同行朋友指點。如需轉載此譯文,需註明英文出處:http://coding.smashingmagazine.com/2013/08/09/backbone-js-tips-patterns/,以及做者相關信息javascript

做者:Phillip Whisenhunthtml

譯者:白牙前端

Backbone.js是一個開源JavaScript「MV*」框架,在三年前它的第一次發佈的時候就得到了顯著的推進。儘管Backbone.js爲Javascript應用程序提供了本身的結構,但它留下了大量根據開發者的須要而使用的設計模式和決策,而且當開發者們第一次使用Backbone.js開發的時候都會遇到許多共同的問題。java

所以,在這篇文章中,咱們除了會探索各類各樣你可以應用到你的Backbone.js應用中的設計模式外,咱們也會關注一些困惑開發者的常見問題。git

執行對象的深複製

JavaScript中全部原始類型變量的傳遞都是值傳遞。因此,當變量被引用的時候會傳遞該變量的值。github

1 var helloWorld = 「Hello World」;
2 var helloWorldCopy = helloWorld;

例如,以上代碼會將helloWorldCopy 的值設爲helloWorld的值。因此對於helloWorldCopy 的全部修改都不會改變helloWorld, 由於它是一個拷貝。另外一方面,JavaScript全部非原始類型變量的傳遞都是引用傳遞,意思是當變量被引用的時候,JavaScript會傳遞一個其內存地址的參照。正則表達式

1 var helloWorld = {
2     ‘hello’: ‘world’
3 }
4 var helloWorldCopy = helloWorld;

舉個例子,上面的代碼會將helloWorldCopy 設爲helloWorld 對象的別名,此時,你可能會猜到,對helloWorldCopy 的全部修改都會直接在helloWorld 對象上進行。若是你想獲得一個helloWorld 對象的拷貝,你必須對這個對象進行一次複製。json

你可能想知道,「爲何他(做者)要在這篇文章中解釋這些按引用傳遞的東西?」很好,這是由於在Backbone.js的對象傳遞中,並不對進行對象複製,意味着若是你在一個模型中調用 get( ) 方法來取得一個對象,那麼對它的任何修改都會直接在模型中的那個對象進行操做!讓咱們經過一個例子來看看何時它會成爲你的麻煩。假設如今你有一個以下的Person 模型:設計模式

 1 var Person = Backbone.Model.extend({
 2    defaults: {
 3         'name': 'John Doe',
 4         'address': {
 5             'street': '1st Street'
 6             'city': 'Austin',
 7             'state': 'TX'
 8             'zipCode': 78701
 9         }
10    }
11 });

而且假設你建立了一個新的person 對象:緩存

1 var person = new Person({
2     'name': 'Phillip W'
3 });

如今讓咱們對新對象person 的屬性作一點修改。

1 person.set('name', 'Phillip W.');

上述代碼會對person 對象中的name 屬性進行修改。接下來讓咱們嘗試修改person 對象的address 屬性。在這以前,咱們先對address屬性添加校驗。

 1 var Person = Backbone.Model.extend({
 2     validate: function(attributes) {
 3 
 4         if(isNaN(attributes.address.zipCode)) return "Address ZIP code must be a number!";
 5     },
 6 
 7     defaults: {
 8         'name': 'John Doe',
 9         'address': {
10             'street': '1st Street'
11             'city': 'Austin',
12             'state': 'TX'
13             'zipCode': 78701
14         }
15     } 
16 });

如今,咱們會嘗試使用一個不正確的ZIP 碼來修改對象的address 屬性。

 1 var address = person.get('address');
 2 address.zipCode = 'Hello World';// 應該產生一個一個錯誤由於ZIP碼是無效的
 3 person.set('address', address);
 4 console.log(person.get('address'));
 5 /* 打印包含以下屬性的對象.
 6 {
 7     'street': '1st Street'
 8     'city': 'Austin',
 9     'state': 'TX'
10     'zipCode': 'Hello World'
11 }
12 */

爲何會這樣?咱們的校驗方法不是已經返回一個錯誤了嗎?!爲何attributes 屬性仍是被改變了?緣由正如前面所說,Backbone.js不會複製模型的attributes對象;它僅僅返回你所請求的東西。因此,你可能會猜到,若是你請求的是一個對象(如上面的address),你會獲得那個對象的引用,而且你對這個對象的全部修改都會直接地操做在模型中的實際對象中(所以這樣的修改方式並不會致使校驗失敗,由於對象的引用並無改變)。這個問題極可能會致使你花費幾小時來進行調試和診斷。

這個問題會逮住一些使用Backbone,js的新手甚至經驗豐富卻不夠警戒的JavaScript開發者。這個問題已經在GitHub issues 的Backbone.js部分引發了大量的討論。像 Jeremy Ashkenas 所指出的,執行深複製是一個很是棘手的問題,對那些有較大深度的對象來講,它將會是個很是昂貴的操做。

幸運地,jQuery提供了一些深複製的實現,$.extend。順帶說一句,Underscore.js,Backbone.js的一個依賴插件,也提供了相似的方法 _.extend ,但我會避免使用它,由於它並不執行深複製。

1 var address = $.extend(true, {}, person.address);

咱們如今獲得了 address 對象的一個精確的拷貝,所以咱們能夠爲所欲爲地修改它的內容而不用擔憂修改到person中的address 對象。你應該意識到此模式適用於上述那個例子僅由於address 對象的全部成員都是原始值(numbers, strings, 等等),因此當深複製的對象中還包含有子對象時必須謹慎地使用。你應該知道執行一個對象的深複製會產生一個小的性能影響,但我從沒見過它致使了什麼顯而易見的問題。儘管這樣,若是你對一個複雜對象的執行深複製或者一次性執行上千個對象的深複製,你可能會想作一些性能分析。這正是下一個模式出現的緣由。

爲對象建立Facades

在真實的世界裏,需求常常會更改,因此那些經過模型和集合的查詢而從終端返回的JSON數據也會有所改變。若是你的視圖與底層數據模型牢牢地耦合,這將會讓你感到很是麻煩。所以,我爲全部的對象建立了獲取器和設置器

不少人同意這種模式。就是若是任何底層數據結構被改變,視圖層不該該更新太多;當你只有一個數據入口的時候,你就不太可能忘記執行深複製,而且你的代碼會變得更加可維護和調試。但帶來的負面影響是這種模式會讓你的模型和集合有點膨脹。

讓咱們經過一個例子來搞清楚這個模式。假設咱們有一個Hotel 模型,其中包含了rooms和當前可用的rooms,咱們但願可以經過牀位尺寸值來取得相應的rooms。

 1 var Hotel = Backbone.Model.extend({
 2     defaults: {
 3         "availableRooms": ["a"],
 4         "rooms": {
 5             "a": {
 6                 "size": 1200,
 7                 "bed": "queen"
 8             },
 9             "b": {
10                 "size": 900,
11                 "bed": "twin"
12             },
13             "c": {
14                 "size": 1100,
15                 "bed": "twin"
16             }
17         },
18 
19         getRooms: function() {
20             $.extend(true, {}, this.get("rooms"));
21         },
22 
23         getRoomsByBed: function(bed) {
24             return _.where(this.getRooms(), { "bed": bed });
25         }
26     }
27 });

讓咱們假設明天你將會發布你的代碼,而且終端的開發者忘記告訴你rooms的數據結構從Object變成了一個array。你的代碼如今以下所示:

 1 var Hotel = Backbone.Model.extend({
 2     defaults: {
 3         "availableRooms": ["a"],
 4         "rooms": [
 5             {
 6                 "name": "a",
 7                 "size": 1200,
 8                 "bed": "queen"
 9             },
10             {
11                 "name": "b",
12                 "size": 900,
13                 "bed": "twin"
14             },
15             {
16                 "name": "c",
17                 "size": 1100,
18                 "bed": "twin"
19             }
20         ],
21 
22         getRooms: function() {
23             var rooms = $.extend(true, {}, this.get("rooms")),
24              newRooms = {};
25 
26            // transform rooms from an array back into an object
27             _.each(rooms, function(room) {
28                 newRooms[room.name] = {
29                     "size": room.size,
30                     "bed": room.bed
31                 }
32             });
33         },
34 
35         getRoomsByBed: function(bed) {
36             return _.where(this.getRooms(), { "bed": bed });
37         }
38     }
39 });

爲了將Hotel 轉換爲應用所指望的數據結構,咱們僅僅更新了一個方法,這讓咱們整個App的仍然正常工做。若是咱們沒有建立一個rooms數據的獲取器,咱們可能不得不更新每個rooms的數據入口。理想狀況下,你爲了使用一個新的數據結構而會想要更新全部的接口方法。但若是因爲時間緊迫而不得不盡快發佈代碼的話,這個模式能拯救你。

順帶提一下,這個模式既能夠被認爲是一個facade 設計模式,由於它隱藏了對象複製的細節,也能夠被稱爲 bridge 設計模式,由於它能夠被用於轉換所指望的數據結構。於是一個好的習慣是在全部的對象上使用獲取器和設置器。

存儲數據但不一樣步到服務器

儘管Backbone.js規定模型和集合會映射到REST-ful終端,但你有時候會發現你只是想將數據存儲在模型或者集合而不一樣步到服務器。一些其餘關於Backbone.js的文章,像「Backbone.js Tips: Lessons From the Trenches」就講解過這個模式。讓咱們快速地經過一個例子來看看何時這個模式會派上用場。假設你有個ul列表。

1 <ul>
2     <li><a href="#" data-id="1">One</a></li>
3     <li><a href="#" data-id="2">Two</a></li>
4     . . .
5     <li><a href="#" data-id="n">n</a></li>
6 </ul>

當n值爲200而且用戶點擊了其中一個列表項,那個列表項會被選中並添加了一個類以直觀地顯示。實現它的一個方法以下所示:

 1 var Model = Backbone.Model.extend({
 2     defaults: {
 3         items: [
 4             {
 5                 "name": "One",
 6                 "id": 1           
 7             },
 8             {
 9                 "name": "Two",
10                 "id": 2           
11             },
12             {
13                 "name": "Three",
14                 "id": 3           
15             }
16         ]
17     }
18 });
19 
20 var View = Backbone.View.extend({
21     template: _.template($('#list-template').html()),
22 
23     events: {
24         "#items li a": "setSelectedItem"
25     },
26 
27     render: function() {
28         $(this.el).html(this.template(this.model.toJSON()));
29     },
30 
31     setSelectedItem: function(event) {
32         var selectedItem = $(event.currentTarget);
33        // Set all of the items to not have the selected class
34         $('#items li a').removeClass('selected');
35         selectedItem.addClass('selected');
36         return false;
37     }
38 });
39 
40 <script id="list-template" type="template">
41     <ul id="items">
42             <% for(i = items.length - 1; i >= 0; i--) { %>
43         <li>
44                     <a href="#" data-id="<%= item[i].id %>"><%= item[i].name %></a></li>
45     <% } %></ul>
46 </script>

如今咱們想要知道哪個item被選中。一個方法是遍歷整個列表。但若是這個列表過長,這會是一個昂貴的操做。所以,當用戶點擊其中的列表項時,咱們應該將它存儲起來

 1 var Model = Backbone.Model.extend({
 2     defaults: {
 3         selectedId: undefined,
 4         items: [
 5             {
 6                 "name": "One",
 7                 "id": 1
 8             },
 9             {
10                 "name": "Two",
11                 "id": 2
12             },
13             {
14                 "name": "Three",
15                 "id": 3
16             }
17         ]
18     }
19 });
20 
21 var View = Backbone.View.extend({
22     initialize: function(options) {
23        // Re-render when the model changes
24         this.model.on('change:items', this.render, this);
25     },
26 
27     template: _.template($('#list-template').html()),
28 
29     events: {
30         "#items li a": "setSelectedItem"
31     },
32 
33     render: function() {
34         $(this.el).html(this.template(this.model.toJSON()));
35     },
36 
37     setSelectedItem: function(event) {
38         var selectedItem = $(event.currentTarget);
39        // Set all of the items to not have the selected class
40         $('#items li a').removeClass('selected');
41         selectedItem.addClass('selected');
42        // Store a reference to what item was selected
43         this.model.set('selectedId', selectedItem.data('id'));
44         return false;
45     }
46 });

如今咱們可以輕易地搜索咱們的模型來肯定哪個item被選中,而且咱們避免了遍歷文檔對象模型 (DOM)。這個模式對於存儲一些你想要跟蹤的外部數據很是有用;還要記住的是你可以建立不須要與終端相關聯的模型和集合。

這個模式的消極影響是你的模型或集合並非真正地採用RESTful 架構由於它們沒有完美地映射到網絡資源。另外,這個模式會讓你的模型帶來一點兒膨脹;而且若是你的終端嚴格地只接收它所指望的JSON數據,它會給你帶來一點兒麻煩。

渲染視圖的一部分而不是渲染整個視圖

當你第一次開發Backbone.js應用,你的視圖通常會是這樣的結構:

 1 var View = Backbone.View.extend({
 2     initialize: function(options) {
 3         this.model.on('change', this.render, this);
 4     },
 5 
 6     template: _.template($(‘#template’).html()),
 7 
 8     render: function() {
 9         this.$el.html(template(this.model.toJSON());
10         $(‘#a’, this.$el).html(this.model.get(‘a’));
11         $(‘#b’, this.$el).html(this.model.get(‘b’));
12     }
13 });

在這裏,你的模型的任何改變都會觸發一次視圖的完整的從新渲染。當我第一次使用Backbone.js來作開發的時候,我也使用過這種模式。但隨着我代碼的膨脹,我很快意識到這個方法是不可維護和不理想的,由於模型的任何屬性的改變都會讓視圖徹底從新渲染。

當我遇到這個問題的時候,我立刻在Google搜索其餘人是怎麼作的而且找到了Ian Storm Taylor的博客寫的一篇文章, 「Break Apart Your Backbone.js Render Methods,」,其中他提到了監聽模型個別的屬性改變而且響應的方法僅僅從新渲染視圖的一部分。Taylor也提到重渲染方法應該返回自身的this對象,這樣那些單獨的重渲染方法就能夠輕易地串聯起來。下面的這個例子已經做出了修改而變得更易於維護和管理了,由於當模型屬性改變的時候咱們僅僅更新相應部分的視圖。

 1 var View = Backbone.View.extend({
 2     initialize: function(options) {
 3         this.model.on('change:a', this.renderA, this);
 4         this.model.on('change:b', this.renderB, this);
 5     },
 6 
 7     renderA: function() {
 8         $(‘#a’, this.$el).html(this.model.get(‘a’));
 9         return this;
10     },
11 
12     renderB: function() {
13         $(‘#b’, this.$el).html(this.model.get(‘b’));
14         return this;
15     },
16 
17     render: function() {
18         this
19             .renderA()
20             .renderB();
21     }
22 });

還要提到的是,許多插件,像 Backbone.StickItBackbone.ModelBinder,提供了視圖元素和模型屬性之間的鍵值綁定,這可以節省你不少的類似代碼。所以,若是你有不少複雜的表單字段,能夠試着使用它們。

保持模型和視圖分離

像Jeremy Ashkenas 在Backbone.js的 GitHub issues指出的一個問題,除了模型不可以由它們的視圖來建立之外,Backbone.js並不在數據層和視圖層之間實施任何真正的關注點分離。你以爲應該在數據層和視圖層之間實施關注點分離嗎?我和其餘的一些Backbone.js開發者,像Oz KatzDayal,都認爲這個答案毫無疑問應該是要的:模型和集合,表明着數據層,應該禁止任何綁定到它們的視圖的入口,從而保持一個徹底的關注點分離。若是你不遵循這個關注點分離,你的代碼很快就會變得像意大利麪條那樣糾纏不清,而沒有人會喜歡這種代碼

保持你的數據層和視圖層徹底地分離可使你擁有更加地模塊化,可重用和可維護的代碼。你可以輕易地在你的應用中重用和拓展模型和集合而不須要擔憂和他們綁定的視圖。遵循這個模式能讓新加入項目的開發者快速的投入到代碼中。由於它們精確的知道哪裏會發生視圖的渲染以及哪裏存放着應用的業務邏輯。

這個模式也強制使用了單一責任原則,該原則規定了每個類應該只有一個單獨的責任,而且它的職責應該封裝在這個類中,由於你的模型和集合應該只負責處理數據,視圖應該只負責處理渲染。

路由器中的參數映射

使用例子是展現這個模式如何產生的最好方法。例如:有一些搜索頁面,它們容許用戶添加兩個不一樣的過濾類型,foo 和bar,每個都附有大量的選項。所以,你的URL結構看起來將會像這樣:

'search/:foo'
'search/:bar'
'search/:foo/:bar'

如今,全部的路由使用一個確切的視圖和模型,因此,理想情況下,你會樂意它們都用同一個方法,search()。可是,若是你檢查Backbone.js,會發現沒有任何形式的參數映射;這些參數只是簡單地從左到右扔到方法裏面去。因此,爲了讓它們都能使用相同的方法,你最終要建立不一樣的方法來正確地映射參數到search()方法。

 1 routes: {
 2     'search/:foo': 'searchFoo',
 3     'search/:bar': 'searchBar',
 4     'search/:foo/:bar': 'search'
 5 },
 6 
 7 search: function(foo, bar) {    
 8 },
 9 // I know this function will actually still map correctly, but for explanatory purposes, it's left in.
10 searchFoo: function(foo) {
11     this.search(foo, undefined);
12 },
13 
14 searchBar: function(bar) {
15     this.search(undefined, bar);
16 },

和你想的同樣,這種模式會快速地膨脹你的路由。當我第一次使用接觸這種模式的時候,我嘗試使用正則表達式在實際方法定義中作一些解析而「神奇地」映射這些參數,但這隻能在參數容易區分的狀況下起做用。因此我放棄了這個方法(我有時候依然會在Backbone插件中使用它)。我在issue on GitHub上提出過這個問題,Ashkenas 給個人建議是在search方法中映射全部的參數。

下面這段代碼已經變得更加具有可維護性:

 1 routes: {
 2     'base/:foo': 'search',
 3     'base/:bar': 'search',
 4     'base/:foo/:bar': 'search'
 5 },
 6 
 7 search: function() {
 8     var foo, bar, i;
 9 
10     for(i = arguments.length - 1; i >= 0; i--) {
11 
12         if(arguments[i] === 'something to determine foo') {
13             foo = arguments[i];
14             continue;
15         }
16         else if(arguments[i] === 'something to determine bar') {
17             bar = arguments[i];
18             continue;
19         }
20     }
21 },

這個模式能夠完全地減小路由器的膨脹。然而,要意識到它對於不可識別的參數時無效的。舉個例子,若是你有兩個傳遞ID的參數而且都它們以 XXXX-XXXX 這種模式表現,你將沒法肯定哪個ID對應的是哪個參數。

model.fetch() 不會清除你的模型

這個問題一般會絆倒使用Backbone.js的新手: model.fetch() 並不會清理你的模型,而是會將取回來的數據合併到你的模型當中。所以,若是你當前的模型有x,,y 和 z 屬性而且你經過fetch獲得了一個新的 y 和z 值,接下來 x 會保持模型原來的值,僅僅 y 和z 的值會獲得更新,下面這個例子直觀地說明了這個概念。

 1 var Model = Backbone.Model.extend({
 2     defaults: {
 3         x: 1,
 4         y: 1,
 5         z: 1
 6     }
 7 });
 8 var model = new Model();
 9 /* model.attributes yields
10 {
11     x: 1,
12     y: 1,
13     z: 1
14 } */
15 model.fetch();
16 /* let’s assume that the endpoint returns this
17 {
18     y: 2,
19     z: 2,
20 } */
21 /* model.attributes now yields
22 {
23     x: 1,
24     y: 2,
25     z: 2
26 } */

PUT請求須要一個ID屬性

這個問題也只一般出如今Backbone.js的新手中。當你調用.save() 方法時,你會發送一個HTTP PUT 請求,要求你的模型已經設置了一個ID屬性。HTTP PUT 是被設計爲一個更新動做的,因此發送PUT請求的時候要求你的模型已有一個ID屬性是合情理的。在理想的世界裏,你的全部模型都會有一個名爲id的屬性,但現實狀況是,你從終端接收的JSON數據的ID屬性並不老是會恰好命名爲id。

所以,若是你須要更新你的模型,請肯定在保存前你的模型具備一個ID。當終端返回的ID屬性變量名不爲 id 的時候,0.5及以上的版本的Backbone.js容許你使用 idAttribute 來改變ID屬性的名字。

若是使用的Backbone.js的版本仍低於0.5,我建議你修改集合或模型中的 parse 方法來映射指望的ID屬性到真正的ID屬性。這裏有一個讓你快速掌握這個技巧的例子,讓咱們假設你有一個cars集合,它的ID屬性名爲carID .

1 parse: function(response) {
2 
3     _.each(response.cars, function(car, i) {
4        // map the returned ID of carID to the correct attribute ID
5         response.cars[i].id = response.cars[i].carID;
6     });
7 
8     return response;
9 },

頁面加載中的模型數據

一些時候你會發現你須要在頁面加載的時候就使用數據來初始化你的集合和模型。一些關於Backbone.js模式的文章,像Rico Sta Cruz的「Backbone Patterns」和Katz的「Avoiding Common Backbone.js Pitfalls,」談論到了這個模式。使用你選擇的服務端語言,經過嵌入代碼到頁面並將數據放在單個模型的屬性或JSON當中,你可以輕易地實現這個模式。舉個例子,在Rails中,我會這樣使用:

1 // a single attribute
2 var model = new Model({
3     hello: <%= @world %>
4 });// or to have json
5 var model = new Model(<%= @hello_world.to_json %>);

使用這個模式可以經過「立刻渲染你的頁面」來提升你的搜索引擎排名,而且它能經過限制應用的HTTP請求來完全地縮短你的應用啓動和運行所花費的時間。

處理驗證失敗的模型屬性

你常常會想知道哪個模型屬性的驗證失敗了。舉個例子,若是你有一個極度複雜的表單域,你可能想知道哪個模型屬性驗證失敗,這樣你可以高亮顯示相應的表單域。不幸的是,提醒你的視圖哪個模型屬性驗證失敗並無直接在Backbone.js中實現,但你可使用不一樣的模式來處理這個問題。

返回一個錯誤對象

通知你的視圖哪個模型屬性驗證失敗的一個模式是回傳一個帶有某種標誌的對象,該對象中詳述哪個模型屬性驗證失敗,就像下面這樣:

 1 // Inside your model
 2 validate: function(attrs) {
 3     var errors = [];
 4 
 5     if(attrs.a < 0) {
 6         errors.push({
 7             'message': 'Form field a is messed up!',
 8             'class': 'a'
 9         });
10     }
11     if(attrs.b < 0) {
12         errors.push({
13             'message': 'Form field b is messed up!',
14             'class': 'b'
15         });
16     }
17 
18     if(errors.length) {
19         return errors;
20     }
21 }// Inside your view
22 this.model.on('invalid’, function(model, errors) {
23     _.each(errors, function(error, i) {
24         $(‘.’ + error.class).addClass('error');
25         alert(error.message);
26     });
27 });

這個模式的優勢是你在一個位置處理了全部的無效信息。缺點是若是你處理不一樣的無效屬性,你的屬性校驗部分會變爲一個比較大的 switch 或 if 語句。

廣播傳統錯誤事件

由我朋友Derick Bailey建議的一個替換的模式,是對個別的模型屬性觸發自定義錯誤事件。這能讓你的視圖爲個別的屬性綁定指定的錯誤事件。

 1 // Inside your model
 2 validate: function(attrs) {
 3 
 4     if(attrs.a < 0) {
 5             this.trigger(‘invalid:a’, 'Form field a is messed up!', this);
 6     }
 7     if(attrs.b < 0) {
 8             this.trigger(‘invalid:b’, 'Form field b is messed up!', this);
 9     }
10 }// Inside your view
11 this.model.on('invalid:a’, function(error) {
12         $(‘a’).addClass('error');
13         alert(error);
14 });
15 this.model.on('invalid:b’, function(error) {
16         $(‘b’).addClass('error');
17         alert(error);
18 });

這個模式的優勢是你的視圖綁定了明確類型的錯誤事件,而且若是你對每一類型的屬性錯誤有明確的執行指令,它能整頓你視圖代碼並使它更加可維護。這個模式的一個缺點是若是存在太多不一樣的想要處理的屬性錯誤,你的視圖代碼會變得更加臃腫。

兩個模式都有他們的優缺點,因此在你應該考慮哪種模式更加適合你的用例。若是你想對全部的驗證失敗處理都採用一個方法,那第一個方法會是個好選擇;若是你的每個模型屬性都有明確的UI改變,那選第二個方法會更好。

HTTP狀態碼200觸發錯誤

若是你的模型或集合訪問的終端返回了無效的JSON數據,它們會觸發一個「error」事件,即便你的終端返回的HTTP狀態碼是200。這個狀況一般出如今根據模擬JSON數據來作本地開發的時候。因此一個好方法是把你正在開發中的全部的模擬JSON文件都扔到JSON 驗證器中檢驗。或者爲你的IDE安裝一個插件能夠捕捉任何格式錯誤的JSON。

建立一個通用的錯誤展現

建立一個通用的錯誤展現意味着你有一個統一的模式來處理和顯示錯誤信息,這可以節省你的時間,並能提高用戶的總體體驗。在我開發的任何的Backbone.js 應用中我都建立了一個通用的視圖來處理警告。

 1 var AlertView = Backbone.View.extend({
 2     set: function(typeOfError, message) {
 3         var alert = $(‘.in-page-alert’).length ? $(‘.in-page-alert’): $(‘.body-alert’);
 4         alert
 5             .removeClass(‘error success warning’)
 6             .html(message)
 7             .fadeIn()
 8             .delay(5000)
 9             .fadeOut();
10     }
11 });

上面這個視圖首先查看在視圖以內是否已經聲明瞭 in-page-alert div。若是沒有聲明,它會回到被聲明在佈局的某個地方的通用 body-alert div中。這讓你可以傳遞一個一致的錯誤信息給你用戶,並在你忘記指定一個特定的 in-page-alert div時提供有效的備用div。上面的模式簡化了你在視圖中對錯誤信息的處理工做,以下面所示:

1 this.model.on('error', function(model, error) {
2     alert.set('TYPE-OF-ERROR', error);
3 });

更新單頁應用的文檔標題

這比關注任何東西都更加有用。若是你正在開發一個單頁應用,請記得更新每一頁的文檔標題!我寫過一個簡單的Backbone.js插件,Backbone.js Router Title Helper,它經過拓展Backbone.js路由器來簡單又優雅地實現這個功能。它容許你指定一個標題的對象常量,它的鍵映射到路由的方法名,值則是頁標題。

 1 Backbone.Router = Backbone.Router.extend({
 2 
 3     initialize: function(options){
 4         var that = this;
 5 
 6         this.on('route', function(router, route, params) {
 7 
 8             if(that.titles) {
 9                 if(that.titles[router]) document.title = that.titles[router];
10                 else if(that.titles.default) document.title = that.titles.default;
11                 else throw 'Backbone.js Router Title Helper: No title found for route:' + router + ' and no default route specified.';
12             }
13         });
14     }
15 });

在單頁應用中緩存對象

當咱們討論單頁應用的時候,你有必要遵循的另外一個模式是緩存那些將會被重複使用的對象。這個技巧是至關簡單和直接的:

 1 // Inside a router
 2 initialize: function() {
 3 
 4     this.cached = {
 5         view: undefined,
 6         model: undefined
 7     }
 8 },
 9 
10 index: function(parameter) {
11     this.cached.model = this.cached.model || new Model({
12         parameter: parameter
13     });
14     this.cached.view = this.cached.view || new View({
15         model: this.cached.model
16     });
17 }

這個模式將會讓你的應用像小石子那般飛快起來,由於你不須要從新初始化你的Backbone.js對象。然而,它可能會致使你的應用的內存佔用變得至關大;所以,我通常僅僅緩存那些貫穿整個應用的對象。若是你之前開發過Backbone.js應用,你可能會問本身「若是我想從新獲取數據會怎樣?」那麼你能夠在每次路由被觸發的時候從新獲取數據。

 1 // Inside a router
 2 initialize: function() {
 3 
 4     this.cached = {
 5         view: undefined,
 6         model: undefined
 7     }
 8 },
 9 
10 index: function(parameter) {
11     this.cached.model = this.cached.model || new Model({
12         parameter: parameter
13     });
14     this.cached.view = this.cached.view || new View({
15         model: this.cached.model
16     });
17     this.cached.model.fetch();
18 }

當你的應用必須從終端中取回最新數據的時候(例如,一個收信箱),這個模式會很好用。然而,若是你正在取回的數據依賴應用的狀態(假設狀態是靠你的URL和參數維持的),那即便自用戶上一次瀏覽頁面以來應用的狀態沒有改變,你仍會更新數據。一個比較好的解決辦法是僅當應用的狀態(parameter)被改變的時候才更新數據。

 1 // Inside a router
 2 initialize: function() {
 3 
 4     this.cached = {
 5         view: undefined,
 6         model: undefined
 7     }
 8 },
 9 
10 index: function(parameter) {
11     this.cached.model = this.cached.model || new Model({
12         parameter:parameter
13     });
14     this.cached.model.set('parameter', parameter);
15     this.cached.view = this.cached.view || new View({
16         model: this.cached.model
17     });
18 }
19 // Inside of the model
20 initialize: function() {
21     this.on("change:parameter", this.fetchData, this);
22 }

JSDoc功能和Backbone.js的類

我喜歡編制文檔而且是JSDoc的忠實粉絲,我用JSDoc 爲全部遵循下面所示格式的Backbone 類和方法生成了文檔:

 1 var Thing = Backbone.View.extend(/** @lends Thing.prototype */{
 2     /** @class Thing
 3      * @author Phillip Whisenhunt
 4      * @augments Backbone.View
 5      * @contructs Thing object */
 6     initialize() {},
 7 
 8     /** Gets data by ID from the thing. If the thing doesn't have data based on the ID, an empty string is returned.
 9      * @param {String} id The id of get data for.
10      * @return {String} The data. */
11     getDataById: function(id) {}
12 });

若是你爲上面這種格式的Backbone類編制文檔,你能夠編制一份漂亮的文檔,它包含你全部的類和帶有參數,返回值和描述的方法。請確保initialize 始終是第一個聲明的方法,由於這有助於生成JSDoc。若是你想要看一個使用JSDoc的項目的例子,請查閱HomeAway Calendar Widget。有個 Grunt.js 插件,grunt-jsdoc-plugin 插件,使用它們會把生成文檔做爲構建過程的一部分。

實踐測試驅動開發

在我看來,若是你正在使用Backbone.js,你應該讓你模型和集合遵循測試驅動開發(TDD)。我經過第一次爲個人模型和集合編寫失敗的 Jasmine.js 單元測試而開始遵循TDD。一旦我寫的單元測試編寫和失敗,我就把模型和集合排出。經過這一點,我全部的Jasmine 測試將會被傳遞,而且我有信心個人模型方法所有都會像預期般工做。由於我一直遵循TDD,個人視圖層已經能夠至關容易地編寫並會極度地輕薄。當你剛開始實踐TDD的時候,你確定會慢下來;不過一旦你深刻其中,你的生產力和代碼質量都會大大提高。

我但願這些技巧和模式會對你有幫助!若是你對其餘的模式有什麼建議或者你發現了一個錯誤或者你認爲其中的一個模式並非最好的方法,請在下面評論或到推特聯繫我。 感謝Patrick Lewis, Addy Osmani, Derick BaileyIan Storm Taylor 爲這篇文章作的審查。

譯者手語:整個翻譯依照原文線路進行,並在翻譯過程略加了我的對技術的理解。若是翻譯有不對之處,還煩請同行朋友指點。謝謝!

關於白牙

現居上海,關注javascript應用,喜好優雅和高效的前端交互設計,我的博客新浪微博Github,歡迎與同窗一塊兒共勉。

如需轉載煩請註明出處:

英文原文:http://coding.smashingmagazine.com/2013/08/09/backbone-js-tips-patterns/

中文譯文:http://www.cnblogs.com/WhiteCusp/p/3356515.html

相關文章
相關標籤/搜索