核心内幕

优质
小牛编辑
139浏览
2023-12-01

在这一章中,你将学习到Backbone的基本元素,models、views、collections和routers,同时还有如何使用命名空间(namespacing)来组织代码。但这意味着这就是官方文档的替代品,而是在你使用Backbone开发应用前帮助你理解背后的许多核心概念。

  • Models(模型)
  • Collections(集合)
  • Routers(路由)
  • Views(视图)
  • Namespacing(命名空间)

模型(Models)

Backbone的models包含了应用中的交互是数据,以及数据的相关逻辑。比如,我们可以用一个model来代表一个photo对象,包含了它的标签(tags),标题(title),位置(location)这些属性

Models可以通过继承Backbone.Model来创建:

var Photo = Backbone.Model.extend({
    defaults: {
        src: 'placeholder.jpg',
        title: 'an image placeholder',
        coordinates: [0,0]
    },
    initialize: function(){
        this.on("change:src", function(){
            var src = this.get("src");
            console.log('Image source updated to ' + src);
        });
    },
    changeSrc: function( source ){
        this.set({ src: source });
    }
});

var somePhoto = new Photo({ src: "test.jpg", title:"testing"});
somePhoto.changeSrc("magic.jpg"); // 触发"change:src",在控制台打印信息

初始化

initialize()方法在当一个model创建一个新的实例是调用。它是可选的,然后,后面你会发现使用它会更好。

var Photo = Backbone.Model.extend({
    initialize: function(){
        console.log('this model has been initialized');
    }
});

// 创建一个自己的Photo实例:
var myPhoto = new Photo();

Getters & Setters

Model.get()

Model.get()简单的提供了对模型属性的访问。 在初始化时传给model的属性是可以立即被访问到的。

var myPhoto = new Photo({ title: "My awesome photo",
                          src:"boston.jpg",
                          location: "Boston",
                          tags:['the big game', 'vacation']}),

    title = myPhoto.get("title"), //My awesome photo
    location = myPhoto.get("location"), //Boston
    tags = myPhoto.get("tags"), // ['the big game','vacation']
    photoSrc = myPhoto.get("src"); //boston.jpg

或者,你想直接访问model实例的所有属性,可以通过下面这种方式:

var myAttributes = myPhoto.attributes;
console.log(myAttributes);

最好使用Model.set()或者model的实例来设置model的属性。

通常不提倡直接访问Model.attributes。如果你想读取或者复制数据,提倡使用Model.toJSON()。 如果你想访问或者复制mode的属性用于JSON的字符串化(比如在传递给view之前序列化),就可以用Model.toJSON()来完成。注意,它返回的是一个对象,如果要获取数据的字符串需要使用JSON.stringify():

var myAttributes = myPhoto.toJSON();
console.log(JSON.stringify(myattributes));
/* this returns:
 { title: "My awesome photo",
   src:"boston.jpg",
   location: "Boston",
   tags:['the big game', 'vacation']}
*/

Model.set()

Model.set() 可以给一个model的实例传入属性。属性可以再初始化的时候设置也可以在后期设置。应该避免直接给model设置属性(比如,Model.caption = 'A new caption')。Backbone使用Model.set()才知道何时广播model的数据改变。

var Photo = Backbone.Model.extend({
    initialize: function(){
        console.log('this model has been initialized');
    }
});

// Setting the value of attributes via instantiation
var myPhoto = new Photo({ title: 'My awesome photo', location: 'Boston' });

var myPhoto2 = new Photo();

// Setting the value of attributes through Model.set()
myPhoto2.set({ title:'Vacation in Florida', location: 'Florida' });

默认值

当你想给model设置默认属性时(比如,当用户不会提供一份完整的数据时),可以用defaults属性。

var Photo = Backbone.Model.extend({
    defaults: {
        title: 'Another photo!',
        tags:  ['untagged'],
        location: 'home',
        src: 'placeholder.jpg'
    },
    initialize: function(){
    }
});

var myPhoto = new Photo({ location: "Boston",
                          tags:['the big game', 'vacation']}),
    title   = myPhoto.get("title"), //Another photo!
    location = myPhoto.get("location"), //Boston
    tags = myPhoto.get("tags"), // ['the big game','vacation']
    photoSrc = myPhoto.get("src"); //placeholder.jpg

监听model的变化

