我对Backbone.js的一些认识

backbone.js已经不是当前最流行的前端框架了,但是对于我而言,依然具有比较好的学习价值。虽然目前来说,react,vue等mvvm框架非常火热,但是感觉自身还不到去使用这种框架的层次。这些技术虽好,但是对个人的挑战也是比较大:首先是在编程基础这个部分,包括数据结构,算法,面向对象编程,设计模式,设计原则等等,我觉得在这些方面积累地还不够;其次是工作方法层面,对比angualr,react以及vue,跟传统的用原生js或者jquery写的代码,包括html,css和js三个方面,你会发现这两种技术直接导致的我们在工作结果上的巨大差别,这对于已经习惯了传统开发的我来说,要挑战的不仅仅是新技术的学习跟研究,更多的是工作思路,工作方法甚至是跟其它同事配合协作方式的改变,这个难度也很大。再加上我本人是一个还比较喜欢去钻研细节的人,所以从去年开始做前端开发到现在,一直都没有大胆地学习很多的新东西,相反,我把更多的精力花在了编程思维的锻炼以及基础知识的巩固上,这件事情看起来很小,但是对我的提高很有帮助。

我还记得去年我刚到上家单位的时候,是顶着比较大的压力去做前端的,因为工作第一年,我在武汉做的是VB.NET的开发工作,技术锻炼的很少,逻辑思维方面跟sql方面锻炼地多,因为公司做ERP系统,有封装很好的技术平台,套着用就行了,所以工作都花在写业务逻辑和数据逻辑上;工作第二年我来了北京,在用友做软件实施,折腾了大半年,最后还是觉得在北京做技术最挣钱,就回到了这个本行;到上家单位的时候,情况是:公司当时没有前端,也没有封装前端任何的东西,我也没有专门做过前端,以前做的开发还是VB.NET的后台开发,所以当时我也比较担心怕完成不了当时的工作任务。幸运的是,当时正好赶上一个紧急的项目,公司给机会让我搭一套前端的架子,其实他们要求也不高,能把各种插件套上就行了。我当时想的是,既然要做前端开发的话,有这个机会,还不如自己动手好好模仿以前公司的开发平台写一套东西出来。当时项目非常着急,我们一伙人连续20天的下班时间都在凌晨1-3点之间,我作为唯一的前端,能够做的就是在后台的同事需要某个东西之前,就提前把它开发出来。我就是在那段时间憋出了自己的第一套可以当做开发平台用的东西,虽然这套东西,我到现在都觉得拿不出手,但是它对我起到的作用是,让我开始对代码的重构和设计产生兴趣,我才开始去注意封装思路,以及设计模式和设计原则在编程中的实际使用。曾经写的那一套很粗糙的东西,后来我重构了3次,第一次重构是为了优化API的使用方式,让它更好用;第二次重构是改写各个组件内部实现的方式,并提供各个组件的详细使用文档,以便其它不是很擅长js的同事也能快速使用;第三次重构是使用requirejs做模块化,并且完全跟后端分离,原来有些组件还是借助jsp来搞的。。。虽然我走了,曾经的同事还在使用我写的东西继续开发,而且会用的人都觉得用起来还挺简单的;更有意思的是,同在上家单位的另外一个很好的朋友,5年的java开发经验,几乎不怎么做前端,连ajax都不怎么会,在他前段时间去另外一个单位的时候,用我跟他搭的这一套前后台的东西,竟然自己一个人搞定了一个内部管理系统的所有前端功能。

上家单位的工作经历,让我开始重视自己的代码质量,关注设计模式与设计原则,平常都有意去看相关的博客和书籍,目前来看,成效也很明显:首先是很习惯性地在代码中融入职责分离与开闭原则的思考,我以前写的几篇博客都有相关的提及;其次是部分设计模式在代码中的实际运用,包括单例模式,单例模式,适配器模式,状态模式,观察者模式等,用多了,对它们的理解也就更深刻了。在过去的一年,我还关注的东西有前端工程化构建,js模块化,浏览器缓存管理,移动端页面开发包括适配及优化等,这些都不是很高大上的一些技术名词,仅仅是前端基础知识的范畴,我去了解它们的目的,是觉得这是把这个岗位的工作做好的准备。正是这些对编程思路和基础知识的学习,我现在看到一些新的功能,都能很快地形成工作思路,如果是要写代码的,很快就能想好要写几个类, 看清哪个地方又得拆分成多个类才能让它们之间不会有强耦合,以及跨项目的复用等问题;在工作中,我在现在岗位上能够一个人独立地完成整个前端的工程化管理以及公司产品从后端到PC端以及移动端的所有开发工作,能够整体把控项目的所有代码,而在一年之前,我还是一个刚从软件实施转回软件行业的前端小白。所以我今年也还是不着急去追逐其它火热的技术,继续搞自己的编程思维跟巩固基础知识,前端技术变化那么大,学那么多用不上也不见得是好事,相反把一些工作中也许会用到的好好琢磨透,也许会来的更有意义。比如说,我个人对动效和svg不是很擅长,那么过一段时间,我就会去专门研究这方面的内容,直到自己也能做出一些别人能比较认可的效果出来才行,这样的好处是,在你工作需要的技术范围内,你方方面面都能做地很好。

