Backbone框架浅析
Backbone是前端mvc开发模式的框架。它能够让view和model相分离,让代码结构更清晰简答,开发进度加快,维护代码方便。但是,现在出了一种mvvm框架,它是下一代前端mvc开发模式的框架,代表作是Angular.js,改天有时间去研究下。现在先来研究下Backbone框架。
Backbone提供了Model, Collection, View,Events,Controller(Router)。Model 用来创建数据,校验数据,绑定事件,存储数据到服务器端;View 用来展示数据。如此这般,在前端也做到了数据和显示分离。Backbone依赖于Underscore.js,这是一个有很多常用函数的js文件。同时依赖jQuery等库来操作DOM和Ajax请求。
1. Backbone.Events
Events可以被添加到任何一个javascript对象中,一旦对象与Events合体,就可以自定义事件了。
var obj = {}; //js对象
_.extend(obj, Backbone.Events); //把Backbone.Events扩展到obj对象中。这时这个对象就拥有操作事件的方法了。_是underscore.js的对象,相当于jquery.js中的$。
obj.bind('data', function(data) {
console.log('Receive Data: ' + data);
});
obj.trigger('data', 'I/'m an Backbone.event'); //打印Receive Data: I'm an Backbone.event
obj.unbind('data');
obj.trigger('data', 'I/'m an Backbone.event');
另外,如果事件很多,可以给事件加上命名空间,例如"change:selection"。属性事件会先于正常事件触发。比如:
我们先监听了change事件,然后再监听了change:name属性事件,但change事件(改变name的值)在触发时,总是会先触发属性事件,然后再触发change事件。如果改变的不是name的值而是其他的值,这里只会触发change事件,而不会触发change:name属性事件。
2. Backbond.Controller(新版本是Router)
Backbone提供了前端的url#fragment的路由支持,并且可以把他们绑定到Action和Event中去。
注意:在使用前端路由的功能之前,一定要调用一次Backbone.history.start()。
var controller = Backbone.Controller.extend({
routes: {
"": "home",
"!/comments": "comments",
"!/mentions": "mentions",
"!/:uid": "profile",
"!/:uid/following": "following",
"!/:uid/followers": "followers",
"!/:uid/status/:id": "status",
"!/search/users/:query": "user_search",
"!/search/:query": "search"
},
initialize: function(){...} ,
home: function(){...} ,
comments: function() {...} ,
mentions: function() {...} ,
profile: function(a) {...} ,
status: function(a, b) {...} ,
following: function(a) {...} ,
followers: function(a) {...} ,
user_search: function(a) {...} ,
search: function(a) {...}
});
var custom = new controller();
Backbone.history.start();
这时当页面URL HASH发生变化时,就会执行所绑定的方法。
Backbone.Controller.extend({}) 的用法。{}里面要有一个routes的哈希表,提供了路由和方法名的键值对。
所以我们看到http://shuo.douban.com/#!/comments对应着下面的comments方法。这个页面对应着“最新回复”模块。
我们还可以看到#fragment里面有!这个符号,这个是给搜索引擎识别用的,我们下次在谈。另外,还有:uid, :query, :id这些符号,这是动态的参数,uid、query、id都是这些参数的参数名,因此
http://shuo.douban.com/#!/search/users/豆瓣
就对应着"!/search/users/:query": "user_search",这个路由,继而可以用user_search()来处理。
顺便提一句,当url匹配后,会触发一个和Action名字有关的事件,比如"!/comments": "comments",如果访问了http://shuo.douban.com/#!/comments,就会触发"route:comments"的事件.
Backbone默认会通过Hash的方式来记录地址的变化,对于不支持onhashchange的低版本浏览器,会通过setInterval心跳监听Hash的变化,因此你不必担心浏览器的兼容性问题。
如果你的项目并不复杂,但你却深深喜欢它的某个特性(可能是数据模型、视图管理或路由器),那么你可以将这部分源码从Backbone中抽取出来,因为在Backbone中,各模块间的依赖并不是很强,你能轻易的获取并使用其中的某一个模块。
3. Backbone.View
View并不操作html或者css,所有的操作留给了各种各样的JS模板库。View有两个作用:1.监听事件.2.展示数据.
var view = Backbone.View.extend({
model: User, //这个View的模型
className: "components cross",
template: $("#user-info-template").html(),
initialize: function() { //new view({})就会调用这个初始化方法
_.bindAll(this, "render");
this.model.bind("change", this.render) //模型User绑定change事件
},
render: function() {
var a = this.model;
$(this.el).html(Mustache.to_html(this.template, a.toJSON())); //使用了Mustache模板库,来解析模板,把模型User中的数据,转换成json,显示在模板中
$(this.el).find(".days").html(function() { //再进行细微的改变
var b = a.get("created_at"); //取到模型User中的created_at的值
return b;
});
return this ;
}
});
在initialize中,一旦User类(模型)触发了change事件就会执行render方法,继而显示新的视图。
render方法中总是有个约定俗称的写法的。this.el是一个DOM对象,render的目的就是把内容填到this.el中。this.el会根据view提供的tagName, className, id属性创建,如果一个都没有,就会创建一个空的DIV。
更新完this.el后,我们还应该return this;这样才能继续执行下面的链式调用(如果有的话)。
我们也可以用$(view.el).remove()或者view.remove()很方便的清空DOM。
View层有一个委托事件的机制。
var view = Backbone.View.extend({
className: "likers-manager",
template: $("#likers-components-template").html(), //模板HTML
events: {
"click .btn-more": "loadMore"
},
initialize: function() { //new view({}),就会调用
_.bindAll(this, "render", "updateTitle", "loadOne", "loadAll", "loadMore"); //调用underscore的bingAll方法
},
render: function() { ... } ,
updateTitle: function() { ... } ,
loadOne: function(a) { ... } ,
loadAll: function() { ... } ,
loadMore: function(a) { ... }
});
在这里面有个events的键值对,格式为{"event selector": "callback"},其中click为事件,.btn-more是基于this.el为根的选择器,这样一旦约定好,当用户点击.btn-more的元素时,就会执行loadMore方法
4. Backbone.Model
Model 用来创建数据,校验数据,存储数据到服务器端。Models还可以绑定事件。比如用户动作变化触发 model 的 change 事件,所有展示此model 数据的 views 都会接收到 这个 change 事件,进行重绘。
最简单的定义如下:
var Game = Backbone.Model.extend({});
稍微复杂一点
var Game = Backbone.Model.extend({
initialize: function(){
},
defaults: {
name: 'Default title',
releaseDate: 2011,
}
});
initialize 相当于构造方法,初始化时调用(new时调用)
简单实用:
var portal = new Game({ name: "Portal 2", releaseDate: 2011});
var release = portal.get('releaseDate');
portal.set({ name: "Portal 2 by Valve"});
此时数据还都在内存中,需要执行save方法才会提交到服务器。
portal.save();
5. Backbone.Collection(集合)
实际上,相当于Model的集合。
需要注意的是,定义Collection的时候,一定要指定Model。 下面让我们为这个集合添加一个方法,如下:
var GamesCollection = Backbone.Collection.extend({
model : Game,
old : function() {
return this.filter(function(game) {
return game.get('releaseDate') < 2009;
});
}
});
集合的使用方法如下:
var games = new GamesCollection
games.get(0);
当然,也可以动态构成集合,如下:
var GamesCollection = Backbone.Collection.extend({
model : Game,
url: '/games'
});
var games = new GamesCollection
games.fetch();
这边的url告诉collection到哪去获取数据,fetch方法会发出一个异步请求到服务器,从而获取数据构成集合。(fetch实际上就是调用jquery的ajax方法)
模板解析是Underscore中提供的一个方法。且Underscore是Backbone必须依赖的库。
模板解析方法能允许我们在HTML结构中混合嵌入JS代码,就像在JSP页面中嵌入JAVA代码一样:
<ul>
<% for(var i = 0; i < len; i++) { %>
<li><%=data[i].title%></li>
<% } %>
</ul>
通过模板解析,我们不需要在动态生成HTML结构时,使用拼接字符串的方法,更重要的是,我们可以将视图中的HTML结构独立管理(例如:不同的状态可能会显示不同的HTML结构,我们可以定义多个单独的模板文件,按需加载和渲染即可)。
在Backbone中,你可以使用on或off方法绑定和移除自定义事件。在任何地方,你都可以使用trigger方法触发这些绑定的事件,所有绑定过该事件的方法都会被执行,如:
var model = new Backbone.Model();
model.on('custom', function(p1, p2) {
});
model.on('custom', function(p1, p2) {
});
model.trigger('custom', 'value1', 'value2'); //将调用以上绑定的两个方法
model.off('custom');
model.trigger('custom');
// 触发custom事件,但不会执行任何函数,已经事件中的函数已经在上一步被移除
如果你熟悉jQuery,你会发现它们与jQuery中的bind、unbind和trigger方法非常类似。
在单页应用中,我们通过JavaScript来控制界面的切换和展现,并通过AJAX从服务器获取数据。
可能产生的问题是,当用户希望返回到上一步操作时,他可能会习惯性地使用浏览器“返回”和“前进”按钮,而结果却是整个页面都被切换了,因为用户并不知道他正处于同一个页面中。
对于这个问题,我们常常通过Hash(锚点)的方式来记录用户的当前位置,并通过onhashchange事件来监听用户的“前进”和“返回”动作,但我们发现一些低版本的浏览器(例如IE6)并不支持onhashchange事件,只有可以使用setInterval。
Underscore还提供了一些非常实用的函数方法,如:函数节流、模板解析等。Underscore是Backbone必须依赖的库,因为在Backbone中许多实现都是基于Underscore。
相信你对jQuery一定不会陌生,它是一个跨浏览器的DOM和AJAX框架。
而对于Zepto你可以理解为“移动版的jQuery”,因为它更小、更快、更适合在移动终端设备的浏览器上运行,它与jQuery语法相同,因此你能像使用jQuery那样使用它。
服务器提供的数据接口需要兼容Backbone的规则,对于一个新的项目来说,我们可以尝试使用这套规则来构建接口。但如果你的项目中已经有一套稳定的接口,你可能会担心接口改造的风险。
没关系,我们可以通过重载Backbone.sync方法来适配现有的数据接口,针对不同的客户端环境,我们还可以实现不同的数据交互方式。例如:用户通过PC浏览器使用服务时,数据会实时同步到服务器;而用户通过移动终端使 用服务时,考虑到网络环境问题,我们可以先将数据同步到本地数据库,在合适的时候再同步到服务器。而这些只需要你重载一个方法就可以实现。
Model是Backbone中所有数据模型的基类,用于封装原始数据,并提供对数据进行操作的方法,我们一般通过继承的方式来扩展和使用它。
Backbone中的Model就像是映射出来的一个数据对象,它可以对应到数据库中的某一条记录,并通过操作对象,将数据自动同步到服务器数据库。(Collection就像映射出的一个数据集合,它可以对应到数据库中的某一张或多张关联表)。
整个Backbone的源码用一个自调用匿名函数包裹,避免污染全局命名空间。
(function() {
Backbone.Events // 自定义事件
Backbone.Model // 模型构造函数和原型扩展
Backbone.Collection // 集合构造函数和原型扩展
Backbone.Router // 路由配置器构造函数和原型扩展
Backbone.History // 路由器构造函数和原型扩展
Backbone.View // 视图构造函数和原型扩展
Backbone.sync // 异步请求工具方法
var extend = function (protoProps, classProps) { ... } // 自扩展函数
Backbone.Model.extend = Backbone.Collection.extend = Backbone.Router.extend = Backbone.View.extend = extend; // 自扩展方法
}).call(this);
Backbone 会自动判断浏览器对 pushState 的支持,以做内部的选择。 不支持 pushState 的浏览器将会继续使用基于锚点的 URL 片段。
Events是Backbone中所有其它模块的基类,无论是Model、Collection、View还是Router和History,都继承了Events中的方法( unbind,bind,on,off,trigger,stopListening )。
我们无法直接实例化一个Events对象。
你需要注意监听函数的调用顺序,all事件总会在其它事件中的监听函数都执行完毕之后触发,同一个事件中如果绑定了多个监听函数,那它们将按照函数绑定时的顺序依次调用。
实际上我们一般并不会重载模块类的constructor方法,因为在Backbone中所有的模块类都提供了一个initialize方法,用于避免在子类中重载模块类的构造函数,当模块类的构造函数执行完成后会自动调用initialize方法。模型的方法:
- get()方法用于直接返回数据
- escape()方法先将数据中包含的HTML字符转换为实体形式(例如它会将双引号转换为"形式)再返回,用于避免XSS攻击。
- previous()方法接收一个属性名,并返回该属性在修改之前的状态;
- previousAttributes()方法返回一个对象,该对象包含上一个状态的所有数据。
需要注意的是,previous()和previousAttributes()方法只能在数据修改过程中调用(即在模型的change事件和属性事件中调用)。
在调用模型的unset()和clear()方法清除模型数据时,会触发change事件,我们也同样可以在change事件的监听函数中通过previous()和previousAttributes()方法获取数据的上一个状态。
Backbone中每一个模型对象都有一个唯一标识,默认名称为id,
id应该由服务器端创建并保存在数据库中,在与服务器的每一次交互中,模型会自动在URL后面加上id,而对于客户端新建的模型,在保存时不会在URL后加上id标识,举个例子:
// 定义Book模型类
var Book = Backbone.Model.extend({
urlRoot : '/service'
});
// 创建实例
var javabook = new Book({
id : 1001,
name : 'Thinking in Java',
author : 'Bruce Eckel',
price : 395.70
});
// 保存数据
javabook.save();
你可以抓包查看请求记录,你能看到请求的接口地址为:http://localhost/service/1001
其中localhost是我的主机名。
service是该模型的接口地址,是我们在定义Book类时设置的urlRoot。
1001是模型的唯一标识(id),我们之前说过,模型的id应该是由服务器返回的,对应到数据库中的某一条记录,但这里为了能直观的测试,我们假设已经从服务器端拿到了数据,且它的id为1001。
如果同时设置了urlRoot和url参数,url参数的优先级会高于urlRoot。
(另一个细节是,url参数不一定是固定的字符串,也可以是一个函数,最终使用的接口地址是这个函数的返回值。)
javabook.save(null, {
url: '/myservice'
});
在这个例子中,我们在调用save()方法的时候传递了一个配置对象,它包含 一个url配置项,最终抓包看到的请求地址是http://localhost/myservice。因此你可以得知,通过调用方法时传递的url参数优 先级会高于模型定义时配置的url和urlRoot参数。
模型的parse()方法默认不会对数据进行解析,因此我们只需要重载该方法,就可以适配上面的数据格式了
// 定义Book模型类
var Book = Backbone.Model.extend({
urlRoot : '/service',
// 重载parse方法解析服务器返回的数据
parse : function(resp, xhr)
{
var
data = resp.data[0];
return {
id : data.bookId,
name : data.bookName,
author : data.bookAuthor,
price : data.bookPrice
}
}
});
另外值得注意的一点是:我们常常会在数据保存成功后,对界面做一些改变。此时你可以通过许多种方式实现,例如通过save()方法中的success回调函数。
但我建议success回调函数中只要做一些与业务逻辑和数据无关的、单纯的界面展现即可(就像控制加载动画的显示隐藏),如果数据保存成功之后涉及到业务逻辑或数据显示,你应该通过监听模型的change事件,并在监听函数中实现它们。虽然Backbone并没有这样的要求和约束,但这样更有利于组织你的代码。
在Backbone中,所有与服务器交互的逻辑都定义在 Backbone.sync方法中,该方法接收method、model和options三个参数。如果你想重新定义它,可以通过method参数得到需要进行的操作(枚举值为create、read、update和delete),通过model参数得到需要同步的数据,最后根据它们来适配你自己定义的 规则即可。
当然,你也可以将数据同步到本地数据库中,而不是服务器接口,这在开发终端应用时会非常适用。
加油!
posted on 2014-12-10 15:43 chaojidan 阅读(2943) 评论(0) 编辑 收藏 举报