Backbone model的任何属性都可以绑定事件监听,观察他们属性的变化。监听器可以添加到initialize() 方法中:

this.on('change', function(){
    console.log('values for this model have changed');
});

In the following example, we log a message whenever a specific attribute (the title of our Photo model) is altered.

var Photo = Backbone.Model.extend({
    defaults: {
        title: 'Another photo!',
        tags:  ['untagged'],
        location: 'home',
        src: 'placeholder.jpg'
    },
    initialize: function(){
        console.log('this model has been initialized');
        this.on("change:title", function(){
            var title = this.get("title");
            console.log("My title has been changed to.. " + title);
        });
    },

    setTitle: function(newTitle){
        this.set({ title: newTitle });
    }
});

var myPhoto = new Photo({ title:"Fishing at the lake", src:"fishing.jpg"});
myPhoto.setTitle('Fishing at sea');
//logs 'My title has been changed to.. Fishing at sea'

验证(Validation)

Backbone支持通过Model.validate()对model进行验证,可以在设置model的属性前对值进行校验。

验证函数可以简单也可以看需要而极为复杂。如果属性值验证通过,.validate()方法可以不返回任何值。如果验证失败,需要返回一个自定义错误(error)。

下面是一个简单的验证例子:

var Photo = Backbone.Model.extend({
    validate: function(attribs){
        if(attribs.src === undefined){
            return "Remember to set a source for your image!";
        }
    },

    initialize: function(){
        console.log('this model has been initialized');
        this.on("error", function(model, error){
            console.log(error);
        });
    }
});

var myPhoto = new Photo();
myPhoto.set({ title: "On the beach" });
//logs Remember to set a source for your image!

提示: Backbone使用Underscore _.extend方法对attributes对象进行浅拷贝,传递给validate函数。这就意味着,不能像JavaScript对象那样通过引用来改变任何Number, String或者Boolean类型的值。因为浅拷贝对对象不进行深度的复制,所以通过这些对象引用可改变属性值。

这里有一个例子(by @fivetanley)说明这个问题。

视图(Views)

Backbone中的Views不包含应用中的标记,但是它们定义models如何呈现给用户的逻辑。通常通过JavaScript模板来完成(比如:Mustache, jQuery-tmpl等)。view的render()方法可以绑定到model的change()事件上,这样view就可以保持更新而不用刷新整个页面。

创建一个views

与上一章讲到的类似,创建一个view相对比较直接。需要继承自Backbone.View,下面代码中有详细的注解:

var PhotoSearch = Backbone.View.extend({
    el: $('#results'),
    render: function( event ){
        var compiled_template = _.template( $("#results-template").html() );
        this.$el.html( compiled_template(this.model.toJSON()) );
        return this; //建议返回this,可以链式调用。
    },
    events: {
        "submit #searchForm":  "search",
        "click .reset": "reset",
        "click .advanced": "switchContext"
    },
    search: function( event ){
        //当'#searchForm'表单提交时调用。
    },
    reset: function( event ){
        //当class为"reset"的元素点击时调用。
    },
    switchContext: function( event ){
        //当class为"advanced"的元素点击时调用。
    }
});

什么是el?

el通常是DOM元素的引用,所有views都必须有一个。所有view的内容都一次性插入这个DOM,可以让让浏览器执行最小化的重绘,渲染更快。

有2种方式给view指定一个DOM元素:元素在页面中已存在,或者一个新创建的元素,开发者手动添加。 如果元素已经存在,你可以设置el为一个css选择器或者直接对DOM的引用。

el: '#footer',
// 或
el: document.getElementById( 'footer' )

如果要给view创建一个新的元素,设置view属性的任意组合:tagName, idclassName。框架会为你创建一个新的元素,并且可以通过el属性来引用这个元素。

tagName: 'p', // 必须, 但默认会设为'div'
className: 'container', // 可选,可以设置多个class,比如:'container homepage'
id: 'header', // 可选

上面这段代码表示创建下面这个DOMElement,但是不会添加到页面DOM中。

<p id="header" class="container"></p>

理解render()

render()是一个可选方法,定义模板的渲染逻辑。在这里示例中我们会用Underscore的micro-templating,但是你要记得,你也可以使用其它的模板框架。

Underscore的_.template方法把JavaScript模板编译成方法, 在渲染的时候执行。在上面这个view中,通过ID results-template获取模板标记,传给_.template()去编译。然后,把el元素的html设为用view相关联的model的JSON数据编译出来的模板结果。

