Backbone.js的技巧和模式

本文由白牙根据Phillip Whisenhunt的《Backbone.js Tips And Patterns》所译,整个译文带有我自己的理解与思想,如果译得不好或不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://coding.smashingmagazine.com/2013/08/09/backbone-js-tips-patterns/,以及作者相关信息

作者:Phillip Whisenhunt

译者:白牙

Backbone.js是一个开源JavaScript“MV*”框架,在三年前它的第一次发布的时候就获得了显著的推动。尽管Backbone.js为Javascript应用程序提供了自己的结构,但它留下了大量根据开发者的需要而使用的设计模式和决策,并且当开发者们第一次使用Backbone.js开发的时候都会遇到许多共同的问题。

因此,在这篇文章中,我们除了会探索各种各样你能够应用到你的Backbone.js应用中的设计模式外,我们也会关注一些困惑开发者的常见问题。

执行对象的深复制

JavaScript中所有原始类型变量的传递都是值传递。所以,当变量被引用的时候会传递该变量的值。

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 对象的拷贝,你必须对这个对象进行一次复制。

你可能想知道,“为什么他(作者)要在这篇文章中解释这些按引用传递的东西?”很好,这是因为在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


转自:http://www.cnblogs.com/WhiteCusp/p/3356515
2019-03-02 23:41

知识点

相关教程

更多

渗透技巧总结、渗透技巧