回到我这篇文章要介绍的backbonejs上来,我为什么在这段时间会去琢磨一个现在不是很火热的前端框架,而且还认为它有比较好的学习价值。因为基于backbone的Model与View的开发方式,或者说它提供的面向对象的代码组织方式,跟我目前惯用的思路还是比较像的,尽管我已有的代码都是jquery搞的。但是它又要优于我现在的编程方式,因为它里面有一个数据驱动UI的思想在里面,而且还有一个很好的内置的事件管理机制,使得它在一些封装层面的东西,比我写的更要严谨,清晰一点。只要是能够提高代码质量的东西,我都认为是编程基础的一部分,这正是我目前仍然想花时间去钻研的东西,所以我想学习。即使不用它做任何的项目,只要把它的思想,能够渗透到我现在的思维中即可,这个我感觉也不太容易,所以我得花一小段时间,才能掌握好它里面的一些机制。我学习它的主要方法是阅读官方文档和写东西实践,阅读文档过程中对于自己有疑问的api,必须写一些简单的代码才能知道它的详细作用。好在它官方文档组织地还不错,所以在学习过程中,需要去测试的api并不是很多。然后为了了解如何在实际工作中运用backbone编写代码以及它与我现在的编程方式上的区别,我分别用jquery跟backbone写了一个todo app。简单起见,jquery实现的版本与官网的功能完全一致,当然代码是不同的;backbone实现的版本在官网的基础上,考虑了异步回调处理以及操作的交互还有批量请求的处理,使得这个简单的app看起来更符合实际的产品需求。最后我发现,虽然这两个版本实现方式不同,但是思路层面却有相似性,这个去看一下两份代码中定义的类名就清楚了。下面是两个版本的demo地址:

jquery版本的预览地址

backbone版本的预览地址

源码在:https://github.com/liuyunzhuge/blog/tree/master/todos

本文将从整体思想,数据驱动,事件机制,存在的问题以及实例分析等多个方面来介绍我对backbone的一些认识,里面有很多个人观点,受技术水平和经验的限制,不一定绝对合理,欢迎批评与指正。

1. backbone的整体思想

js的作用从大范围上讲,主要包含两个内容,第一是将浏览器的数据与服务器的数据在适当的时候进行相互同步;第二是在用户与浏览器,浏览器与服务器之间的交互过程中,控制页面的变化来反馈不同的功能效果。从技术上来说,前者主要体现为异步请求的处理,后者主要体现为对DOM的操作。大家都知道,这两个方面的东西虽然不难,但是很繁琐,backbone也好,后来的angular react也好,它们诞生都有一个目的就是为了简化这两个方面的工作。不过本文的重点肯定只有backbone了,要说明backbone的作用,得先看下我们不采用backbone的时候是如何处理一个极其简单的编辑页面的,然后再来看backbone的解决方法,通过对比就能看出backbone的一些思想以及给我们工作带来的好处。

这是要演练的编辑页面的原型:

需求如下:

1)这个编辑页面用来处理某个类型的数据,这个类型的数据有两个字段分别是id和name,表示它的唯一标识和名称;
2)当用户选择新增模式打开这个编辑页面的时候,页面初始化状态如原型所示,当用户在文本框中输入非空的文本再点击保存时,页面会把新增的文本传递给服务端,由服务端保存后再返回这个数据的id,同时在文本框下方的文本处显示刚刚保存好的数据的名称;
3)当用户选择编辑模式打开这个页面时,页面会传递一个id以便查出要显示的数据条目,当正确查出了数据的名称之后,把它显示到文本框以及文本框下方的文本中。
4)在用户新增完数据之后,以及从编辑模式打开这个页面时,都可以再次编辑文本框中的内容,通过保存按钮,将最新的名称同步至服务器,成功之后,再在文本框下方显示刚录入的名称。

不采用backbone,我们可能会这样去实现这个页面的功能(提供大概的代码,非完整的):

var $input = $('#new_input'),//输入框
    $save = $('#btn_save'),//保存按钮
    $name_text = $('#name_text');//数据名称

//表示页面要编辑的数据的唯一标识,新增时为空,编辑时才有值
var id = (function () {
    //获取页面id,详细实现略
})();

