这篇文章主要介绍了 Backbone.js 的一些使用技巧, Backbone.js 是一款人气 JavaScript 库, 需要的朋友可以参考下
Backbone 为复杂 Javascript 应用程序提供模型 (models)、集合(collections)、视图(views) 的结构。其中模型用于绑定键值数据和自定义事件;集合附有可枚举函数的丰富 API; 视图可以声明事件处理函数,并通过 RESTful JSON 接口连接到应用程序。
自从 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,你的编程效率和质量会神奇般的提高。
来源: