《Backbone 应用开发》读书笔记(1)

假语村言

所有架构设计的目的都是让软件更好,但是如何定义这个“好”呢?
基本要求是满足功能性需求,进而考虑性能、可扩展性、可维护性等要求,上面这些在实际工作中会有人在不同阶段跳出来,讲一堆不满足会死的理由。这些可以说是架构设计第一层次的要求,如果对比马斯洛需求模型,这个只能算是温饱阶段。
对于苦逼又要面(儿)的码农来说,好的架构设计,或者说在此设计理念之上的框架、库,要足够时髦,说出来以后要足够酷,有个性。不夸张的说,大多数时候码农选框架要比女人选衣服费劲得多,要试穿多次,反复比较,一旦衣服不满意,码农也会发小姐脾气。虽说发小姐脾气好像不太体面,但是讲究衣服穿着的人,如果再有点品位,应该算是进入小康了吧。
再往上…如闲云野鹤般,偶尔指点一下江山?或者人家也没这么悠哉,而是站的高看的远,不断变着花样地解决着勤劳的码农早已熟视无睹的问题,或者走在前面,不经意间引领了你行进的方向。只能YY一下,这样的人,还没认识的。很多人被挡在外面进不了这一层,可能就是因为太勤奋了,或者说战术上过于勤奋,战略上又过于懒惰。
下笔千言,离题万里。是病,得改!

什么是MVC?(参见我的 另一篇文章)对于有点强迫症,非要弄清哪是M,哪是V或C的人,恐怕真的有点痛苦,亲身经历过,代码草草看过一遍,拿着MVC来套,来分解,整了半天说:你这不符合MVC,有问题。这个阶段,看山是山,看水是水,一旦不那么清楚,自己就先蒙了。Backbone应用开发告诉我们,很多Javascript MVC框架(包括Backbone)并没有严格遵循传统的MVC,各个部分的内涵不同,划分也有区别,但是仍遵循了数据和表现的分离,所以还是叫MV*吧。题外话,近几年流行的单页面应用(SPA)也是一种数据和表现的分离, WEB端专注于表现,服务器端专注于数据,二者通过RESTful API建立联系,这也促成了很多BaaS(Backend as a service)的出现。
言归正传,因为是笔记,后面部分是一些书的摘要或注释,主要是便于自己理解,会凌乱一些。
Backbone代码总共1000多行,是一个轻量级的Javascript库,不是完整的框架,各个部分很独立,可以单独应用在你的工程中,也可以跟其他框架协作,比较灵活。看Backbone的官网就会发现,网页布局跟Underscore非常类似,尤其是左树,所以从这个角度上说,Backbone本来就是要当Utility来用的。
Backbone只提供最基本的数据结构(models,collections)和用户接口(views,routes),本身没有UI控件,不限制模板引擎,可以直接用它来实现简单的WEB应用,或者结合其他重一些的框架亦或在它基础上进行增强来实现复杂的功能。
SPA与传统的WEB Page有什么区别,最主要的是在第一次进行页面加载后,后续的导航和请求不必重新加载整个页面,而只需要通过AJAX请求得到需要更新部分的数据,然后部分地刷新页面。SPA仍旧能够利用浏览器的History API,不同视图对应不同的URL,进行可以保存为书签或者前进后退。
这本书中样例代码的命名规范,一个比较好的实践是所有jquery对象都以$开头,以区别于普通javascript对象,例如el和$el。

MV*

Backbone如何实现数据和视图的绑定呢?一方面,一个view可以将它的render()方法绑定到model的change事件,在模型变化时自动刷新视图。另一方面,当view发生变化时,需要call model的方法来保存变化。也就是视图发生变化时要主动更新模型,而模型发生变化时视图要通过监听事件来被动响应。在Backbone的术语中,有Model和View,但没有Controller,事实上,Backbone的View包含了controller的功能,Routers用来管理应用状态。更进一步地分析,发现Backbone实现了自己的MVP*架构,更像是MVP模型。 