if(id) {
    //编辑时先异步查询数据,再做页面初始化
    $.ajax({
        url: '/api/data/query',
        data: {
            id: id
        }
    }).done(function(res){
        if(res.code == 200) {
            var name = res.data.name;
            $input.val(name);
            $name_text.text(name);
        }
    })
}

$save.on('click', function () {
    var name = $.trim($input.val());
    if (!name) return;

    var params = {name: name};
    //编辑时再绑定一个id参数,以便服务器做更新操作
    id && (params.id = id);

    $.ajax({
        url: '/api/data/save',
        data: params
    }).done(function (res) {
        if (res.code == 200) {
            !id && (id = ~~res.data.id);
            $name_text.text(name);
        }
    })
});

这种实现的主要问题在于:

1)在数据变化的时候,必须手工更新DOM,看那两个ajax请求的回调就知道。对这种简单页面可能还好说,要是页面里面包含几十个不同类型的表单控件时,这些页面的更新操作就会变得非常繁杂,而且还容易出错;

2)缺乏封装,没有体现数据的管理,功能都是直接靠请求与DOM操作实现的,实际上按照面向对象的思路以及职责分离的原则,应该把数据的同步和数据的管理功能单独封装起来,把界面变化的功能也单独封装起来,两部分的内容通过接口或者事件来交互。

如果我们把它换成backbone的写法,就会变成:

//创建一个Data类,来表示一个实体类型
var Data = Backbone.Model.extend({
    //定义每个Data类实例的默认值
    defaults: function () {
        return {
            name: ''
        }
    },
    //解析异步请求返回的结果,fetch方法与save方法都会调用它
    parse: function (res) {
        return res.data;
    }
});

//创建一个AppView类,来完成这个页面的所有UI功能
var AppView = Backbone.View.extend({
    //指定这个AppView的实例关联的DOM元素
    el: 'body',
    //指定这个AppView实例在做DOM更新时要采用的html模板
    template: _.template(document.body.innerHTML),
    //定义这个AppView内部要注册的一些事件,类似jquery的委托方式注册
    events: {
        'click #btn_save': 'save'
    },
    initialize: function () {
        //监听关联的model实例的change事件,只要model实例的属性发生变化,都会调用自身的render方法
        this.listenTo(this.model,'change', this.render);

        this.$input = $('#new_input');
    },
    render: function () {
        //根据model实例的内容重新渲染html
        this.$el.html(this.template(this.model.attributes));
        return this;
    },
    save: function(){
        var name = $.trim($input.val());
        if (!name) return;

        //直接调用model的save方法来与服务器进行同步
        this.model.save({name: name});
    }
});

//创建一个Data实例
var model = new Data();

//创建一个AppView的实例,并把它关联的model属性指定为上一步创建的Data实例
new AppView({
    model: model
});

//表示页面要编辑的数据的唯一标识,新增时为空,编辑时才有值
var id = (function () {
    //获取页面id,详细实现略
})();

if (id) {
    //编辑模式下设置id
    model.set('id', id);
    //通过fetch自动发送请求与后台同步
    model.fetch();
}

对比前面这两份代码,你会发现,backbone的实现:

1)没有了对ajax请求的直接调用

2)没有了对$new_input以及$name_text这两个DOM元素的直接操作

3)引入了html模板,以便能够快速地更新DOM

4)多了很多封装,创建了Data和AppView类,最重要的是这个Data类,它的性质就代表着我们的页面在真实世界或者是数据库中的一个业务实体类型,它的作用一方面是将数据管理的逻辑与界面逻辑进行解耦,同时把数据同步的逻辑包含在自身内部,这也是为啥我们没有看到ajax直接调用的原因,使得数据的逻辑严密性更强,也就是所谓的高内聚。

采用backbone之后,即使将来页面增加了几十个文本控件,有可能我们只需要调整save方法即可,利用jq的serializeArray方法我们能一次性的快速收集整个表单的数据,所以整体上代码也不会增加很多。另外,从代码之道的角度来说,backbone之后的代码由于更强的封装性,使得代码的可阅读性也更强。所以从结果上来说,backbone能够对我们的工作起到的作用还是很明显的。

