backbone.js的使用报告

一、准备阶段

1.1 框架选型

随着对MV*架构模式的逐步理解,越来越发觉对于一般的业务场景,mvvm是前端架构的不二选择。在我所了解的框架范围内,与mvvm架构模式最贴合的框架只有angular,无论是双向数据绑定、指令,还有强化的html标签,angular提出的很多新概念都像是为mvvm理论量身定做的一套外衣。很可惜angular对ie8的支持不太友好,兼容ie8的angular插件也非常稀缺,对于国内环境,ie8仍然占有较大的市场份额,无奈之下只能转向轻量级的backbone。

1.2 官方文档vs电子书

通过阅读backbone的官网简介,总结下backbone特点(并非优点):

  • 提供了基本的MVC结构,可以做到业务逻辑与视图的分离(MV*框架最基本的功能)
  • 内置支持restful风格的API(做过项目后会发觉,内置的那套基本没什么用,自定义会更合适)
  • 路由的支持(大部分MV*框架也都支持,也不是亮点)
  • 方便与第三方插件集成(因为backbone太轻量了,对于插件没什么要求,这勉强可以算优点)
  • 兼容ie8(对比其他MV*框架,感觉这是唯一的优点,也是选择它的理由)
  • 粗粒度的单向数据绑定(缺点,每次更新视图,都只能全部更新,当然你可以自己写局部更新,会比较繁琐)
  • 太轻量了(缺点,连angular这种重量级的框架写法都会五花八门,backbone这种就更别提了,新手根本无法驾驭它写出优良的代码)

由于backbone功能单薄,无法形成固有的mvc或是mvp模式,但是为了让它变得好用,我们可以尝试增强它的功能,让它更接近我们想要的mvvm框架。mvvm框架最大特点是:双向数据绑定,于是通过google:backbone data binding,找到了以下几个backbone插件:Backbone.ModelBinder、Rivets.js、Backbone.Stickit。通过测试发现Rivets.js很好用,可惜不兼容ie8,ModelBinder配置不够灵活,于是Stickit成为最佳选择。

关于框架的学习,我的思路是这样的:官方文档是一定要看的,但是没必要从头看到尾,官方开头那部分介绍是一定要看的,那里会告诉你框架是什么样的,有什么特性,而具体的API只要看能反应出框架特性的那几个API就够了,其他的都是用来查的。有些API不用查,你也知道它肯定有,就像你学完java后学C++,你知道C++里肯定有for循环。
<b>只看官网文档是远远不够的,文档只是告诉你可以这样用,但是没有告诉你该不该这样用,所以当你大体了解了一个框架的基础知识后,应该找一本名叫《xxx框架最佳实践》的电子书,了解下怎么用才合理,对于轻量级的框架尤其如此。</b>

学计算机知识,一个wiki百科就够了(要翻墙),千万不要看国内某百科,连自己贴吧的内容都可以当参考文献,实在是太不靠谱了。

二、实践阶段

准备阶段做得越多,实践起来就越轻松,项目需求分析阶段,开发人员的时间如果用来做技术调研、框架选型、编写demo、测试性能、编写非业务组件等工作,时间还是会比较紧张的。总结了一下项目中的问题及解决方案:

2.1 如何编写model层代码

对于mvc新手,经常会误解m的含义,认为m只是表示数据。实际上mvc是按照职责来划分的,而非数据类型。m表示业务层,即包含表示业务逻辑的数据模型,同时也包含操作这些数据的方法。反应到backbone项目中,m层的写法如下:

var Product = Backbone.Model.extend({

    // 业务数据模型
    defaults: {
        name: “”,
        price: 0
    },
    
    // 操作数据的方法
    create: function() {...},
    remove: function() {...},
    modify: function() {...},
    query: function() {...}
});

不要在view中直接调用没有业务含义的底层方法:fetch、save等,这将导致model与view耦合在一起,view层的代码变得繁杂且难以维护。

2.2 事件与逻辑分离

backbone提供了View类,很多人并没有意识到事件与逻辑的解耦,他们通常这样写:

var ProductView = Backbone.View.extend({

    // 注册事件
    events: {
        "click #saveBtn": "create",
        ....
    },
    
    // 创建
    create: function() {

        // 针对click准备一些数据,可能涉及到dom操作
        var data = ....;
        
        // 执行创建的逻辑
        ...
    }
});

当触发创建逻辑的事件不止一个时,会变成这样:

var ProductView = Backbone.View.extend({

    // 注册事件
    events: {
        "click #saveBtn": "createForClick",
        "blur #xxInput": "createForBlur",
        ....
    },
    
    // 创建for click
    createForClick: function() {

        // 针对click准备一些数据,可能涉及到dom操作
        var data = ....;
        
        // 执行创建的逻辑
        ...
    },

    // 创建for blur
    createForBlur: function() {

        // 针对blur准备一些数据,可能涉及到dom操作
        var data = ....;
        
        // 执行创建的逻辑
        ...
    }
});

此时你会发现处理逻辑的代码重复了,所以将事件与逻辑分离的一个优点是:逻辑代码可以复用,正确的写法如下:

var ProductView = Backbone.View.extend({

    // 注册事件
    events: {
        "click #saveBtn": "createForClick",
        "blur #xxInput": "createForBlur",
        ....
    },
    
    // 响应click事件的创建
    createForClick: function() {

        // 针对click准备一些数据,可能涉及到dom操作
        var data = ....;
        
        // 执行创建的逻辑
        this.create(data);
    },

    // 响应blur事件的创建
    createForBlur: function() {

        // 针对blur准备一些数据,可能涉及到dom操作
        var data = ....;
        
        // 执行创建的逻辑
        this.create(data);
    },

    // 执行创建
    create: function(data) {...}
});

事件与逻辑的分离最大的好处是可以方便的进行单元测试,以上面为例,只需针对create方法进行测试,就能验证逻辑的正确性,而非逻辑的dom操作是不在单元测试范围之内的。

2.3 净化路由代码

backbone提供了路由功能,可以方便的根据网址跳转到指定的视图,很多人的写法是这样的:

var AppRouter = Backbone.Router.extend({
    "route1": "createView1",
    "route2": "createView2",
    ....,

    createView1: function() {
        // 1.操作model层方法,获取视图所需数据
        ...
        // 2.new一个视图对象,传递数据到视图中
        ...
        // 3.其他的逻辑
        ...
    },

    createView2: function() {
        // 1.操作model层方法,获取视图所需数据
        ...
        // 2.new一个视图对象,传递数据到视图中
        ...
        // 3.其他的逻辑
        ...
    }
});

随着需求的增加,创建视图的逻辑会变的越来越复杂,整个路由的代码会显得非常臃肿,还有一个问题,不单单路由里需要创建视图,其他的地方也会用到,这个时候,重复的代码又出现了。解决这个问题的方案是抽取出创建视图的逻辑,实际项目中我抽取了一个文件夹叫作:controllers,里面以业务功能为单位,存放创建视图逻辑的js文件,比如处理产品的controller,可以定义为productCtrl.js,代码如下:

var productCtrl= (function() {
    var create = function() {
        // 1.操作model层方法,获取视图所需数据
        ...
        // 2.new一个视图对象,传递数据到视图中
        ...
        // 3.其他的逻辑
        ...
    };

    return {
        create: create
    };
});

此时路由的代码就变得非常清爽了,同时其他地方需要创建视图时,只需要调用productCtrl.create()即可,路由代码:

var AppRouter = Backbone.Router.extend({
    "route1": "createView1",
    "route2": "createView2",
    ....,

    createView1: function() {
        productCtrl.create();
    },

    createView2: function() {
        ...
    }
});
2.4 引入bower-installer优化bower文件结构

前端构建时需要合并第三方的js、css文件,但是每个插件的目录结构不尽相同,导致grunt命令写起来非常繁琐,为了尽量统一处理,引入了bower-installer插件,主要目的是抽取出插件的核心文件,将其放入规则统一的目录中,方便进一步处理。 bower-installer的github地址:https://github.com/blittle/bower-installer

2.4.1 bower-installer使用
  • 安装:npm install -g bower-installer
  • 运行:bower-installer
    具体配置参考github上的文档
2.4.2 基于bower-installer引入第三方插件的流程(以jquery为例)
  1. 配置bower.json文件:"jquery": "1.11.3"
  2. 执行bower install下载jquery插件
  3. 执行bower installer抽取jquery核心文件到bower_main_files目录
  4. 引入文件:在index.html中引入插件的根目录由bower_components改为bower_main_files
2.5 mvvm的缺陷

开发过程一切都很顺利,只踩到两个坑:<b>性能</b>、<b>复杂界面的逻辑处理</b>,而这两个坑是由mvvm的基因决定的。

mvvm的双向数据绑定可以让开发变得非常便利,但同时也带来了两个问题:

  • 为了将model与dom中的元素一一对应进行绑定,即便最简单的界面,也会消耗大量性能,当需要一次性加载大量数据时,性能问题就会凸显。
  • 数据绑定是基于观察者模式构建的,当界面中存在多个组件,且组件间有复杂交互时,代码很难跟踪,调试会变得非常困难,当修改了某个model时,你无法清晰的了解后面会发生什么。

针对mvvm的缺点,以下业务场景是不适合的:

  1. 由于极端的用户体验要求,需要一次性渲染大量数据,不能分页的场景。
  2. 组件多且交互过复杂的场景。

据说angular2.0也倾向单向数据流,估计也是考虑到这个原因,react+flux也推崇单向数据流,也许这是一种趋势。但对于一般的业务场景来说,双向数据绑定还是非常实用的,所以具体采用什么方案还是要结合具体的业务场景。

三、总结

  • backbone.js是一个功能单薄的框架,它需要其他插件的辅助才能变得好用。
  • 没有了解过xxx最佳实践,就不要轻易在项目里使用,不然往往会得到xxx不好用的错误结论。
  • 没有一个模式永远是最佳的,只有适合业务场景的模式才是最佳模式。

推荐阅读更多精彩内容