在MV*框架中,模板引擎扮演了很重要的角色,模板的重要作用在于避免了拼凑大段html字符串,让页面各部分能够解耦,层次更加清晰,通过数据来驱动页面展现。常见的模板引擎像Mustache,Handlebars,不但能够完成基本的变量替换,而且可以包含复杂的逻辑,如if、else、for, 甚至模板的继承,这能够让UI展现和业务逻辑进一步地得到分离。

Model

Model就是一个数据容器,包含应用程序的数据及相关逻辑。
当一个Model被实例化时,Initialization()方法会首先被调用。事实上,Backbone提供的4个基本类Model、Collection、Router、View中都有一个initialization()方法,可以用它来进行一些初始化工作,比如事件绑定。 
var Todo = Backbone.Model.extend({
    defaults: {
        title: '',
        completed: false
    },
    initialize: function () {
        console.log('This model has been initialized.');
        this.on('change:title', function () {
            console.log('Title value for this model has changed.');
        });
    }
});
var myTodo = new Todo();
myTodo.set('title', 'Check what\'s logged.');
Todo Model定义了两个默认属性,title和completed,通过set方法修改属性时会触发change事件。Backbone支持监听单个变量的事件,比如上例中的“change:title”,只监听title属性的变化,不管completed属性。
如果实现了validate()方法,在model的save()或者set(value,{validate:true})时,会自动调用validate()进行验证。

View

View代表DOM中的一部分UI,比如sidebar,panel,或者是包含整个应用的外框架等。将整个网页分开定义为多个View,可以灵活控制各部分的渲染,响应模型事件。
View有model/collection属性,可以在构造函数中将模型数据传进来,如: 
var myTodo = new Todo({
    title:'Read the whole book', 
    id: 2
});
每个View都有一个el属性,指向当前View所代表的DOM元素,同时,View还有一个$el属性,二者的区别就在于后者是jQuery对象,有show(), hide()等方法可以调用。view.$el等价于$(view.el)。
将el关联到DOM有两种方式,
方法1:新建一个View并指定html标签类型,然后将View对应的元素添加到DOM中 
var TodosView = Backbone.View.extend({
    tagName: 'ul', // required, but defaults to 'div' if not set
    className: 'container', // optional, you can assign multiple classes to
// this property like so: 'container homepage' id: 'todos', // optional
});
var todosView = new TodosView();
console.log(todosView.el); // logs <ul id="todos" class="container"></ul>
后面可以$ele.append(todoVIew.el)添加到DOM中
方法2: 通过CSS选择器添加到页面已有的元素上 
var todosView = new TodosView({
    el: $('#footer')
});
实际情况中,View表示的html不可能这么简单,可以通过模板来实现复杂的渲染,这里包括两部分:模板和渲染(render)。前面说过Backbone并不限定模板引擎,所以可以选择任意自己喜欢的模板引擎,但本书样例采用Underscore的内置引擎。
app.TodoView = Backbone.View.extend({
    //... is a list tag.
    tagName: 'li',

    // Cache the template function for a single item.
    template: _.template($('#item-template').html()),

    initialize: function () {
        this.listenTo(this.model, 'change', this.render);
    },

    render: function () {
        this.$el.html(this.template(this.model.toJSON()));
        return this;
    }
});
定义一个模板属性,名字任意。上例中用Underscore的template(),通过jQuery选择器定义了一个模板,模板代码在html文件中。render是实现BackBone的View通常要重写(override)的一个方法,但Backbone自己并不自动调用这个方法,一般是在initialize方法中通过监听model的“change”来触发重新render。
render方法的一个约定是在最后返回this,便于view的嵌套和统一渲染。
View还包括一个events属性,用于定义DOM事件和响应函数的map关系,定义中可以支持选择器,格式如下:'eventName selector': 'callbackFunction'。 
events: {
    'click .toggle': 'toggleCompleted',
    'dblclick label': 'edit',
    'click .destroy': 'clear',
    'blur .edit': 'close'
}
这种声明方式定义的代理事件不必关心DOM元素是否已经渲染完成,而用on()或bind()方法时就要保证DOM元素已经存在。
有一点需要注意的是,上面两种方式的回调函数中this表示的对象不同,在events声明方式中,this指的是View对象,而通过jQuery的on()或bind()方式,this指的是触发事件的DOM元素,这时$(this)是对this的jQuery包装。
将View从DOM中删除可以调用remove(), 默认同时会call stopListening()。