那么它是如何做到这些的呢?正如你在代码中所看到的:Backbone.Model,Backbone.View,这两个东西就是它实现这些漂亮代码的关键。Model跟View属于Backbone提供的两个核心模块,简单来说,Model这个模块可以让我们用来定义一些纯数据管理的类,大部分情况下,这些类就是我们所要开发的功能对应的业务实体,比如一个学生选课系统中,学生,课程,选课记录这三个都是我们所要开发的功能的业务实体;用Model定义的类能够为我们提供直接修改和获取业务实体数据属性的功能,也能够通过简单明了的api直接与服务器进行同步,比如前面用到的fetch,save,还有没用到的destroy等;View这个模块可以让我们来封装页面中某个独立完整部分的UI功能,它往往会与Model模块进行关联,并且配合模板一起完成UI的更新,通过监听Model实例的变化来重新渲染html,通过自身注册的事件将UI的变化同步到Model实例,它就像一个控制器,同步数据与界面UI的实时变化。除了Model跟View之外,Backbone的底层还有一个sync模块,封装了数据同步时异步请求管理的功能,虽然它是底层的实现,但却不是一个特别好用的东西,在后面的内容中我会说明一些它的不合理的问题,现在只要知道它是用来管理异步请求的即可。Backbone官方文档里面,描述这三个模块之间的关系,用到了一张非常清晰明了的示意图:

我对Backbone.js的一些认识_第1张图片

希望这个图加上前面的举例和描述,能够让你明白这三个模块之间的关系以及作用。在下一部分我还会进一步的去说明这些模块之间是如何互相影响的问题,尤其是Model与View之间的交互。

以上的内容,都是跟前面举例引入的那个编辑页面有关,都是为了说明backbone在简化编辑页面开发的时候,是如何实现的以及它背后的核心内容,但是在实际工作中,我们同样遇到很多的页面功能,并不是只处理单条数据的逻辑,而是以列表的形式展现多条数据,甚至还会有直接在列表上编辑单条数据等更复杂的功能出现,这个时候如果我们还是采用传统方法来实现,肯定还会遇到我们在开发编辑页面时遇到的那些问题,而且哪怕是最简单的列表功能也会比前面的那个编辑页面要复杂不少,所以这种方法也是需要考虑去改进的。backbone为了解决这个问题,使用了另外的一个模块Collection,这个模块你可以把它定义出来的东西看成是一个数组,但是它比数组的功能更丰富,因为它可以指定存储某种Model的实例,代表Model实例的一个集合,也提供有简单的api比如fetch,create方法来直接同步服务器的数据;如果说Model跟View的关系,是把数据与UI进行解耦,那么Collection跟View,就是把数据列表与UI进行解耦,它们的内涵跟机制都是差不多的,只不过Model实例仅仅是作用于单条数据的功能,而Collection实例可以作用于多条数据的功能;就跟Model可以被直接关联到View一样,Collection实例也能直接通过collection属性,在创建View实例的时候,传递给View;在Collection内的model发生增删改的时候,通知View实例去重新渲染html;在用户与View实例发生交互的时候,View主动去调整Collection里面的内容;View层还是充当控制器的作用,实时同步UI与Collection之间的变化。关于Collection这个模块的具体使用,这里就不再提供了,第一,前面给出的todos地址,就是一个很好的例子能对比说明所有模块的作用和关系;第二是,Collection的作用,确实跟Model的作用差不多,理解它的方法,完全可以类比Model。这是Backbone官方文档里面,提供的描述Collection Model View sync这四个模块之间关系的示意图,希望对理解这个模块的作用能有所帮助:

我对Backbone.js的一些认识_第2张图片

以上就是我认为的Backbone整体思想的核心内容。不过看过或者用过的人,肯定知道backbone还有另外几个模块:Events,History和Router,在我看来:

1)Events很重要,是backbone所有机制的核心基础,但它并不是这个框架的思想所在,backbone只是需要它来完成自己想做的事情,你在其它框架里面也能看到这样的基础模块,所以它不属于思想的核心;而且用法简单,没有太多介绍的必要;

2)History跟Router只能算是backbone提供的工具,要是觉得它们不好用,或者不想做单页的应用,完全可以不用它们,至少我现在不会用,所以我也不打算花时间去研究,没有它们,我们依然可以使用backbone构建封装性很强的应用程序。

还有一点就是,虽然backbone为我们提供了sync这个模块,前面我说过它不好用,因为它是强restful风格的api形式,这个得完全看项目团队内的现有情况去考虑是否要这么干,而且它要求必须用http 200来表示成功的请求,这对于那些自己去捕获后台异常,然后对http response自定义code的后台服务来说,显然是有问题的。好在backbone的api还不错,即使我们不直接使用那些sync相关的方法,我们也可以通过手工管理请求的方式来管理数据,那么此时backbone为我们起到的作用,就真的只是让代码更漂亮,让DOM操作更简单了。当然更好地办法就是去重写一个backbone的sync模块了。

下一部分说明前面这两张图里面,这些模块之间互相影响的内部机制。

2. 数据驱动及背后的事件机制

