Backbone.js的技巧和模式
本文由白牙根据Phillip Whisenhunt的《Backbone.js Tips And Patterns》所译,整个译文带有我自己的理解与思想,如果译得不好或不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://coding.smashingmagazine.com/2013/08/09/backbone-js-tips-patterns/,以及作者相关信息
译者:白牙
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.StickIt 和 Backbone.ModelBinder,提供了视图元素和模型属性之间的键值绑定,这能够节省你很多的相似代码。因此,如果你有很多复杂的表单字段,可以试着使用它们。
保持模型和视图分离
像Jeremy Ashkenas 在Backbone.js的 GitHub issues指出的一个问题,除了模型不能够由它们的视图来创建以外,Backbone.js并不在数据层和视图层之间实施任何真正的关注点分离。你觉得应该在数据层和视图层之间实施关注点分离吗?我和其他的一些Backbone.js开发者,像Oz Katz和 Dayal,都认为这个答案毫无疑问应该是要的:模型和集合,代表着数据层,应该禁止任何绑定到它们的视图的入口,从而保持一个完全的关注点分离。如果你不遵循这个关注点分离,你的代码很快就会变得像意大利面条那样纠缠不清,而没有人会喜欢这种代码。
保持你的数据层和视图层完全地分离可以使你拥有更加地模块化,可重用和可维护的代码。你能够轻易地在你的应用中重用和拓展模型和集合而不需要担心和他们绑定的视图。遵循这个模式能让新加入项目的开发者快速的投入到代码中。因为它们精确的知道哪里会发生视图的渲染以及哪里存放着应用的业务逻辑。
这个模式也强制使用了单一责任原则,该原则规定了每一个类应该只有一个单独的责任,并且它的职责应该封装在这个类中,因为你的模型和集合应该只负责处理数据,视图应该只负责处理渲染。
路由器中的参数映射
使用例子是展示这个模式如何产生的最好方法。例如:有一些搜索页面,它们允许用户添加两个不同的过滤类型,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 Bailey 和 Ian Storm Taylor 为这篇文章做的审查。
译者手语:整个翻译依照原文线路进行,并在翻译过程略加了个人对技术的理解。如果翻译有不对之处,还烦请同行朋友指点。谢谢!
关于白牙
现居上海,关注javascript应用,喜爱优雅和高效的前端交互设计,个人博客、新浪微博和Github,欢迎与同学一起共勉。
如需转载烦请注明出处:
英文原文:http://coding.smashingmagazine.com/2013/08/09/backbone-js-tips-patterns/