旁站路径问题 1、读网站配置。 2、用以下VBS On Error Resume Next If (LCase(Right(WScript.Fullname,11))="wscript.exe") Then Msgbox Space(12) & "IIS Virtual Web Viewer" & Space(12) & Chr(13

渗透技巧总结

旁站路径问题  1、读网站配置。  2、用以下VBS  On Error Resume Next  If (LCase(Right(WScript.Fullname,11))="wscript.exe") Then  Msgbox Space(12) & "IIS Virtual Web Viewer" & Space(12) & C

谈判技巧

如果你对项目管理、系统架构有兴趣,请加微信订阅号“softjg”,加入这个PM、架构师的大家庭    A 好家伙、坏家伙(good guy,bad guy):两人谈判中扮演好家伙和坏家伙,也就是俗话说的一个唱红脸一个唱白脸。老外用一个挥大棒一个捧萝卜来比喻。  B. 拖延(delay):提出休会,以将对方的注意力从当前讨论的问题上转移开,或者是改变己方的谈判部署。拖延可以使谈判平静下来。主要是指战

Hadoop和Couchbase结合使用的技巧

Hadoop 和数据处理 Hadoop 将许多重要特性结合在一起,这使 Hadoop 对于将大量数据分解为更小、实用的数据块非常有用。 Hadoop 的主要组件是 HDFS 文件系统,它支持将信息分布到整个集群中。对于使用这种分布格式存储的信息,可以通过一个名为 MapReduce 的系统在每个集群节点上进行单独处理。MapReduce 进程将存储在 HDFS 文件系统中的信息转换为更小的、经过处

shell脚本常规技巧

邮件相关  发送邮件:    #!/usr/bin/pythonimport sys;import smtplib;from email.MIMEText import MIMETextmail_host = sys.argv[1]mail_user = sys.argv[2]mail_pass = sys.argv[3]mail_from = sys.argv[4]mail_to = sys.a

js怎样获取所有windos对象?

用js怎样获取所有打开的浏览器窗口?  并一一关闭?

微信公众平台运营技巧

目前很多企业对于微信营销有着很大的兴趣,也很想从其中渗透点东西出来作为商业价值,但是,偏偏很多人都不知道怎么去把微信运营这块弄好,有的甚至都不知道从何下手,为此,作为我小编一直在做微信公众平台的运营工作,今天也试着分享一下微信的运营技巧知识如下:   一、内容为王少不了:   当然“内容为王”是少不了:做微信依然会是内容为王,如何把内容做到大家喜欢?如何维持粉丝不让粉丝下降?如果实现自然增加粉丝?

Hadoop安全模式

在分布式文件系统启动的时候,开始的时候会有安全模式,当分布式文件系统处于安全模式的情况下,文件系统中的内容不允许修改也不允许删除,直到安全模式结束。安全模式主要是为了系统启动的时候检查各个DataNode上数据块的有效性,同时根据策略必要的复制或者删除部分数据块。运行期通过命令也可以进入安全模式。在实践过程中,系统启动的时候去修改和删除文件也会有安全模式不允许修改的出错提示,只需要等待一会儿即可。

JS URL编码函数

js对文字进行url编码涉及3个函数:escape,encodeURI,encodeURIComponent,相应3个解码函数:unescape,decodeURI,decodeURIComponent。

Linux下VMWare虚拟机的使用技巧

使用技巧: 1.虚拟机安装文件:vm-workstation-full-8.0.3-703057.x86_64.bundle,./vm-workstation-full-8.0.3-703057.x86_64.bundle即可安装,最后要输入序列号。安装文件可以到我的百度网盘下载。 链接:http://pan.baidu.com/share/link?shareid=2973346686&

JS数据类型

java数组类型 ==> 基本数据类型 和 引用数据类型.  js中 类型也分为两种 ==> 原始数据类型 和 对象数据类型. 与java一模一样. java中 基本数据类型有哪些? byte short int long float double boolean char  js中 原始数据类型有哪些?       number(数字,浮点型,整型)       string(js语

js 缺少对象

报的是错误是 drawcharts is not defined ,但是我的定义没错啊?  js代码  <script type="text/javascript" src="script/prototype.js"></script>  <script type="javascript">  func

Hadoop的安全模式

the ratio of reported blocks 1.0001 has reached the threshold 0.9990. Safe mode will be turned off automatically in 7 seconds. 平常不知道安全模式是啥的情况下,Hadoop提供了一些命令:hadoop dfsadmin -safemode NameNode在启动的时候首先进

js页面显示广告

各位大虾好,小弟最近有一个想法,就是在自己的网页插入一段广告。具体的效果和ku6等网站类似,在下载页面出来以后立即加载一段10s的广告,广告覆盖页面的一部风,页面其他部分设置为灰色,不可点击,广告播放完以后自动关闭,可以正常使用页面。  请问这用js怎么实现,谢谢啦。

FileUpload 对象 怎么用js 操作

在 HTML 文档中 <input type="file"> 标签每出现一次,一个 FileUpload 对象就会被创建。  但是这个是只读的不能用js去改变文件名  有没有什么办法可以新创建一个FileUpload 对象  并把自己要的文件名保存进去然后提交给后台?

最新教程

更多

java线程状态详解(6种)

java线程类为:java.lang.Thread,其实现java.lang.Runnable接口。 线程在运行过程中有6种状态,分别如下: NEW:初始状态,线程被构建,但是还没有调用start()方法 RUNNABLE:运行状态,Java线程将操作系统中的就绪和运行两种状态统称为“运行状态” BLOCK:阻塞状态,表示线程阻塞

redis从库只读设置-redis集群管理

默认情况下redis数据库充当slave角色时是只读的不能进行写操作,如果写入,会提示以下错误:READONLY You can't write against a read only slave.  127.0.0.1:6382> set k3 111  (error) READONLY You can't write against a read only slave. 如果你要开启从库

Netty环境配置

netty是一个java事件驱动的网络通信框架,也就是一个jar包,只要在项目里引用即可。

Netty基于流的传输处理

​在TCP/IP的基于流的传输中,接收的数据被存储到套接字接收缓冲器中。不幸的是,基于流的传输的缓冲器不是分组的队列,而是字节的队列。 这意味着,即使将两个消息作为两个独立的数据包发送,操作系统也不会将它们视为两个消息,而只是一组字节(有点悲剧)。 因此,不能保证读的是您在远程定入的行数据

Netty入门实例-使用POJO代替ByteBuf

使用TIME协议的客户端和服务器示例,让它们使用POJO来代替原来的ByteBuf。

Netty入门实例-时间服务器

Netty中服务器和客户端之间最大的和唯一的区别是使用了不同的Bootstrap和Channel实现

Netty入门实例-编写服务器端程序

channelRead()处理程序方法实现如下

Netty开发环境配置

最新版本的Netty 4.x和JDK 1.6及更高版本

电商平台数据库设计

电商平台数据库表设计:商品分类表、商品信息表、品牌表、商品属性表、商品属性扩展表、规格表、规格扩展表

HttpClient 上传文件

我们使用MultipartEntityBuilder创建一个HttpEntity。 当创建构建器时,添加一个二进制体 - 包含将要上传的文件以及一个文本正文。 接下来,使用RequestBuilder创建一个HTTP请求,并分配先前创建的HttpEntity。

MongoDB常用命令

查看当前使用的数据库    > db    test  切换数据库   > use foobar    switched to db foobar  插入文档    > post={"title":"领悟书生","content":"这是一个分享教程的网站","date":new

快速了解MongoDB【基本概念与体系结构】

什么是MongoDB MongoDB is a general purpose, document-based, distributed database built for modern application developers and for the cloud era. MongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。

windows系统安装MongoDB

安装 下载MongoDB的安装包:mongodb-win32-x86_64-2008plus-ssl-3.2.10-signed.msi,按照提示步骤安装即可。 安装完成后,软件会安装在C:\Program Files\MongoDB 目录中 我们要启动的服务程序就是C:\Program Files\MongoDB\Server\3.2\bin目录下的mongod.exe,为了方便我们每次启动,我

Spring boot整合MyBatis-Plus 之二:增删改查

基于上一篇springboot整合MyBatis-Plus之后,实现简单的增删改查 创建实体类 添加表注解TableName和主键注解TableId import com.baomidou.mybatisplus.annotations.TableId;
import com.baomidou.mybatisplus.annotations.TableName;
import com.baom

分布式ID生成器【snowflake雪花算法】

基于snowflake雪花算法分布式ID生成器 snowflake雪花算法分布式ID生成器几大特点: 41bit的时间戳可以支持该算法使用到2082年 10bit的工作机器id可以支持1024台机器 序列号支持1毫秒产生4096个自增序列id 整体上按照时间自增排序 整个分布式系统内不会产生ID碰撞 每秒能够产生26万ID左右 Twitter的 Snowflake分布式ID生成器的JAVA实现方案