Collection

Collection是Model的集合,在定义collection时一般要指定包含的Model类型。Collection实际主要包括一个model的数组,所以能够想到的数组的操作函数和Underscore的部分函数,collection都支持,如add, remove, map, reduce, find等。数组变化时会触发响应的事件,这里不细说了。 
var Todos = Backbone.Collection.extend({
    model: app.Todo,
});

Events

Backbone的Events用起来很方便,所有Backbone主要的类中都包含了事件功能,比如:Backbone,Backbone.Model,Backbone.Collection,Backbone.Router,Backbone.History,Backbone.View。
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');
基本方法除了on(), off(), trigger(),还有listenTo()和stopListening(),后者可以让a对象监听b对象的事件,如
a.listenTo(b, 'anything', function (event) {
    console.log("anything happened");
});

Routers

路由将URL和回调函数映射起来,任何需要保存为书签、需要共享、需要前进后退的网页都需要有对应的URL。Backbone支持多种路由规则,能够匹配各种不同的URL并提取URL参数传递给回调函数。参见样例: 
var TodoRouter = Backbone.Router.extend({
    /* 定义URL和回调函数的映射 */
    routes: {
        "about": "showAbout",
        /* Sample usage: http://example.com/#about */
        "todo/:id": "getTodo",
        /* This is an example of using a ":param" variable, which allows us to match any of the components between two URL slashes */
        /* Sample usage: http://example.com/#todo/5 */
        "search/:query": "searchTodos",
        /* We can also define multiple routes that are bound to the same map function, in this case searchTodos(). Note below how we're optionally passing in a reference to a page number if one is supplied */
        /* Sample usage: http://example.com/#search/job */
        "search/:query/p:page": "searchTodos",
        /* As we can see, URLs may contain as many ":param"s as we wish */
        /* Sample usage: http://example.com/#search/job/p1 */
        "todos/:id/download/*documentPath": "downloadDocument",
        /* This is an example of using a *splat. Splats are able to match any number of URL components and can be combined with ":param"s*/
        /* Sample usage: http://example.com/#todos/5/download/todos.doc */
        /* If you wish to use splats for anything beyond default routing,
         it's probably a good idea to leave them at the end of a URL;
         otherwise, you may need to apply regular expression parsing
         on your fragment */
        "*other": "defaultRoute"
        /* This is a default route that also uses a *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 */
        /* Sample usage: http://example.com/# <anything> */,
        "optional(/:item)": "optionalItem",
        "named/optional/(y:z)": "namedOptionalItem"
        /* Router URLs also support optional parts via parentheses, without
         having to use a regex.  */
    },
    showAbout: function () {},
    getTodo: function (id) {},
    searchTodos: function (query, page) {},
    downloadDocument: function (id, path) {},
    defaultRoute: function (other) {}
});
hashchange事件会在页面URL中的片段标识符(第一个#号开始到末尾的所有字符,包括#号)发生改变时触发,Backbone.history.start()告诉Backbone开始监听浏览器的hashchange事件(如果浏览器支持的话)。如果只想修改URL而不触发hashchange,可以用navigate()方法。

Sync API

前面没有讲到的一点是,Backbone的Model和Collection都支持通过RESTful API来获取或者持久化数据,而这些方法的背后实际上是sync()方法。每次Backbone试图read、save或者delete model时都会调用sync()。
根据需要可以在自己的Collection或者Model中重写sync(),从而使用不同的RESTful API后端或增加自己的逻辑代码。 
Backbone.sync = function(method, model, options) {};
有一些现成的Backbone插件实现了不同的sync后端,比如保存在localStroage的或者Redis的。

Backbone基础大概就这些了,后面会抽时间总结一下Backbone.Marionette以及与requireJS的结合。 



你可能感兴趣的:(JavaScript,mvc,backbone)