什么是 Backbone.js
Backbone.js 是一个在JavaScript环境下的 模型-视图-控制器 (MVC) 框架。任何接触较大规模项目的开发人员一定会苦恼于各种琐碎的事件回调逻辑、以及金字塔般的代码。而且,在传统的Web应用程序代码中,不可避免的都有在应用逻辑中加入显示数据的代码的情况。当项目规模愈发变大时,这种形式的代码变得越发的难以维护,因为任何在主干逻辑中的变更都可能影响到数据显示逻辑,反之亦然。
Backbone就是要来解决这样的代码耦合的问题。它通过提供一个控制层-显示层的框架,以及模版(template)来分离各自逻辑。这样的MVC框架类似于传统意义上桌面程序以及服务器端程序的程序框架。
模型 Model
运用Backbone框架的程序的核心是“模型”。多数情况下,“模型”代表了数据库中存储的一种对象结构。
Backbone的设计思想是,让模型服务于存储、获取和更改数据。注意,有另外一些MVC框架的设计是让控制器负责更改数据。可是backbone的设计思想是,控制器的作用仅仅是用来处理从视图层来的用户请求、以及访问相对应的模型层。而模型层自身则需要负责(从数据源中)获取数据、以及数据封装。
下面的例子阐释了在Backbone里,数据模型是怎样声明和初始化的。
Stooge = Backbone.Model.extend({ defaults: { 'name': 'Guy Incognito', 'power': 'Classified', 'friends': [], }, initialize: function () { // Do initialization } }); var account = new Stooge({ name: 'Larry', power: 'Baldness', friends: ['Curly', 'Moe'] });
在backbone中,要创建“模型”,可以扩展 Backbone.Model 并且提供实例的属性。在上述例子中, extend 函数给 Stooge 类建立了一个原型链,因此只需要通过Stooge你就可以访问 模型 中的所有属性。并且,由于 extend 正确的设置了原型链,因此通过 extend 创建的子类 (subclasses) 也可以被深度扩展。Extend在backbone中是很重要的概念,在多数JavaScript库里面,extend函数用来处理从一个对象复制到另一个对象。在backbone里面,extend函数同样也会创建一个构造函数,因此你可以用来初始化一个类,再把它复制到新的类,从而达到多层扩展。
视图 View
视图表示了在一种“模型”的基础上展示数据的方法。根据应用逻辑上下文的不同,视图决定了怎样展示数据。比如,在加拿大,一个公民可以选择使用短格式的出生证明或者是较长格式的出生证明。两种出生证明文档都包括了一样的关键信息(Model),但是细节程度不同(View)。在Backbone里,视图提供了一个“窗口”来查看一个模型中的数据、协助监听在界面上的用户交互或者数据模型上的变化,从而触发数据显示的更新。
下面的是在网页中显示出生证明数据的例子。
<div id="certificate"></div>
<script type="text/javascript"> CertificateView = Backbone.View.extend({ initialize: function () { this.render(); }, render: function () { $(this.el).html("<h1>Guy Incognito</h1><p>DOB: March 2, 1967</p>"); } }); var certificate_view = new CertificateView({ el: $("#certificate") }); </script>
在上述代码中,声明了一个 CertificateView 类,作为出生证明的视图实例。它同样使用了backbone的extend方法来扩展了一个定制的视图结构,也就是出生证明这个结构(使用extend方法就跟上一节中的扩展“模型”一样)。这个定制的视图包含了两个方法:
initialize 方法 —— 当一个该视图结构的实例被创建时,这个方法被执行。在上述代码示例中,initialize要做的就是立即触发render函数来处理数据显示。
render方法 —— 调整DOM节点里面的内容,达到显示数据的目的。
在backbone里,所有的“视图”都不可避免的要跟DOM树操作打交道。在上述代码里,我们实例化视图时,传入了一个需要显示数据的DOM节点。如果你没有传入任何DOM节点,那么backbone将会处理页面上所有的div节点。
视图模板 View Template
上一节的代码有一个潜在的问题:在JavaScript代码里出现了生成HTML代码的逻辑。因此,我们需要使用“视图模板”来分离耦合的处理逻辑。注意,Backbone里的 View 并不包含template本身,它只提供view的控制逻辑,而具体生成template的功能是靠其依赖库 underscore 的 .template() 方法实现的。
具体的说,我们把生成HTML的逻辑从视图层的render函数中移了出来,把它放在一个<script>标签下。为了防止浏览器把它当作真正的JavaScript代码去处理它,<script>的type属性被设置成 text/template (而不是默认的 text/javascript)。如果不重新声明默认的type属性,那么浏览器就会用JS的引擎去处理那里面的内容,而由于视图模板常常是HTML代码段,所以通常会抛出JS语句解析错误。
<div id="certificate"></div>
<script type="text/template" id="tpl-certificate"> < h1 > <%= name %> < /h1> <p> DOB: <%= dob %> </p > </script>
<script type="text/javascript"> CertificateView = Backbone.View.extend({ template: _.template($('#tpl-certificate').html()), initialize: function () { this.render(); }, render: function () { var templateArgs = { name: "Guy Incognito", dob: "March 2, 1967" }; $(this.el).html(this.template(templateArgs)); } }); var certificate_view = new CertificateView({ el: $("#certificate") }); </script>
在上述代码中,name和date被当作视图模板的参数变量。在模板里使用变量时,需要加上<% 和 %>标签。如果需要输出变量,则需要使用 <%= 标签,跟JSP的语法一样。
在视图中使用模板需要注意两步:第一步,视图逻辑必须知道采用哪一个模板来渲染数据;第二步,使用模板时需要传入变量参数。
我们使用了jQuery的 .html() 函数来获取<script>标签下的模板内容。 接着,我们又使用了 underscore 的 .template() 函数 根据参数变量和模板原文 来组合生成最终要显示的HTML代码。
现在,使用 实例名.render() 就可以实现数据显示的工作了。 在render函数里,首先声明了需要传入模板的参数变量(大多数情况下,这个需要从模型层获取),接着使用template函数生成HTML代码,最后使用 jQuery.html()更改DOM节点中的内容。
集合 Collection
在Backbone里,集合是模型Model的有序组合,我们可以在集合上绑定 "change" 事件,从而当集合中的模型发生变化时获得通知,集合也可以监听 "add" 和 “remove" 事件, 从服务器更新。同时,你也可以使用 Underscore 提供的方法(例如 forEach, filter, sortBy等等)。
你可以通过扩展 Backbone.Collection 创建一个 集合。
var Library = Backbone.Collection.extend({ model: Book });
尽管使用集合有开销,为什么还要使用集合而不使用JavaScript内置的数组? 首先,通过扩展Backbone.Collection的方式来创建对象有利于“文档化”你的代码。例如,在下面的例子中,集合类 Team 定义了 Stooge 作为它的数据模型类型,所以当你书写代码时,你就知道backbone在处理该集合时是依照怎样的数据类型结构。 当然,使用集合更重要的原因是,集合提供了一套强大的命令让你 获取/操纵 里面的内容,因此你不需要自己写对应的方法。
var Stooge = Backbone.Model.extend({ defaults: { 'name': '', 'power': '' } }); var Team = BackBone.Collection.extend({ model: Stooge }); var larry = new Stooge({ name: 'Larry', power: 'Baldness' }); var moe = new Stooge({ name: 'Moe', power: 'All Powers' }); var curly = new Stooge({ name: 'Curly', power: 'Hair' }); var threeStooges = new Team([larry, curly, moe]);
当集合中包含的“模型”发生数据变化时,集合会激发相应的事件,因此你可以捕获那些事件从而完成视图上的更新。
同步 Sync
在backbone中,使用 Sync 类(Backbone.sync)完成与服务器端的数据模型的读取与存储。 通常情况下,你可以用 jQuery的 .ajax 方法来发送和接收 JSON 格式的数据。如果想采用不同的持久化方案,比如 WebSockets, XML, 或 Local Storage,我们可以重载该函数。
Backbone.sync 的语法为 sync(method, model, [optional callback])。
method – CRUD 方法 ("create", "read", "update", 或 "delete") create对应POST,read 对应GET。
model – 要被保存的模型(或要被读取的集合)
与Sync配套使用最常见的方法是 model.save([attributes], [options])。 model.save 通过委托 Backbone.sync 保存数据模型到服务器端。 attributes 散列表 (在 set) 应当包含想要改变的属性,不涉及的键不会被修改。 如果模型含有 validate 方法,并且验证失败,模型不会保存。 如果模型在服务器端不存在, save将采用 "create" (HTTP POST) 方法, 如果模型已经在服务器存在,保存将采用 "update" (HTTP PUT) 方法.
在下面的示例,我们在client端生成一个图书的数据模型,并把它sync到服务器端。
Backbone.sync = function (method, model) { alert(method + ": " + JSON.stringify(model)); model.id = 1; }; var book = new Backbone.Model({ title: "The Rough Riders", author: "Theodore Roosevelt" }); book.save(); 由于模型在服务器端没有,所以这一次调用,sync会使用 "create" 请求 book.save({ author: "Teddy" }); 这一次服务器端有了,所以sync会使用 "update"请求。注意,这只更新了模型的部分属性。没有涉及的属性在服务器端不会被修改。
路由 Router
web应用程序通常需要为应用的重要位置提供可链接,可收藏,可分享的 URLs。 越来越多的web程序开始使用 锚点(hash)片段(#page)来提供这种可收藏,可分享的链接。 同时随着 History API 的到来,锚点已经可以用于处理标准 URLs (/page)。 Backbone.Router 为客户端路由提供了许多方法,并能连接到指定的动作(actions)和事件(events)。 对于不支持 History API 的旧浏览器,路由提供了优雅的回调函数并可以透明的进行 URL 片段的转换。
看上去,在 Backbone 里似乎缺少了 Controller 类型,实际上,每一个“路由”都可以被看作是从 Controller 上扩展的。 传统意义上的 Controller 是有问题的,因为它融合了两种功能,第一就是根据 View 来控制 Model,第二就是控制用户在不同的 View 之间切换。而 Backbone 通过把 Router 单独剥离出来,就优雅地分离了 用户界面 和 操纵Model。
那为什么不给 controller 建立单独的类呢?根据MVC定义,Controller类应该是你程序主干逻辑的实现。在backbone里,由于程序的具体处理细节都从 controller 层分离出去了(比如 在Model内部处理数据,由Router处理用户request等等),因此你不太需要单独的一个 Controller 类来处理程序的主干逻辑。
下面这个例子显示了如何设置一个路由,该路由需要处理的URL诸如 #/certificates/123 或者 #/certificates/mycertificatename,然后该路由依据certificateID或者certificatename 新建一个 View (new CertificateView)。 下面这个简单的代码并没有依据 ID 处理CertificateView,但是实际开发中,你可能需要依据ID来 读取/处理 模型。
var MyRouter = Backbone.Router.extend({ routes: { "/certificates/:id": "getCertificate", }, getCertificate: function (id) { new CertificateView({ el: $("#certificate") }); } }); var router = new MyRouter; Backbone.history.start();
参考资料:
Backbone中文文档 http://www.csser.com/tools/backbone/