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

相关文章
相关标签/搜索