在没有将数据管理从页面逻辑中分离出来之前,能够导致DOM发生变化的大部分是2种情况:第一,是用户的输入,包括键鼠的操作,比如表单输入,窗口滚动及调整,鼠标点击拖拽等;第二,是页面中js代码执行导致的变化。拿第二种情况来说,在一个有大量表单控件的页面中,如果我们想收集表单数据同步到服务器,就必须找到合适的DOM元素然后获得它们的值,再一并组织好通过异步请求发送至后端;如果我们从服务器获取到了表单的初始数据,需要把这些数据一一回填到表单上的各个控件时,我们就必须一一找到各个数据属性对应的DOM元素然后设置它们的值。表单收集相对而言,还是比较容易,但是表单回填就比较麻烦了,尤其是表单组件很多,且有复杂逻辑的时候。依靠手工的方式一一设置value,显得非常重复而繁琐,还容易出问题。对于列表功能,这个问题也会同样存在,比如某一个更新操作,在选中列表中的一行数据后,更新了该数据的内容,当结束编辑的时候,就显然要在该列表中显示该数据条目的最新状态,如果采取手工的方式更新列表中显示的内容,显然就要找到该数据所关联的DOM元素,然后根据数据属性及其位置一一替换。

当backbone把界面逻辑拆分成Model,View和Collection三个模块之后,由于数据的变化,导致UI变化的逻辑,处理起来就特别容易。我们不用再去一一手工按数据属性找到相应的元素去做替换了,只要View层实例,监听到关联的Model实例或者Collection实例有变化(增删改)的时候,去调用View层实例的render方法,重新将界面渲染一遍即可,而这个render方法因为有模板引擎的帮助,渲染的步骤仅用一句话就能完成:

这种方式就可以看作是数据驱动式的DOM更新方式,相比原来手工的方法,它显然要省事不少。如此以来,我们在编码过程中对DOM操作的工作,就能简化不少。而在数据驱动的背后,依赖backbone提供的Events这个基础模块的功能,Model,View以及Collection三个模块都继承了Events,使得它们的实例都拥有直接进行事件管理的能力,都可以直接使用on once trigger off等方法来注册和移除事件监听,另外backbone为了方便起见,还提供了一种主动式的事件绑定方式,相关的api是listenTo stopListenTo listenToOnce,名字都起得很明了,看到的话,非常好理解。这个Events模块,除了让Model等模块的实例拥有强大的自定义事件管理,同时它还提供了一套内置的事件体系,这套事件体系其实就是前面数据驱动的关键:

我对Backbone.js的一些认识_第3张图片

注意红框的这些事件,有些是只有Model实例才会触发的,有的是只有Collection实例才会派发的,有的是都会触发的,只要当View层的实例,与之相关联的Model实例和Collection实例触发了这些事件,都可以直接通知View层实例执行相应的事件回调,我们所要做的只需要在View层实例初始化的时候,注册好跟它关联的Model实例或Collection实例的事件监听即可。如:

image(关联Model实例时)

我对Backbone.js的一些认识_第4张图片(关联Collection实例时)

前面这些能够解释为什么当Model或Collection发生变化的时候,为什么能够引起View层的变化。但是还有一个方面没有说清楚,就是由于用户与浏览器交互导致的View层的变化,如何同步到数据。这个方法,backbone给出的实现,其实跟平常使用jquery绑定各类键鼠事件,然后在事件监听里面直接去更新关联的Model或Collection实例没有区别,事实上,它本身也是这么做的。比如todos这个小东西里面的TodoView,用events注册了以下事件:

我对Backbone.js的一些认识_第5张图片

相应的回调都设置成了View层的实例方法:

我对Backbone.js的一些认识_第6张图片

结合这两段代码,就能发现backbone的写法,就是直接在事件回调里面,去调用Model实例的方法,好在Model类要用的方法都可以实现封装好,所以在调用的时候并不会很麻烦,如果碰到有表单数据收集的场景,也可以考虑写个简单的方法做批量收集,毕竟只要找到表单元素即可完成这个事情。在另外一个View组件AppView里面,你依然可以找到类似的代码,这里就不再重复了。

不过这种从View层到Model层同步的方式,从我个人的角度,有一个别扭的问题,就是当用户的键鼠操作,已经改变了界面上的内容时,由于这些回调内对Model实例的同步操作,会导致Model实例触发change等事件,然后又会导致View层跟Model的change事件绑定的方法(通常是render方法)被再次调用,也就是说页面内容会做一次无意义的重新渲染。为啥无意义,因为本身用户的键鼠操作就已经改变完了界面内容 阿。不过看在DOM操作被简化的份上,这个事情也就算了。但是这个得注意防止事件循环的情况。什么意思呢?就是在render过程中,触发了某些元素的事件,恰巧这些事件,你加了一些不合适的监听,在这个监听里面又做了model层的同步,导致change事件被再次调用,render方法又被触发,某些元素的事件也被触发,然后就事件死循环了。