转眼间!在短短几行代码之内,填充模板,给你一个完成数据填充的标签集合。

events属性

Backbone的events属性可以让我们通过自定义选择器或者直接绑定到el来添加事件监听。事件按照格式{"事件名称 选择器": "回调函数"},支持大量的DOM事件,包括click, submit, mouseover, dblclick 还有更多。

不是特别明显的是,Backbone用jQuery的.delegate()来提供事件代理的支持,但有些改进,this始终指向当前的view对象。需要记住的是,events属性中指定的回调函数名称必须在view范围内有一个对应函数。

集合(Collections)

Collections是Models的集合,通过继承Backbone.Collection来创建。

通常,创建collection时也传入model的属性,和一些必要的实例化属性。

下面这个例子中将创建一个PhotoCollection,包含Photo的models:

var PhotoCollection = Backbone.Collection.extend({
    model: Photo
});

Getters和Setters

从collection获取model有几种不同的方法。最直接的方式是使用Collection.get(),接受一个唯一id:

var skiingEpicness = PhotoCollection.get(2);

有时你可能想通过它的client id来获取model。client id是在model未保存前Backbone自动给它分配的一个id。可以通过model的.cid属性来获取它的client id。

var mySkiingCrash = PhotoCollection.getByCid(456);

Backbone的Collections没有setters这样的方法,但可以通过.add()来添加models,通过.remove()来移除models。

var a = new Backbone.Model({ title: 'my vacation'}),
    b = new Backbone.Model({ title: 'my holiday'});

var photoCollection = new PhotoCollection([a,b]);
photoCollection.remove([a,b]);

监听事件

collections代表了一组元素,我们同样可以监听从collection addremove models。下面是示例:

var PhotoCollection = new Backbone.Collection();
PhotoCollection.on("add", function(photo) {
  console.log("I liked " + photo.get("title") + ' it\'s this one, right? '  + photo.get("src"));
});

PhotoCollection.add([
  {title: "My trip to Bali", src: "bali-trip.jpg"},
  {title: "The flight home", src: "long-flight-oofta.jpg"},
  {title: "Uploading pix", src: "too-many-pics.jpg"}
]);

另外,也可以绑定change事件来监听collection中models的变化。

PhotoCollection.on("change:title", function(){
    console.log("there have been updates made to this collection's titles");
});

从服务器端获取models

Collections.fetch()默认接受从服务器端获取的models json数组。当数据返回时,当前collection的内容就会被替换成这个数组。

var PhotoCollection = new Backbone.Collection;
PhotoCollection.url = '/photos';
PhotoCollection.fetch();

在配置的时候,Backbone设置了一个变量来标识扩充的HTTP方法是否被服务器支持。另外一个设置控制服务器端是否识别正确的JSON MIME类型。

Backbone.emulateHTTP = false;
Backbone.emulateJSON = false;

Backbone.sync方法使用到的这些值实际上是Backbone.js不可分割的一部分。这个方法类似jQuery的ajax方法,所以参数结构上是基于jQuery的API。通过搜索代码里sync方法的调用会发现,任何时候,在model保存,获取,删除(销毁)时就会调用。

在后台,Backbone.sync方法在Backbone试图读取或保存models到server的时候就会调用。它使用jQuery或者Zepto的ajax实现来完成这些RESTful请求,当然,也可以使用你喜欢的方式去重写它。:

sync方法可以通过Backbone.sync全局性的重写,或者在一个更精确的范围内通过给Backbone collection或者一个单独的model添加sync方法重写。

没有什么花哨的插件API来添加一个持久层——简单的覆盖Backbone.sync方法就可以了:

Backbone.sync = function(method, model, options) {
};

options默认参数:

var methodMap = {
  'create': 'POST',
  'update': 'PUT',
  'delete': 'DELETE',
  'read':   'GET'
};

在上面这个列子中如果想在.sync()调用时log一个事件,可以这样做:

var id_counter = 1;
Backbone.sync = function(method, model) {
  console.log("I've been passed " + method + " with " + JSON.stringify(model));
  if(method === 'create'){ model.set('id', id_counter++); }
};

重置/刷新 Collections

不同于单独的添加或者删除models,你可能想一次性整体更新collection。Collection.reset()可以设置新的models来更新整个collection:

PhotoCollection.reset([
  {title: "My trip to Scotland", src: "scotland-trip.jpg"},
  {title: "The flight from Scotland", src: "long-flight.jpg"},
  {title: "Latest snap of Loch Ness", src: "lochness.jpg"}]);

注意,使用Collection.reset()不触发任何addremove事件。而是触发一个reset事件。

Underscore实用方法

因为Backbone强烈依赖Underscore,所以我们就可以使用它的许多实用方法来帮助开发。这里有一个使用Underscore的sortBy()方法基于某个属性对photo的collection进行排序的例子。

var sortedByAlphabet = PhotoCollection.sortBy(function (photo) {
    return photo.get("title").toLowerCase();
});

可以在Underscore官方文档找到更完整的用法。

链式API

/* This works by calling the original method with the current array of models and returning the result. In case you haven’t seen it before, the chainable API looks like this: */ 跳过实用方法,Backbone的另一个甜蜜之处就是它对Underscore chain方法的支持。工作原理是使用当前的models数组调用原始方法,并且把结果返回。如果你之前从未见过,链式API就像这样:

var collection = new Backbone.Collection([
  { name: 'Tim', age: 5 },
  { name: 'Ida', age: 26 },
  { name: 'Rob', age: 55 }
]);

collection.chain()
  .filter(function(item) { return item.get('age') > 10; })
  .map(function(item) { return item.get('name'); })
  .value();

// 将返回['Ida', 'Rob']
某些Backbone特定方法会返回this,同样也可以链接:

var collection = new Backbone.Collection();

collection
    .add({ name: 'John', age: 23 })
    .add({ name: 'Harry', age: 33 })
    .add({ name: 'Steve', age: 41 });

collection.pluck('name');
// ['John', 'Harry', 'Steve']

事件(Events)

正如我们讲述的,Backbone的对象都设计成继承而来,而且下面的每个对象都继承自Backbone.Events

  • Backbone.Model
  • Backbone.Collection
  • Backbone.Router
  • Backbone.History
  • Backbone.View

事件是处理用户行为的标准方式,通过对views,model和collection的变化声明事件绑定。掌控事件是使用Backbone提高生产力最快速的方式。

Backbone.Events同样提供一种方式给任何对象绑定和触发自定义事件。我们可以非常容易的把它与任何对象混合,而且不要求事件在绑定前必须声明。

示例:

var ourObject = {};

// Mixin
_.extend(ourObject, Backbone.Events);

// Add a custom event
ourObject.on("dance", function(msg){
  console.log("We triggered " + msg);
});

// Trigger the custom event
ourObject.trigger("dance", "our event");

如果你对jQuery自定义事件或者发布者/订阅者(Publish/Subscribe)的概念比较熟悉的话,Backbone.Events 提供了一套类似的系统,onsubscribe类似,triggerpublish类似。

on基本上允许我们给任何对象绑定一个回调方法,就想上面例子中dance一样。任何时候事件被触发,回调函数就被执行。

官方的Backbone.js文档建议对事件名称使用命名空间的方式来连接,如果你的页面中有比较多事件的话,比如:

ourObject.on("dance:tap", ...);

如果你想要在任何事件触发时都触发一个事件的话(比如你想要在屏幕中某个位置显示事件触发的名称),可以用一个特定的all事件。all事件可以像下面这样使用:

ourObject.on("all", function(eventName){
  console.log("The name of the event passed was " + eventName);
});

off可以从一个对象中移除已经绑定的回调。回到发布者/订阅者(Publish/Subscribe)的对照关系,思考下 退订(unsubscribe)自定义事件。

要移除前面给myObject绑定的dance事件,可以这样做:

myObject.off("dance");

这会移除所有绑定在dance事件上的回调。如果只想移除指定名称的回调,可以这样:

myObject.off("dance", callbackName);

最后,trigger触发指定事件的回调(或者以空格分隔的事件列表)。例如:

// 单个事件
myObject.trigger("dance");

// 多个事件
myObject.trigger("dance jump skip");

也可以通过trigger第二个参数给每个(或所有)事件传入附加参数。例如:

myObject.trigger("dance", {duration: "5 minutes"});

路由(Routers)

在Backbone中,Routers是用于帮助管理应用状态,以及关联url和应用的事件。通过URL片段的hash-tags,或者使用浏览器pushState或者History API。下面是一些Routers的示例:

http://unicorns.com/#whatsup
http://unicorns.com/#search/seasonal-horns/page2

提示: 一个应用通常至少有一个路由映射,一个URL路由到一个function,这个function决定了当用户到达这个指定route的时候应该做什么。这种关系像下面这样定义:

"route" : "mappedFunction"

那让我们通过继承Backbone.Router来定义第一个控制器吧。在这段指南中,我们继续创建图片库应用,需要一个GalleryRouter。

请注意下面代码中的注解:

var GalleryRouter = Backbone.Router.extend({
    /* 定义route和function的映射*/
    routes: {
        "about" : "showAbout",
        /*使用范例: http://unicorns.com/#about*/

        "photos/:id" : "getPhoto",
        /*这是一个使用参数变量的列子(":param"),可以匹配2个斜杠之间的部分*/
        /*使用范例: http://unicorns.com/#photos/5*/

        "search/:query" : "searchPhotos",
        /*也可以定义多个routes映射到同一个function,比如这里的searchPhotos()。
        注意下面我们如何随意的传入一个已经存在的页码*/
        /*使用范例: http://unicorns.com/#search/lolcats*/

        "search/:query/p:page" : "searchPhotos",
        /*我们可以看到,URL中可以包含多个":param"参数*/
        /*使用范例: http://unicorns.com/#search/lolcats/p1*/

        "photos/:id/download/*imagePath" : "downloadPhoto",
        /*这是一个使用通配符(*splat)的例子。通配符可以匹配任意数量的URL部分,
        而且可以与参数":param"组合使用*/
        /*使用范例: http://unicorns.com/#photos/5/download/files/lolcat-car.jpg*/

        /*如果你想用通配符来做默认路由,最好放在URL的最后,
        不然就要在URL片段上使用正则表达式了。*/

        "*other"    : "defaultRoute"
        /*这是一个使用*splat的默认路由。Consider the
        default route a wildcard for URLs that are either not matched or where
        the user has incorrectly typed in a route path manually*/
        /*使用范例: http://unicorns.com/#anything*/

    },

    showAbout: function(){
    },

    getPhoto: function(id){
        /*
        注意前面route匹配的id会传入这个方法
        */
        console.log("You are trying to reach photo " + id);
    },

    searchPhotos: function(query, page){
        var page_number = page || 1;
        console.log("Page number: " + page_number + " of the results for " + query);
    },

    downloadPhoto: function(id, path){
    },

    defaultRoute: function(other){
        console.log("Invalid. You attempted to reach:" + other);
    }
});

/*现在我们就建立了一个router,记得实例化。*/

var myGalleryRouter = new GalleryRouter();

Backbone 0.5+以上版本,可以选择window.history.pushState对HTML5 pushState的支持。这就允许你像这样http://www.scriptjunkie.com/just/an/example来定义routes。浏览器不支持pushState时会降级处理。在这部教程中我们会使用hashtag方法。

对路由器(routers)的数量会有限制吗?

安德鲁·德·安德拉德(Andrew de Andrade)指出,DocumentCloud在他们大多数应用中通常只使用一个路由器(router)。 在自己大多数应用项目中最好不要超过一个router,这样路由可以放在一个controller中,不会变的笨重。

Backbone.history

下面,我们需要初始化Backbone.history,它应用中处理了hashchange事件。当它们被访问时会自动处理被定义的routes,并且触发回调。

Backbone.history.start()方法会告诉Backbone监听所有hashchange事件,就想下面这样:

Backbone.history.start();
Router.navigate();

另外,如果你想在URL中的某个特定地方保存应用的状态可以用.navigate()方法。它可以不触发hashchange事件更新URL片段:

/*假设当用户方法一张照片的时候我们需要给定一个片段(fragment)*/
zoomPhoto: function(factor){
    this.zoom(factor); //假设这是放大照片
    this.navigate("zoom/" + factor); //更新fragment,但是不触发hashchange事件。
}

Router.navigate()也可以在更新URL fragment的同时触发路由。

zoomPhoto: function(factor){
    this.zoom(factor); //假设这是放大照片
    this.navigate("zoom/" + factor, true); //更新fragment,触发路由
}

Backbone的同步API

Backbone.sync方法就是用于被重写以对后端的支持。该内置方法是根据RESTful JSON API衍生而来的—— Backbone最初是从一个Ruby on Rails应用中提取出来的,该应用以同样的方式使用像PUT之类的HTTP方法。

