自從3年前Backbone.js發布第一版以來,Backbone.js就成為一個流行的開源JavaScript “MV*”框架,並獲得人們的青睞。儘管Backbone.js為JavaScript應用提供了框架,但是它仍然給開發者留有很多設計模式供選擇,不管怎樣,當開發者第一次使用Backbone.js時還會產生很多普遍的問題的。
因此,在這篇文章中,我們將介紹很多不同的設計模式供你在Backbone.js應用中使用,而且我們也會一同來看看對於開發者來說會產生很多普遍的有關性能伸縮的問題。
對象深度拷貝
JavaScript對待所有原生類型變數是傳值。所以,當變數被引用時就傳遞了變數的值。
var helloWorld = “Hello World”; var helloWorldCopy = helloWorld;
舉個例子,上面的程式碼將變數helloWorldCopy的值設定為變數helloWorld的值。這樣, 自從它的值被複製之後,所有修改helloWorldCopy的值不會修改helloWorld的值。 JavaScript對待所有非原始類型的變數時傳引用,這意味著當變數傳遞的時候將會傳遞記憶體位址引用。
var helloWorld = { ‘hello': ‘world' } var helloWorldCopy = helloWorld;
舉個例子,上面的程式碼將設定helloWorldCopy為helloWorld的引用,而且,也許你會猜到任何修改helooWorldCopy的值都會直接導致helloWorld值的變化。如果你想要helloWorld的拷貝,你可以建立一個拷貝物件即可。
也許你會想到「為什麼Backbone.js可以解釋為所有的工作都是通過傳遞引用?」事實上,Backbone.js不會拷貝對象,這將意味著如果你從模型裡調用.get()方法得到一個對象,任何給這個對象的修改都會直接修改原來的對象。讓我們一起來看一個例子來闡明哪裡會發生這樣的情況。如果你有個如下的Person模型:
var Person = Backbone.Model.extend({ defaults: { 'name': 'John Doe', 'address': { 'street': '1st Street' 'city': 'Austin', 'state': 'TX' 'zipCode': 78701 } } });
這樣你就建立了一個新的person物件:
var person = new Person({ 'name': 'Phillip W' });
現在我們來對新物件的一些屬性進行操作:
person.set('name', 'Phillip W.', { validate: true });
上面的程式碼成功的給person物件的name屬性賦了值。現在我們在來操作person物件的位址屬性。當然,在我們這樣做之前先驗證一下地址屬性。
var Person = Backbone.Model.extend({ validate: function(attributes) { if(isNaN(attributes.address.zipCode)) return "Address ZIP code must be a number!"; }, defaults: { 'name': 'John Doe', 'address': { 'street': '1st Street' 'city': 'Austin', 'state': 'TX' 'zipCode': 78701 } } });
現在,讓我們試著為位址屬性設定一個不正確的ZIP程式碼。
var address = person.get('address'); address.zipCode = 'Hello World'; // Raises an error since the ZIP code is invalid person.set('address', address, { validate: true }); console.log(person.get('address')); /* Prints an object with these properties. { 'street': '1st Street' 'city': 'Austin', 'state': 'TX' 'zipCode': 'Hello World' } */
這將會怎樣呢?我們的驗證出現了錯誤!為什麼屬性依舊被改變了?前邊我們說過,Backbone.js不會拷貝模型屬性;它會回傳你所請求的一切。這樣,你也許會猜到,如果你需要一個對象,你將得到這個對象的引用,對這個對象的任何操作都會直接改變模型裡的對象。如果你要debug,這可能會把你帶入無底的兔子黑洞。
這個問題對於新的Backbone.js使用要引起注意,甚至對於老練的JavaScript程式設計師有時也會沒有提防。這個問題在GitHub的Backbone.js討論組中有很激烈的討論。正如Jeremy Ashkenas指出,執行一個深的物件引用是一個很難解決的難題,一個很深的物件引用是要花費很大代價的。
幸運的,jQuery 提供了一個深度拷貝功能來實現,$.extend. 如同, Underscore.js ,一個Backbone.js的依靠,提供_.extend 方法,但是我必須避免使用它,因為它沒有執行一份是個深度的複製,Lo-Dash, Underscore.js的一個分叉版本,提供了物件一個深度克隆的_.clone 方法的選項。然而,我使用 $.extend 方法的模型所使用的語法規則去執行一個任意物件的深度克隆。記得通過後,結果它執行的是一個深度的克隆方法
var address = $.extend(true, {}, person.address);
我们现在快速准确的复制一个theaddressobject?,并且我们能够更改它对于我们要点没有包括在内的我们不用担心会更改它原有的模型。你必须要意思到这个父工厂对于上面所有的事例因为它所有的地址对象成员都是不可变的(numbers, strings, etc.),与此同时这上面所有的事例工厂当你要深度复制对象里面包含的对象时你都必须小心的使用。你必须也要知道一个小小的性能影响都来自于执行一个深度的克隆,但是我从来没有看到过很明显的问题。然而,如果你要深度的克隆一个大对象或者成千上万的对象所有的立即复制,你将有可能做大量的性能分析。这将领导我们直接到下一个模式。
为对象创建外观
在现实世界中,需求经常变化,JavaScript对象符号(或者说JSON)也是一样,这些对象是由模型和集合所在的端点返回的。这或许会成为你的基础代码中的一个真正的大麻烦,如果你的视图与底层的数据模型是紧耦合的话。因此,我为所有对象创建了getters和setters。
支持这个模式的人非常多。如果任何底层的数据结构改变了,那么视图层并不需要更新许多;你将有一个数据的访问点,所以你不太可能忘记做一个深度拷贝,你的代码将会更易于维护更易于调试。负面因素在于这个模式可能导致模型或集合的一点点膨胀。
我们看一个例子来阐明这个模式。想像我们有一个Hotel模型,包含有rooms和目前可获得的rooms,而且我们希望可以通过床位大小来获得rooms。
var Hotel = Backbone.Model.extend({ defaults: { "availableRooms": ["a"], "rooms": { "a": { "size": 1200, "bed": "queen" }, "b": { "size": 900, "bed": "twin" }, "c": { "size": 1100, "bed": "twin" } }, getRooms: function() { $.extend(true, {}, this.get("rooms")); }, getRoomsByBed: function(bed) { return _.where(this.getRooms(), { "bed": bed }); } } });
现在我们假设明天你就要发布你的代码,而你又发现端点开发者忘记告诉你rooms的数据结构改变了,由一个对象变为一个数组。你的代码现在看起来会像下面这样。
var Hotel = Backbone.Model.extend({ defaults: { "availableRooms": ["a"], "rooms": [ { "name": "a", "size": 1200, "bed": "queen" }, { "name": "b", "size": 900, "bed": "twin" }, { "name": "c", "size": 1100, "bed": "twin" } ], getRooms: function() { var rooms = $.extend(true, {}, this.get("rooms")), newRooms = {}; // transform rooms from an array back into an object _.each(rooms, function(room) { newRooms[room.name] = { "size": room.size, "bed": room.bed } }); }, getRoomsByBed: function(bed) { return _.where(this.getRooms(), { "bed": bed }); } } });
我们仅仅更新了一个函数,以便将Hotel的结构转变为这个应用的其余部分所期望的结构,同时整个应用仍然像我们所期待的一样运作。如果这里没有一个getter,我们很可能不得不为rooms更新每个访问点。理想情况下,你会希望更新所有的函数,以适应新的数据结构,但如果你在时间方面有压力急于发布的话,这个模式将可以拯救你。
离题说一句,这个模式既可以被认为是装饰模式,因为它隐藏了创建对象拷贝的复杂性,也可以认为是桥接模式,因为它可以用来将数据转换为所期望的形式。一个好的经验是对任何对象元素使用getters 和setters 。
存储数据不是通过服务器保存
尽管Backbone.js有模型和集合映射的规定去具象状态的传输(or REST-ful)的端点,你将花大量的时间去找你想要的存储数据在你的模型或者不是在服务器上的连接。另外一些关于Backbone.js的文章,例如“Backbone.js Tips: Lessons From the Trenches” 是通过SupportBee的Prateek Dayal ,这个模式还有其他的描述。让我们一起来快速的看一个小例子来帮助我们说明它可能会派上用场。假设你有一个集合。
<ul> <li><a href="#" data-id="1">One</a></li> <li><a href="#" data-id="2">Two</a></li> . . . <li><a href="#" data-id="n">n</a></li> </ul>
当使用者点击其中一个项目时,这个项目成为了被选中状态并且对于使用者作为选中项目是通过 aselectedclass 添加的是可视化的。以下这是一种方式:
var Model = Backbone.Model.extend({ defaults: { items: [ { "name": "One", "id": 1 }, { "name": "Two", "id": 2 }, { "name": "Three", "id": 3 } ] } }); var View = Backbone.View.extend({ template: _.template($('#list-template').html()), events: { "#items li a": "setSelectedItem" }, render: function() { $(this.el).html(this.template(this.model.toJSON())); }, setSelectedItem: function(event) { var selectedItem = $(event.currentTarget); // Set all of the items to not have the selected class $('#items li a').removeClass('selected'); selectedItem.addClass('selected'); return false; } }); <script id="list-template" type="template"> <ul id="items"> <% for(i = items.length - 1; i >= 0; i--) { %> <li> <a href="#" data-id="<%= item[i].id %>"><%= item[i].name %></a></li> <% } %></ul> </script>
现在我们能够很容易的判断被选中的项目,并且我们没有必要通过对象模型去判断。这种模式对于存储无用的数据是非常有用的以至于 你可能非常想要去跟踪;请记住你能够创建一个模型并且没有必要去关联于他们存储的一些无用的图像数据。
var View = Backbone.View.extend({ initialize: function(options) { // Re-render when the model changes this.model.on('change:items', this.render, this); }, template: _.template($('#list-template').html()), events: { "#items li a": "setSelectedItem" }, render: function() { $(this.el).html(this.template(this.model.toJSON())); }, setSelectedItem: function(event) { var selectedItem = $(event.currentTarget); // Set all of the items to not have the selected class $('#items li a').removeClass('selected'); selectedItem.addClass('selected'); // Store a reference to what item was selected this.selectedItemId = selectedItem.data('id')); return false; } });
现在我们可以很容易的确定哪些项已经被选中,并且我们没有必要通过这些对象模型来了解。这个模式对于存储无用的数据是非常有用的,请记住,您可以创建不一定有端点相关联的存储无关的视图数据的模型和集合。
这种模式的缺点是你存储了无用的数据在你的模型或者集合中,它们不能真正意义上的追随一个平静的架构是因为它们不会完美的去映射在web资源上;另外,这个模式会引起一些很膨胀的在你的模型中;;并且当你保存你的模型的时候如果你的端点严格的只接受JSON数据它会引起一个很大的烦恼。
你可能会问你自己,“我如何确定我是否应该讲把额外的数据放进视图或者是模型中?”。如果额外的属性你将要增加的是围绕性的呈现,例如一个容器的高度,我们应该要添加它的图形。如果这个属性跟底层的数据模型有一些关系,然后你想要将它放进这个模型中。例如,如果上面的例子更多的显露出,因为某些原因我仅仅只希望用户通过从模型返回的项目列表中选择一个特殊的项,我可能会增加这种逻辑模型。总而言之,大多数的事情,它实际上取决于这种依赖。你能够为保持你的模型而辩论并且你可以认为保持你的观点是可能的并且把尽可能多的逻辑放进你的模型中。
渲染部分视图,而不是整个视图
当你第一次开始开发Backbone.js应用时,典型的视图结构是像这样的:
var View = Backbone.View.extend({ initialize: function(options) { this.model.on('change', this.render, this); }, template: _.template($(‘#template').html()), render: function() { this.$el.html(template(this.model.toJSON()); $(‘#a', this.$el).html(this.model.get(‘a')); $(‘#b', this.$el).html(this.model.get(‘b')); } });
在这里,任何对模型的改变都会触发对视图的一个全面的重新渲染。我第一次用Backbone.js开发时,我是这个模式的实践者。但随着视图代码的增长,我迅速的意识到,这种方法不利于维护或优化,因为当模型的任何一个属性发生变化时,视图将会完全的重新渲染。
当我遇到这个问题,我迅速的用Google搜索了一下,看看别人是怎么做的,结果找到了Ian Storm Taylor的博客,“分解你的Backbone.js渲染方法”,他在其中描述了在模型中监听单独的属性变化,然后仅仅重新渲染相对于变化属性的视图部分。Taylor也描述了返回对象的引用,以便单独的渲染函数可以很容易的链接在一起。上面的例子现在现在就变得更易于维护,性能更优。因为我们仅仅更新了模型变化的属性相对应的视图部分。
var View = Backbone.View.extend({ initialize: function(options) { this.model.on('change:a', this.renderA, this); this.model.on('change:b', this.renderB, this); }, renderA: function() { $(‘#a', this.$el).html(this.model.get(‘a')); return this; }, renderB: function() { $(‘#b', this.$el).html(this.model.get(‘b')); return this; }, render: function() { this .renderA() .renderB(); } });
我应该说一下有许多插件,比如Backbone.StickIt和Backbone.ModelBinder,提供了模型属性与视图元素的键-值绑定,这会让你省去编写许多样板代码,如果你具有复杂的表单字段检验一下它们。
保持模型与视图无关
正如 Jeremy Ashkenas 在 Backbone.js的 GitHub问题 之一中所指出的,Backbone.js 并没有实施数据与视图层之间关注点的任何真正分离,除非模型未引用视图而创建。因为Backbone.js并没有执行一个关注点分离,所以你应该将其分离吗?我和许多其他的Backbone.js开发人员,如Oz Katz 和 Dayal ,都相信答案毫无疑问是yes:模型与集合,也就是数据层,应该彻底的与绑定到它们的视图无关,保持一个清晰的关注点分离。如果你没有遵循关注点分离,你的基础代码将很快变成意大利面条式的代码,而没有人喜欢意大利面条式的代码。
保持模型与视图无关将会帮助你预防意大利面条式的代码,而没有人喜欢意大利面条式的代码!
保持你的数据层彻底的与视图层无关,这将会使你创建出更具模块化,可复用与可维护的基础代码。你可以非常容易的在应用程序各个地方复用与扩展模型和集合,而不需要考虑它们所绑定的视图。遵循这个模式使对你项目不熟悉的开发者能迅速的深入到基础代码之中,因为他们会确切的知道哪里发生了渲染,哪里存在有你的应用的所有商务逻辑。
这个模式也执行了单一职责原则,规定了每个类应该具有一个单一的职责,而且它的职责应该封装与这个类之中,因为模型与集合要处理数据,而视图要处理渲染。
路由中的参数
最好的演示这个模式工作方式是举个例子。比如说你需要对搜索页面进行排序,每个搜索页面都允许用户添加两个不同的过滤类型foo和bar,每个类型代表不同的观点。
因此,你的URL结构将会呈现如下:
'search/:foo' 'search/:bar' 'search/:foo/:bar'
现在,所有的路由都用的是同一个试图和模型,这样大多数人喜欢用同一个函数search()来实现。然而,你要是检查过Backbone.js代码的话,你会发祥它里面没有排序的参数映射;这些参数只是从左至右依次传入函数。这样,为了都能统一使用一个函数,你就要停止创建不同的函数正确的来为search()匹配参数。
routes: { 'search/:foo': 'searchFoo', 'search/:bar': 'searchBar', 'search/:foo/:bar': 'search' }, search: function(foo, bar) { }, // I know this function will actually still map correctly, but for explanatory purposes, it's left in. searchFoo: function(foo) { this.search(foo, undefined); }, searchBar: function(bar) { this.search(undefined, bar); },
你也许能想象的到,这个模式可以使路由功能很快膨胀。当我第一次遇到这个问题时,我试图创建了一些用正则表达式定义的解析函数来“神奇”的去匹配参数,当然这个是可以工作的-但这也是有约束条件的。这样,我废弃了这个想法(有时,我仍然可以用Backbone插件来解决)。我进入GitHub中的一个 议题,其中Ashkenas建议应该让所有的参数都和search函数匹配。
上面的代码现在转变为下面维护性更强的样子:
routes: { 'search/:foo': 'searchFoo', 'search/:bar': 'searchBar', 'search/:foo/:bar': 'search' }, search: function(foo, bar) { }, // I know this function will actually still map correctly, but for explanatory purposes, it's left in. searchFoo: function(foo) { this.search(foo, undefined); }, searchBar: function(bar) { this.search(undefined, bar); },
这种模式可以戏剧性的减少路由的过分膨胀。然而,需要注意到它不会服务于不能区别的参数。比如,如果你有两个作为ID的参数,如模式XXXX-XXXX,你不能区分哪个ID是对哪个参数的回应。
model.fetch() 不会清除你的模型
这通常会将那些Backbone.js的新手给绊倒:model.fetch()并不能丢掉你的模型,而是扩展了你的模型的属性。因此,如果你的模型具有属性x,y和z,你获取到y和z,那么x将仍然是模型中的那个x,只有y和z会被更新。下面的例子将这个概念形象化了。
var Model = Backbone.Model.extend({ defaults: { x: 1, y: 1, z: 1 } }); var model = new Model(); /* model.attributes yields { x: 1, y: 1, z: 1 } */ model.fetch(); /* let's assume that the endpoint returns this { y: 2, z: 2, } */ /* model.attributes now yields { x: 1, y: 2, z: 2 } */
PUTs 需要一个 ID 属性
这一条也经常将Backbone.js的新手绊倒。要想在调用.save()的时候让模型发送一个HTTP PUT请求,你的模型需要有一个ID属性集。记得HTTP PUT谓词是设计来做更新的吧,所以发送一个PUT请求,你的模型需要有一个ID,这么做是有意义的。在理想的世界里,你的所有模型都具有一个名为ID的完美的ID属性,但是你从端点接收到的JSON数据可能并不总是具有完美命名的IDs。
因此,如果你需要更新一个模型,请在保存之前确认模型上有ID。Backbone.js 的0.5以及更高版本允许你用id属性来更新模型的ID属性名称,如果你的端点返回的不是名为id的IDs的话。
如果困顿于使用的是版本低于0.5的Backbone.js,我建议你修改你的模型或集合的parse函数,以便将你期望的ID属性映射到属性ID。这里有一个快速上手的例子,说明了你应怎样修改parse函数来做到这一点。我们假设你有一个cars的集合,它的IDs是carID。
parse: function(response) { _.each(response.cars, function(car, i) { // map the returned ID of carID to the correct attribute ID response.cars[i].id = response.cars[i].carID; }); return response; },
页面加载时创建模型数据
有时你会发现你的模型或者集合需要在页面加载时被初始化赋值。许多关于Backbone.js模式的文章,例如Rico Sta Cruz的 “Backbone 模式” 和 Katz的 “ 避免常见的Backbone.js陷阱” ,讨论了这种模式。这种模式实现很容易,只需在页面中内联一段脚本,通过你选择的服务端语言,将单个模型属性或者JSON形式的数据呈现出来。例如,在Rails语言中,我采用下面方法之一:
// a single attribute var model = new Model({ hello: <%= @world %> }); // or to have json var model = new Model(<%= @hello_world.to_json %>);
应用这种模式可以通过“立即的”渲染页面,改善你的搜索引擎排名,而且它也可以通过限制应用初始化HTTP请求的方式,大大缩短你的应用启动与运行所需要的时间。
处理失败的模型属性验证
很多时候,你会想知道是哪个模型属性验证失败了。例如,如果你有一个极其复杂的表单,你或许想知道哪个模型属性验证失败,这样你就可以将这个属性对应的输入字段高亮显示。不幸的是,提醒视图到底是哪个模型属性验证失败并没有直接集成于Backbone.js,但是你可以用一些不同的模式去处理这个问题。
返回一个错误对象
一个给视图提醒哪个模型属性验证失败的模式是,返回一个对象,其中包含某种标志,它详细的记录了哪个属性验证为失败,就像下面这样:
// Inside your model validate: function(attrs) { var errors = []; if(attrs.a < 0) { errors.push({ 'message': 'Form field a is messed up!', 'class': 'a' }); } if(attrs.b < 0) { errors.push({ 'message': 'Form field b is messed up!', 'class': 'b' }); } if(errors.length) { return errors; } } // Inside your view this.model.on('invalid', function(model, errors) { _.each(errors, function(error, i) { $(‘.' + error.class).addClass('error'); alert(error.message); }); });
这个模式的优点在于,你是在一个地方处理所有不合法的消息。缺点在于,如果你以不同的方式处理不合法的属性的话,你的invalid方法可能会成为一个很大的switch或者if语句。
广播自定义Error事件
我的一个朋友,Derick Bailey,推荐了一个可替代模式,就是为每个模型属性触发自定义的errors事件。这将允许你的视图能够针对单独的属性绑定到特定的error事件:
// Inside your model validate: function(attrs) { if(attrs.a < 0) { this.trigger(‘invalid:a', 'Form field a is messed up!', this); } if(attrs.b < 0) { this.trigger(‘invalid:b', 'Form field b is messed up!', this); } } // Inside your view this.model.on('invalid:a', function(error) { $(‘a').addClass('error'); alert(error); }); this.model.on('invalid:b', function(error) { $(‘b').addClass('error'); alert(error); });
这个模式的优点在于,你的视图明确的绑定到它们所绑定到的error类型,而且如果你对每一种属性error有特定的指令的话,它可以清理你的视图部分代码,使之更易于维护。这个模式的一个不好的地方在于,如果在你处理不同的属性error时并没有太多的不同的话,你的视图可能会变得极为膨胀。
这两种模式都有其利弊,你应该考虑清楚哪个模式对你的应用案例是最优的。如果你按照同样的方式处理所有失败的验证,那么第一个方法可能是最好的;如果你对每个模型属性有特定的UI变化,那么后一种方法更好。
HTTP状态代码200所触发的错误
如果你的浏览器端模型或者集合收到了无效的JSON,尽管HTTP的状态代码是200,但浏览器端依然会触发一个“错误”事件。这种事件常发生于本地模拟JSON数据造成的。那么,一个好的方法就是读取经过 JSON 验证器验证了的模拟JSON数据文件。或者从你的IDE获得相应的 插件来及时获取格式错误的JSON信息。
创建一个一般性错误显示模式
创建一个常见错误显示代码可以节省你的时间以及创建一个统一的模式来处理、可视化错误信息,而且它可以增加开发者的经验。我之前开发的每一个Backbone.js应用中我都会创建一个可以处理alert的视图:
var AlertView = Backbone.View.extend({ set: function(typeOfError, message) { var alert = $(‘.in-page-alert').length ? $(‘.in-page-alert'): $(‘.body-alert'); alert .removeClass(‘error success warning') .addClass(typeOfError) .html(message) .fadeIn() .delay(5000) .fadeOut(); } });
上面的代码首先会检查是否已在视图代码中创建了指定视图in-page-alert div。如果没有,则接着查看一般性的在其它地方声明的body-alert div。这样可以让你发送具有一致性的错误信息以及当你忘记指定一个in-page-alert div时提供有用且可靠的信息。如下面的模式简化了让你怎样在你的试图中处理错误信息:
var alert = new AlertView(); this.model.on('error', function(model, error) { alert.set('TYPE-OF-ERROR', error); });
单页面应用中更新浏览器页面标题
这是一个比任何东西都重要的可用性问题。如果你正在开发一个单页面应用程序,谨记更新每个页面的标题。我写过一个的插件(Backbone.js Router Title Helper)来扩展 backbone.js router 的功能。它通过一个 Map 对象来控制路由,键来代表路由函数的名字,值则映射到页面的标题。
Backbone.Router = Backbone.Router.extend({ initialize: function(options){ var that = this; this.on('route', function(router, route, params) { if(that.titles) { if(that.titles[router]) document.title = that.titles[router]; else if(that.titles.default) document.title = that.titles.default; else throw 'Backbone.js Router Title Helper: No title found for route:' + router + ' and no default route specified.'; } }); } });
单页面应用中的缓存对象
当我们谈论单页面应用时,另一个叫缓存对象模式你将会经常用到!下面的例子直截了当而且简单:
// Inside a router initialize: function() { this.cached = { view: undefined, model: undefined } }, index: function(parameter) { this.cached.model = this.cached.model || new Model({ parameter: parameter }); this.cached.view = this.cached.view || new View({ model: this.cached.model }); }
这个模式可以加速你得应用,因为你不用重复初始化你得Backbone.js对象。然而,它会过多的消耗内存;所以,缓存对象就要在整个应用中使用。如果以前你用过Backbone.js开发过应用,也许你会问你自己,“ 我要重取数据该怎么做?”你可以每次在如下路径中触发后重取数据:
// Inside a router initialize: function() { this.cached = { view: undefined, model: undefined } }, index: function(parameter) { this.cached.model = this.cached.model || new Model({ parameter: parameter }); this.cached.view = this.cached.view || new View({ model: this.cached.model }); this.cached.model.fetch(); }
当你的应用从端点(如,一个收件箱)必须检索最新数据时上面的模式就可以工作。当然,如果你要拿的数据时凭借应用的某个状态(假设这个状态是通过URL和参数来决定的),甚至是在用户上一个页面应用的状态没有改变, 你可以重取数据。一个好的解决方案去重拿数据时当应用(参数)发生变化时:
// Inside a router initialize: function() { this.cached = { view: undefined, model: undefined } }, index: function(parameter) { this.cached.model = this.cached.model || new Model({ parameter:parameter }); this.cached.model.set('parameter', parameter); this.cached.view = this.cached.view || new View({ model: this.cached.model }); } // Inside of the model initialize: function() { this.on("change:parameter", this.fetchData, this); }
JSDoc函数和Backbone.js类
我是文档注释和JSDoc的超级粉丝。我用JSDoc对所有的Backbone类添加了文档注释:
var Thing = Backbone.View.extend(/** @lends Thing.prototype */{ /** @class Thing * @author Phillip Whisenhunt * @augments Backbone.View * @contructs Thing object */ initialize() {}, /** Gets data by ID from the thing. If the thing doesn't have data based on the ID, an empty string is returned. * @param {String} id The id of get data for. * @return {String} The data. */ getDataById: function(id) {} });
如果你對Backbone類別進行如上添加文檔註釋,這樣你可以為所有類別和函數 添加參數、返回類型以及描述文檔註釋了。確保保持初始化函數作為一個宣告的函數,這樣可以幫助我們產生JSDoc。如果你想看JSDoc的範例工程,那就在 HomeAway Calendar Widget下載範例。同時這裡也有個 Grunt.js插件, grunt-jsdoc-plugin,這個也可以作為你建立文件註解時的一部分。
聯絡測試驅動的開發模式
我認為如果你用Backbone.js,你應該在開發模型和集合時遵循測試驅動開發(TDD)。我第一次用Jasmine.js建立模型和集合時遵循TDD進行單元測試,但失敗了。一旦寫下單元測試並且失敗,我會對整個模型和集合進行重寫。
通過這一點,我的所有Jasmine測試都通過了,而且我有信心我的模型和集合會和我期望的一樣工作。自從我遵循TDD,我的視圖層非常容易寫而且非常簡單。當你開始用TDD時,你得速度當然會很慢;但一但你得腦海裡一直想著TDD,你的程式效率和品質會神奇般的提升。