前面的这些内容,再结合第一部分的那张图,就已经能把backbone的机制说的比较清楚了,最重要的东西无非就是Model,View,Collection对界面的解耦,事件机制,以及模板引擎而已。

3. 细说sync模块的问题

看过官方文档就知道backbone的Model模块提供的fetch, save, destroy方法,和Collection模块的fetch方法,都会发送异步请求,与后端服务进行交互。然后所有这些有异步请求操作的方法,都依赖于sync这个模块,来完成请求头和请求数据的封装,以及请求回调的处理。sync这个模块,采用的是比较原始的异步请求的调用方式,比如它的成功或失败回调还是通过jQuery.ajax调用时传递的success和error这两个option来传递的,这个模块完全遵循rest接口的规范来封装请求信息和请求数据,比如HTTP请求的METHOD会根据调用的方法,选用GET POST DELETE PUT PATCH中的一种;请求数据的传递方式,不是通过form data的形式,而是通过application\json的形式。这种方式从系统接口规范管理来说,肯定是非常有好处的,因为语义化很强,接口的地址跟接口所使用的HTTP METHOD,简单明了的表达了何种资源的何种操作。但是从我个人角度而言,个人对异步请求的使用习惯,以及团队后台同事对rest接口设计的支持程度,都是我自己不想采用这种请求接口的原因,具体来说,有以下几点:

1)我个人习惯还是喜欢只用传统的GET跟POST方法,而不是非得根据对资源操作的语义然后选用DELETE PUT PATCH之类的方法。在异步请求的处理过程中,我认为有两点比较重要,第一是接口的地址,要友好要有语义,方便大家看懂;第二是请求数据和返回数据的封装形式,要便于前后端快速解析,提高工作效率;而具体用什么HTTP METHOD对工作或者对代码影响都很小,只是从整体上感知系统的设计水平不一样而已。我喜欢做一些区别很大的一些改进,而不是为了一些规范而去强加约束。

2)由于这种强rest接口的请求形式,导致请求成功与否完全取决于HTTP请求状态码是否等于200。也就是说,只有在异步请求状态码为200的时候,异步请求的success回调才会被调用,否则都会调用error回调。看过fetch等方法的源码就知道,backbone的Model模块以及Collection模块,在请求成功之后,对实例本身内部管理调用的那些东西,比如set方法,sync事件的触发,都是在success回调里面做的。这个对于那些做了自定义HTTP响应封装的后端服务来说就会存在问题,比如一个HTTP请求,如果成功,HTTP状态码是200,然后后台会返回这样的一个格式的数据:

{code: 200, data: {...}}

如果失败,HTTP状态码还是200,只不过返回的响应就是下面的一个格式的数据:

{code: 500, data: {...}}

到时候前端在回调里面,根据后端自定义的这个后端来判断请求是否成功,这个在现在的一些软件设计里面也很常见,这是后端的设计习惯,而且这种封装形式也挺好的,总比直接抛出HTTP 500要来的友好一些。但是问题就来了,因为不管HTTP请求,从业务角度而言是成功还是失败,HTTP状态码都是200,导致Backbone里面success的回调会始终被触发,而从业务角度而已,显然有一部分情况下被触发的话就是错误的逻辑。

这个问题是我个人觉得在使用rest接口时最不灵活的一个问题。

3)这种rest接口形式把系统的复杂性想的太简单了,每个系统,不是由对资源的简单的增删该查的四种操作就完成了的,做过大型一点的管理系统就知道,一个复杂的功能,可能涉及到某个实体数据的查询,就有可能划分十多个查询的场景,每种查询场景下要使用的参数或者条件都不相同,我在开发这种功能的时候,通常会采用追加一些额外的请求参数来辅助后端进行判断处理,而sync这个模块,并没有提供一个很好的方式来追加自定义的任意的请求参数。在官方文档中,我见到的唯一的一个可以传递额外参数的说明,就是Collection模块的fetch方法,如果用于一些分页查询的场景,可以通过下面的形式来传递分页参数:

Documents.fetch({data: {page: 3}})

可是要是能更灵活一些就好了 阿。

以上就是我觉得在使用backbone做异步请求的时候,从个人角度发现的一些问题,我一再说明是个人角度,因为这些观点都跟我的经验能力习惯有关系,每个人只要坚持自己的方式就好。尽管如此,我还是很喜欢backbone默认的url的生成机制,因为根据model,collection生成的url,比较友好,对其它人看懂系统的请求路径有帮助,所以我会尝试一下完全在工作中使用它的这种restful的url。