这种方式的工作原理就是model和collection类有一个sync方法,它调用Backbone.sync。当获取,保存或者删除元素时它们内部都会调用this.sync。

sync方法有3个参数:

  • method: 可以是create, update, delete, read
  • model: Backbone model对象
  • options: 可能包含成功和错误的回调方法。

可以使用下面这种模式来实现一个sync方法:

Backbone.sync = function(method, model, options) {
  var requestContent = {}, success, error;

  function success(result) {
    // 处理MyAPI的返回结果
    if (options.success) {
      options.success(result);
    }
  }

  function error(result) {
    // 处理MyAPI的返回结果
    if (options.error) {
      options.error(result);
    }
  }

  options || (options = {});

  switch (method) {
    case 'create':
      requestContent['resource'] = model.toJSON();
      return MyAPI.create(model, success, error);

    case 'update':
      requestContent['resource'] = model.toJSON();
      return MyAPI.update(model, success, error);

    case 'delete':
      return MyAPI.destroy(model, success, error);

    case 'read':
      if (model.attributes[model.idAttribute]) {
        return MyAPI.find(model, success, error);
      } else {
        return MyAPI.findAll(model, success, error);
      }
  }
};

这种模式把API的调用委派给了一个新的对象,这个对象可以是Backbone风格支持事件的的对象。它可以被安全的独立测试,而且可以用于除Backbone以外的其它库中。

除此之外还有很多其它的同步实现:

  • Backbone localStorage
  • Backbone offline
  • Backbone Redis
  • backbone-parse
  • backbone-websql
  • Backbone Caching Sync

处理冲突

跟大部分客户端项目一样,Backbone.js把所有东西都包裹在一个即时执行的函数表达式中:

(function(){
  // Backbone.js
}).call(this);

在这个结构被执行的时候发生了几件事情。一个Backbone名字空间(namespace)被创建了,而且可以通过noConflict模式来让多个版本的Backbone在同一个页面中共存:

var root = this;
var previousBackbone = root.Backbone;

Backbone.noConflict = function() {
  root.Backbone = previousBackbone;
  return this;
};

可以像下面这样调用来让多个版本Backbone在同一个页面中共存:

var Backbone19 = Backbone.noConflict();
// Backbone19指向最近载入的Backbone
// `window.Backbone`则保存的是前一个载入的版本

这段初始化代码同样支持CommonJS模块,所以Backbone可以用在Node项目中:

var Backbone;
if (typeof exports !== 'undefined') {
  Backbone = exports;
} else {
  Backbone = root.Backbone = {};
}

继承(Inheritance)&混合(Mixins)

