chaojidan

导航

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字符转换为实体形式(例如它会将双引号转换为&quot;形式)再返回,用于避免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编辑  收藏  举报