有了这些问题之后,我就在想如何去解决它们,只有这样,我才能将backbone完全应用到项目中来。这两天稍微看了一下源码,如果想去改变sync模块内部对请求回调的处理,就必须去改动源码,才能去改变它回调的处理方式,后来看到了它最后对请求调用的一个处理,就发现了一个更好地方法,可以不用去改backbone的源码,只用在外部去覆盖就可以了,这个方法就是去重写Backbone.ajax这个模块,默认情况下它是对jQuery.ajax的代理,我们完全可以重写这个模块,在请求真正发送前,对请求的option做一些小小的改动,就能解决我前面说的那三个问题,把异步请求形式换成传统的方式,同时还能保证Model,Collection模块中跟异步请求相关api的正确使用。

我把这一块的代码写在了另外一个位置,感兴趣地可以去了解一下:

https://github.com/liuyunzhuge/blog/tree/master/backbone_ajax

另外也针对这一个内容,提供了一份测试地址,请参考:

http://liuyunzhuge.github.io/blog/backbone_ajax/index.html

不过这个地址在预览,执行里面的代码的时候,看不到我期望你看到的结果,因为这个页面是发布在gh-pages上的,github不允许这些静态页面发送post请求,所以最好的预览方法,是clone到本地区查看:)

4. 增删改注意事项

这里要介绍的内容,如果只看官方文档中给出的todos,肯定发现不了,但是当你把todos这个应用考虑成一个真实的软件时,你就不难发现官方的todos在增删改时其实是不够的。

先来说增:

想想如果把todos这个app,做成一个直接保存到数据库的应用,我们在增加一个todo的时候的逻辑是怎么样的?在官方给出的代码中,它的新增逻辑是,只要新的todo一保存就立即显示到todo列表里面去,根本都不判断是否有保存成功(当然一个localStorage的应用,也没有失败的情况吧)。如果考虑成一个直接保存的数据库应用,我想大部分人习惯的逻辑应该是这样的,先创建一个todo,然后调用异步请求持久化的数据库,并且给异步请求添加回调,只有根据响应判断请求成功之后,才往todo列表里面添加一个新的,具体代码就是这个样子:

createTodo: function (e) {
    var $new_input = this.$new_input,
        value = $.trim($new_input.val());

    if (e.which == 13 && value) {

        //创建todo
        var td = new Todo({
            text: value
        }, {
            //必须通过collection指定todo属于的集合,否则后面的save方法会报错
            collection: this.todos
        });

        //异步保存
        //此处加wait: true也是为了保证后端请求与前端UI展现一致,只有后端保存成功了,我们才会在前端新增一个TodoView
        var _async = td.save({}, {wait: true}), that = this;

        _async && _async.done(function () {
            //保存成功后与用户交互
            TipView.create({info: '新增成功', type: 'success'}).show();

            //添加到todos,以便触发todos的add事件
            that.todos.add(td);

            $new_input.focus().val('');
        });
    }
}

另外Collection有提供一个create方法,相当于一步完成save和add的操作,这个方法不好,因为它的返回值不是一个xhr对象,不好添加回调,当然使用option.success是可以的,不过这种回调方式已经过时了,不符合现在的习惯了,所以我宁愿拆开来用,先save,再add,代码更清晰。

再来说删:

同样考虑保存到数据库时的删除场景,通常的逻辑应该是先发起删除的异步请求,等请求成功并判断删除成功后,再从界面上清除相关的dom内容。所以正确的做法应该是这样的:

clear: function (e) {
    //1. 此处调用destroy方法建议加{wait: true},目的是为了只有在后端添加成功之后才去更新UI,否则可能会出现后端没有删除成功,但是前端已经删除了的问题
    var _async = this.model.destroy({wait: true});
    _async && _async.done(function () {
        TipView.create({info: '删除成功', type: 'success'}).show();
    });
}

要特别注意那个wait: true选项,因为如果没有这个,model会立即触发destroy事件,有可能导致数据并没有从数据库删除成功,但是界面上已经看不到了,你再次刷新的时候又能看到。

最后说改:

其实改的问题跟前面的增删也差不多,主要是要注意异步请求的回调处理。还有一点要说明的是,wait: true这个选项,要准确使用,因为在有了这种数据驱动模式来修改DOM的时候,我们修改DOM的方式除了用户的输入,js代码直接操作DOM,还有js代码改变跟DOM相关的数据,导致数据的change,最终引起DOM的重新渲染。wait: true这个选项,会影响到我们能否保证model与view始终保持一致性,就是说model在某个状态的时候,view应该就是某个状态,而不是另外一个不相符的状态。什么情况下会导致这种不一致性,看下面的这个代码:

toggle: function () {
        //1. 将异步对象返回,方便view层做交互
        //2. 此处调用save方法不建议加{wait: true},如果加了,就只能等到异步请求成功才会触发change事件,而此时可能UI已经发生变化,最终导致UI与model不一致的问题
        return this.save('complete', !this.get('complete'));
    }