Backbone的继承使用inherits方法,灵感来自于Google在Closure库中的实现。它主要是建立原型链。

 var inherits = function(parent, protoProps, staticProps) {
      ...

主要的不同是,Backbone的API接受2个对象,包括instance(实例)static(静态)方法。

依此而论,为了达到继承的目的,Backbone对象都包含一个extend方法:

Model.extend = Collection.extend = Router.extend = View.extend = extend;

大部分基于的Backbone的开发都从这几个对象中继承而来,而且它们被设计成模仿经典的面向对象的实现。

如果这听起来很熟悉,因为extend是Underscore.js的一个功能,虽然Backbone用它做了很多事情。看看下面Underscore的extend:

each(slice.call(arguments, 1), function(source) {
  for (var prop in source) {
    obj[prop] = source[prop];
  }
});
return obj;

上面跟ES5的Object.create有很大不同,因为它实际上是复制一个对象的属性(方法和值)给另外一个对象。它不足以支持Backbone的继承和类模型,使用下面的步骤会更好:

  • 实例方法检查是否有constructor属性。如果有,使用当前class的constructor,否则使用parent的constructor(比如Backbone.Model)。
  • Underscore的extend方法会把父类的方法添加到新的子类上。
  • prototype属性是用父类的原型(prototype)定义一个空的构造函数(constructor function)
  • 定义子类原型的constructor和一个__super__属性。
  • 这种模式也同样用于CoffeeScript中的class,所以Backbone中的class与CoffeeScript中的class兼容。

extend可以更广泛的使用,对于那些忠实于mixins的开发者来说也可以这样使用。你可以把功能定义在自己的object上,然后直接把它复制到一个Backbone对象上。

例如:

 var MyMixin = {
  foo: "bar",
  sayFoo: function(){alert(this.foo);}
}

var MyView = Backbone.View.extend({
 // ...
});

_.extend(MyView.prototype, MyMixin);

myView = new MyView();
myView.sayFoo(); //=> "bar"

我们可以借此更近一步,把它应用到View的继承上。下面是如何扩展一个View的例子:

var Panel = Backbone.View.extend({
});

var PanelAdvanced = Panel.extend({
});

不过,如果Panely已经有一个initialize()方法,PanelAdvanced同样也有一个initialize()方法,上一个就不会调用,所以需要显示的调用Panel的initialize方法:

var Panel = Backbone.View.extend({
   initialize: function(options){
      console.log('Panel initialized');
      this.foo = 'bar';
   }
});

var PanelAdvanced = Panel.extend({
   initialize: function(options){
      this.constructor.__super__.initialize.apply(this, [options])
      console.log('PanelAdvanced initialized');
      console.log(this.foo); // Log: bar
   }
});

这不是最优雅的解决方案,如果有很多Views都要继承自Panel的话,就需要每个都记得调用Panel的initialize。

可能Panel目前initialize方法没什么用处所以没写,将来要添加的话你就不得不检查下所有继承自它的子类都调用了它的initialize方法。

所以,有另外一种方式来定义Panel,继承自它的views就不用调用它的initialize方法:

var Panel = function (options) {

    // Panel的初始化代码
    console.log('Panel initialized');
    this.foo = 'bar';

    Backbone.View.apply(this, [options]);
};

_.extend(Panel.prototype, Backbone.View.prototype, {

    // 把所有Panel的方法都放这,例如:
    sayHi: function () {
        console.log('hello from Panel');
    }
});

Panel.extend = Backbone.View.extend;


// 其它继承自Panel的view:
var PanelAdvanced = Panel.extend({

    initialize: function (options) {
        console.log('PanelAdvanced initialized');
        console.log(this.foo);
    }
});

var PanelAdvanced = new PanelAdvanced(); //Log: Panel initialized, PanelAdvanced initialized, bar
PanelAdvanced.sayHi(); // Log: hello from Panel

只要使用得当,Backbone的extend方法可以节省很多时间并且减少冗余代码。

(非常感谢Alex Young, Derick BaileyJohnnyO 给出这些提示).

命名空间(Namespacing)

当学习使用Backbone时,通常教程会忽视命名空间这个重要点。如果你对JavaScript中命名空间已经有经验的话,后面部分会给你一些如何确切的应用Backbone一些概念的建议,不过我还是要为初学者讲解下这个话题。

什么是命名空间namespacing?

命名空间的基本概念就是避免与全局名字空间下的对象或者变量冲突。这非常重要,所以要保护好你的代码,避免被同一个页面里其它使用相同变量的代码破坏。作为全局空间下的一个良好’公民(citizen)’,同样要尽可能的不破坏其它开发者的代码运行。

JavaScript本身不想其它语言一样内置对命名空间的支持,但是有闭包可以达到类似的效果。

这部分我们通过一些例子简短的看下如果对models, views, routers和其它组件用命名空间管理。有几种模式:

  • 单一全局变量(Single global variables)
  • 对象常量(Object Literals)
  • Nested namespacing

单一全局变量

在JavaScript中一种常用的命名空间的方式就是使用单一全局变量作为主要对象的引用。可以像下面这样返回一个包含方法和属性的对象:

var myApplication = (function(){
    function(){
      // ...
    },
    return {
      // ...
    }
})();

可能前面你已经看到过这种技术了。一个Backbone特有的例子可能会像这样:

var myViews = (function(){
    return {
        PhotoView: Backbone.View.extend({ .. }),
        GalleryView: Backbone.View.extend({ .. }),
        AboutView: Backbone.View.extend({ .. });
        //etc.
    };
})();

这里我们可以返回一组views,使用同样的方式也可以然会整个应用中的models, views和routers。尽管在确切的情况下它可以正常的工作,但单一全局变量最大的问题就是必须确保在同一个页面中没有其它人使用同样的全局变量。

Peter Michaux提到一种可以解决这个问题的方案,使用命名空间前缀。它只是一个内心的一个概念,他的观点就是使用一个通用的前缀(下面例子中,myApplication_),然后所有方法,变量和其它对象的定义都加上这个前缀。

var myApplication_photoView = Backbone.View.extend({}),
myApplication_galleryView = Backbone.View.extend({});

这得益于减少全局域下特定变量变化的观点,不过唯一对象命名也有同样的效果。另外,这种模式的最大问题在于当应用变得庞大时就会产生大量的全局变量。

更多关于Peter单一全局变量模式的观点可以阅读更多好的文章

提示:单一变量模式有很多其它的变种,不过经过一段时间评测,我觉得加前缀的方式在Backbone应用的最好。

对象常量

对象常量的好处是不会污染全局空间,但是要逻辑性的组织代码和参数。如果你想容易的创建支持深层嵌套的可读性结构代码的话,它非常有用。不像单一全局变量,对象常量经常考虑检测同样名称的变量是否存在,来减少冲突。

下面有2中方式在定义前来检测一个名字是否存在。我通常使用第二种。

/*不会检测myApplication的存在*/
var myApplication = {};

/*
检测是否存在。如果已经定义则直接使用该实例。
方案1:   if(!myApplication) myApplication = {};
方案2:   var myApplication = myApplication || {};
然后就可以使用它来支持models, views和collections (或者任何其它数据):
*/

var myApplication = {
    models : {},
    views : {
        pages : {}
    },
    collections : {}
};

也可以选择直接给它添加属性:

var myGalleryViews = myGalleryViews || {};
myGalleryViews.photoView = Backbone.View.extend({});
myGalleryViews.galleryView = Backbone.View.extend({});

这种模式的好处是,可以容易的封装所有的models, views, routers等等,把他们清晰的分离,而且对代码扩展提供的坚实的基础。

This pattern has a number of benefits. It’s often a good idea to decouple the default configuration for your application into a single area that can be easily modified without the need to search through your entire codebase just to alter it. Here’s an example of a hypothetical object literal that stores application configuration settings:

var myConfig = {
    language: 'english',
    defaults: {
        enableGeolocation: true,
        enableSharing: false,
        maxPhotos: 20
    },
    theme: {
        skin: 'a',
        toolbars: {
            index: 'ui-navigation-toolbar',
            pages: 'ui-custom-toolbar'
        }
    }
}

Note that there are really only minor syntactical differences between the Object Literal pattern and a standard JSON data set. If for any reason you wish to use JSON for storing your configurations instead (e.g. for simpler storage when sending to the back-end), feel free to.

For more on the Object Literal pattern, I recommend reading Rebecca Murphey’s excellent article on the topic.

Nested namespacing

An extension of the Object Literal pattern is nested namespacing. It’s another common pattern used that offers a lower risk of collision due to the fact that even if a top-level namespace already exists, it’s unlikely the same nested children do. For example, Yahoo’s YUI uses the nested object namespacing pattern extensively:

YAHOO.util.Dom.getElementsByClassName('test');

Yahoo’s YUI uses the nested object namespacing pattern regularly and even DocumentCloud (the creators of Backbone) use the nested namespacing pattern in their main applications. A sample implementation of nested namespacing with Backbone may look like this:

var galleryApp =  galleryApp || {};

// perform similar check for nested children
galleryApp.routers = galleryApp.routers || {};
galleryApp.model = galleryApp.model || {};
galleryApp.model.special = galleryApp.model.special || {};

// routers
galleryApp.routers.Workspace   = Backbone.Router.extend({});
galleryApp.routers.PhotoSearch = Backbone.Router.extend({});

// models
galleryApp.model.Photo   = Backbone.Model.extend({});
galleryApp.model.Comment = Backbone.Model.extend({});

// special models
galleryApp.model.special.Admin = Backbone.Model.extend({});

This is readable, clearly organized, and is a relatively safe way of namespacing your Backbone application. The only real caveat however is that it requires your browser’s JavaScript engine to first locate the galleryApp object, then dig down until it gets to the function you’re calling. However, developers such as Juriy Zaytsev (kangax) have tested and found the performance differences between single object namespacing vs the nested approach to be quite negligible.

Recommendation

Reviewing the namespace patterns above, the option that I prefer when writing Backbone applications is nested object namespacing with the object literal pattern.

Single global variables may work fine for applications that are relatively trivial. However, larger codebases requiring both namespaces and deep sub-namespaces require a succinct solution that’s both readable and scalable. I feel this pattern achieves both of these objectives and is a good choice for most Backbone development.