这个方法是todos应用里面,Todo类提供的一个实例方法,它是在点击todo列表上的单个todo项的复选框的时候被调用的:

toggle: function (e) {
    var _async = this.model.toggle();
    _async && _async.done(function () {
        TipView.create({info: '修改成功', type: 'success'}).show();
    });
},

这个代码是TodoView类上的一个实例方法,跟上面的toggle方法位置不同。由于点击todo列表上的单个todo项的复选框这个操作,引发的数据流向是从dom到model,所以当你点击一个没有完成的todo的时候,它的复选框会先勾上,然后你准备调用model.toggle方法,去把model的属性跟复选框的状态同步起来,假如你在model.toggle里面使用wait: true这个选项,那么model的属性同步就只能能到请求成功才行;但是在请求过程中,用户有可能有再次点击复选框的情况,请求也有失败的情况,最终可能会存在UI与model数据不一致的问题。

以上就是一些在做增删改的时候要注意问题,同时还得考虑一些交互的逻辑,这个也是必不可少的,在我前面给出的demo地址中,这些东西都有考虑进去。

5. 如何做批量操作

这个问题也是我当时比较苦恼的一个问题,backbone官方文档也说了,要不就自己写单独的ajax请求吧,好像现有的api方法也不支持批量操作。又得提到官方todos的问题了,官方todos在做批量处理的时候,是直接遍历调用各个todo的相关方法,来完成各个异步操作的,这在实际的工作中,能这么搞吗,得有多少个请求阿,一般批量处理无非就是把要改的东西的主键以数组的形式统一传递给后台,当然要修改的内容也得传递过去,就可以了。所以我是这么来完成todos里面的两个批量操作的:

toggleAll: function (e) {
    //1. 批量修改model,但是先不发异步请求
    var complete = this.$complete_all.find('input')[0].checked, data = [];
    this.todos.each(function (todo) {
        todo.set({complete: complete});
        data.push(todo.toJSON());

        //由于这个批量功能只是对真实的功能场景的模拟,数据实际上还是存在localStorage里面的
        //前面并没有调用todo的save方法,导致数据的修改并没有同步到localStorage里面,所以为了保存数据,必须直接拿localStorage对象来更新todo。
        //在真实的环境中,也就是使用ajax的场景里面,这一步不需要。
        todo.collection.localStorage.update(todo);
    });

    //2. 发送异步请求批量更新
    $.ajax({
        url: '',//这里应该是真实的批量修改的接口地址
        data: {
            data: JSON.stringify(data)
        }
    }).done(function(){
        TipView.create({info: '批量更新成功!', type: 'success'}).show();
    });
},
clearCompleted: function () {
    //1. 先获取所有要删除的model id,放到一个数组里面
    var data = [],completes = this.todos.getComplete();
    completes.forEach(function (todo) {
        data.push(todo.id);
    });

    //2. 发送异步请求批量删除
    $.ajax({
        url: '',//这里应该是真实的批量删除的接口地址
        data: {
            ids: JSON.stringify(data)
        }
    }).done(function(){
        TipView.create({info: '批量删除成功!', type: 'success'}).show();

        completes.forEach(function (todo) {
            //由于这个批量功能只是对真实的功能场景的模拟,数据实际上还是存在localStorage里面的
            //后面的clear跟destory会导致todo不能自动从localStorage里面删除,所以也必须手动的去更新localStorage里面的数据
            //在真实的环境中,也就是使用ajax的场景里面,这一步不需要。
            todo.collection.localStorage.destroy(todo);

            //清空todo的内容,让backbone认为它是一个新创建的对象,以便在下一步调用destroy的时候不会发送请求!
            todo.clear({slient: true});
            todo.destroy();
        });
    });
}

要注意这个批量代码中,有一部分代码在真实的环境下,应该是不需要的,因为我这里只是对真实场景的模拟,如果不加那些代码,我批量修改的数据,就无法持久化到localStorage里面了。

6. 总结

backbone虽然现在有点早了,但是当时刚出的也还是挺火热的,现在还在用的也不少,而且相比react vue那些更高大上的框架,这个框架有自己的特点:第一,它很简单,思想不错,用多了对自己写代码肯定有帮助;第二,其它框架多多少少都会有借鉴它的一些想法,我原来看react的文档的时候,我看到单向数据流,想到一个类似的东西就是backbone,所以用多了再去学其它框架,应该也有好处。然后也没有其它内容可介绍的了,毕竟这也只是一篇介绍backbone大概内容的文章,然后把我自己发现的一些问题以及很多个人的想法说明了一下,总而言之,就是希望能给同样对backbone有兴趣的朋友提供一些参考的东西,如果文中有什么不对的,欢迎任何方式帮我纠正,先谢谢了。

你可能感兴趣的:(我对Backbone.js的一些认识)