写在前面
Backbone似乎是第一个被广泛接受的前端MVC框架,好像确实上年纪了(第一个release在2010年)。现在面对让人眼花缭乱的MV*框架,几乎很难再想起Backbone,更没有理由去选择它
真的是这样吗?还在用Backbone只是因为懒得换,现在还选Backbone只是因为情怀?
不是。因为它足够灵活,Backbone不是MV*框架里最强大的(实际上确实很弱),却是最灵活的,没有之一
一.Backbone v0.1.0
回到2010年,看看Backbone最初想做什么
结构
View 包裹DOM元素。结合jq事件代理,管理视图相关逻辑
-------
Collection Model集合
Model 数据结构
数据增删改查 -> 触发CRUD(create、read、update、delete) -> Backbone.sync() -> Server
-------
Events sync(method, model, success, error)
给任意对象提供自定义事件支持 完成CRUD到RESTful API的转换,由jq.ajax()发出请求
用图来说是这样的:
最底层最重要的部分是Events,M到V的通信、M变更通知Server都是由Events提供的自定义事件完成的,甚至有一种技巧是把Backbone对象自身当做事件总线来用,如下:
// 主题
var EVENT_DATA_READY = 'dataReady';
// 订阅
Backbone.on(EVENT_DATA_READY, function(data) {
console.log(data); // Object {res: 1}
});
// 发布
Backbone.trigger(EVENT_DATA_READY, {res: 1});
Backbone提供的所有类(Model、Collection、View,以及最新版本的Router、History)都是基于Events的,所以,可以在任何一个Backbone实例上使用自定义事件,比如Model实例:
var EVENT_BEFORE_CHANGE = 'beforeChange';
var Model = Backbone.Model.extend({
// Overriding set
set: function(attributes, options) {
this.trigger(EVENT_BEFORE_CHANGE, attributes, options);
// Will be triggered whenever set is called
if (attributes.hasOwnProperty('prop')) {
this.trigger('change:prop');
}
return Backbone.Model.prototype.set.call(this, attributes, options);
}
});
var model = new Model();
model.on(EVENT_BEFORE_CHANGE, function() {
console.log(arguments); // ["key", "value"]
});
// test
model.set('key', 'value');
在model
属性改变之前,我们发出了beforeChange
通知,也就是说,如果Backbone提供的原生事件不够用的话,我们可以随意添加各种beforeXXX
、afterXXX
,以及完全独立的自定义事件
数据同步
关键源码如下:
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
var methodMap = {
'create': 'POST',
'update': 'PUT',
'delete': 'DELETE',
'read' : 'GET'
};
// Override this function to change the manner in which Backbone persists
// models to the server. You will be passed the type of request, and the
// model in question. By default, uses jQuery to make a RESTful Ajax request
// to the model's `url()`. Some possible customizations could be:
//
// * Use `setTimeout` to batch rapid-fire updates into a single request.
// * Send up the models as XML instead of JSON.
// * Persist models via WebSockets instead of Ajax.
//
Backbone.sync = function(method, model, success, error) {
$.ajax({
url : getUrl(model),
type : methodMap[method],
data : {model : JSON.stringify(model)},
dataType : 'json',
success : success,
error : error
});
};
在Model中说明资源对应的url
,然后Model实例变化时触发CRUD,交由jQuery.ajax()
通知Server
提供了RESTful API支持,但鼓励根据实际场景重写,或者不想用这一套数据同步机制的话,不在Model
中说明url
即可,没有任何强加的规则
路由
实际上Backbone第一版没有提供路由控制,只是提供了一个相对通用的工具集,用来实现MVC,连数据同步机制都算是赠品(可用可不用)
MVC
M和V都有明确的定位(分别对应Model和View),但C没有清晰的位置,只好分散在M和V里,数据校验、数据同步等业务逻辑放在Model里,交互逻辑放在View里
二.Backbone v1.3.3
设计理念
Philosophically, Backbone is an attempt to discover the minimal set of data-structuring (models and collections) and user interface (views and URLs) primitives that are generally useful when building web applications with JavaScript.
想在原始数据和界面之上,提出一个通用的最小集
所以这么多年过去了,仅添加了路由支持,最初的Events、Model、Collection、View几乎没什么变化
路由结构
Router 负责建立路由表
-------
History 实际实现路由控制,修改url,监听变动,提取URL参数并执行路由回调、触发路由事件
用图来描述是这样:
把路由分为建立路由表(Router)和路由控制(History)两部分,实际使用时是这样的:
var App = Backbone.Router.extend({
routes: {
// key为path模式,val为事件名
'': 'index',
':list1ItemId': 'list2', // 'rsshelper/2'
':list1ItemId/:list2ItemId': 'detail' // 'rsshelper/2/5'
}
});
//--- run
var app = new App();
// listen route events
app.on('route:index', function() {
console.log('trigger route:index');
}).on('route:list2', function(list1ItemId) {
console.log('trigger route:list2');
}).on('route:detail', function(list1ItemId) {
console.log('trigger route:list2');
});
// 启用pushState,默认是hashchange
Backbone.history.start({pushState: true,
root: location.pathname.replace('index.html', '')
});
在Router中建立路由表,通过Router实例监听路由事件,最后通过History启用路由
看起来由Router全权负责,实际上Router是History的依赖项,实际控制是由History完成的
路由实现
//--- History
// 跳转函数,传入的fragment需要手动进行encode
// 传入options {trigger: true}才会触发路由事件,默认不会
// 传入options {replace: true}才会盖掉当前URL,无法退回,默认是插入一条,退回
// pushState/replaceState | location.hash赋值/location.replace | location.assign(url)刷新
navigate: function(fragment, options) {
if (!History.started) return false;
if (!options || options === true) options = {trigger: !!options};
// Normalize the fragment.
fragment = this.getFragment(fragment || '');
// Don't include a trailing slash on the root.
var rootPath = this.root;
if (fragment === '' || fragment.charAt(0) === '?') {
rootPath = rootPath.slice(0, -1) || '/';
}
var url = rootPath + fragment;
// Strip the hash and decode for matching.
fragment = this.decodeFragment(fragment.replace(pathStripper, ''));
if (this.fragment === fragment) return;
this.fragment = fragment;
// If pushState is available, we use it to set the fragment as a real URL.
if (this._usePushState) {
//!!! backbone不使用state参数,传入了{}
this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
// If hash changes haven't been explicitly disabled, update the hash
// fragment to store history.
} else if (this._wantsHashChange) {
this._updateHash(this.location, fragment, options.replace);
// 更新iframe url
if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) {
var iWindow = this.iframe.contentWindow;
// Opening and closing the iframe tricks IE7 and earlier to push a
// history entry on hash-tag change. When replace is true, we don't
// want this.
if (!options.replace) {
iWindow.document.open();
iWindow.document.close();
}
//! 把iframe的window.location对象传过去了
this._updateHash(iWindow.location, fragment, options.replace);
}
// If you've told us that you explicitly don't want fallback hashchange-
// based history, then `navigate` becomes a page refresh.
} else {
return this.location.assign(url);
}
// 查路由表
if (options.trigger) return this.loadUrl(fragment);
}
//--- Router
// Manually bind a single named route to a callback. For example:
//
// this.route('search/:query/p:num', 'search', function(query, num) {
// ...
// });
//
route: function(route, name, callback) {
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (_.isFunction(name)) {
callback = name;
name = '';
}
if (!callback) callback = this[name];
var router = this;
Backbone.history.route(route, function(fragment) {
var args = router._extractParameters(route, fragment);
if (router.execute(callback, args, name) !== false) {
router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args);
Backbone.history.trigger('route', router, name, args);
}
});
return this;
}
实际建立路由表时(Router.route()
中的Backbone.history.route()
),把Router实例整个交给了History,所以History是在Router之上的
History中路由实现方式也是pushState
-> onhashchange
-> iframe poll
的降级方案,但Backbone默认不使用pushState
,需要手动启用pushState
,这与其它框架的路由实现方案不同:
// 启用pushState,默认是hashchange
Backbone.history.start({pushState: true,
root: location.pathname.replace('index.html', '')
});
服务端支持
Backbone路由需要服务端支持,如下:
Note that using real URLs requires your web server to be able to correctly render those pages, so back-end changes are required as well. For example, if you have a route of /documents/100, your web server must be able to serve that page, if the browser visits that URL directly.
至少要保证定义的路由可访问,强制要求路径可访问,对于不愿意/没必要做同步镜像站的单页面应用来说,这一点就很难受,大大限制了路由的灵活性
还需要注意root
的问题
If your application is not being served from the root url / of your domain, be sure to tell History where the root really is, as an option: Backbone.history.start({pushState: true, root: “/public/search/”})
默认history.start()
后直接跳转到/
,而不是页面所在的当前路径,所以上面的示例做了一件很奇怪的事情:
// 启用pushState,默认是hashchange
Backbone.history.start({pushState: true,
//!!! 把root改为当前路径
root: location.pathname.replace('index.html', '')
});
虽然路由在单页面应用中有关键性作用,但毕竟不是业务逻辑,为个路由折腾这么多就显得麻烦了
三.Backbone与单页面应用
单页面应用至少需要:
视图(假页面)
路由(建立页面与URL的联系)
模版(用于从缓存恢复页面)
缓存机制(单页面应用的一大优势)
Backbone提供了前三个,但很难满足应用场景,因为:
不支持子视图,而稍复杂的场景都需要嵌套视图
路由很不好用,麻烦,且不灵活
模版来自underscore,同样不支持子模版
当然,勉强要用的话,也是可以的,只是需要多做一些事情,多写一些代码,然后好处是一切尽在掌控之中
几乎所有细节都在掌控之中,这是很实在的好处,也是Backbone与其它框架最大的差别,约束少,非常灵活
但仍然不推荐单页面应用采用Backbone,因为太费劲了,没有必要,Angular显然更合适更方便
四.灵活性
作为框架,Backbone是最灵活的了,对比Angular等重量级选手,Backbone限制最少,自由度最高:
原生View事件不够多。很容易自己添
不想修改Model,非要手动操作DOM。随意,然后手动保持Model一致就好
嫌性能太差,有很多不必要的
render()
。不用内置add
、remove
、change
、sort
事件了,需要渲染的时候再自定义事件通知View渲染嫌underscore模版太弱了,想换jade。随便换,反正
view.render()
是完全可控的嫌jQuery太大了,想换Zepto。可以,换这个几乎没有成本(如果没有用到Zepto不支持的jQuery API的话)
嫌underscore太慢了,想换lodash。当然可以,完全没有成本
Backbone把DOM操作交给jQuery/Zepto
实现,把Collection操作交给underscore/lodash
实现,自己仅保证数据与界面之间最核心的那一部分,并且没有内置的渲染机制,Backbone只是建议哪些东西应该放在哪里,而不放在建议位置也完全可以,尤其适用于需要精细控制的场景
五.应用场景
之前以为Backbone过时了,没什么用,因为比起其它各种MV*框架,Backbone显得太弱了
业务需要实现类似于易企秀、搜狐快站等通过拖放快速生成页面的东西,最初考虑数据绑定是个问题,决定采用Angular,很快发现Drag&Drop很难与Angular结合起来,因为DnD需要持有DOM元素,而Angular要不停的创建新的DOM元素,又不希望直接访问DOM元素,因为可能会引起状态不一致
灵活组合
改用Backbone,立即发现了其巨大优势,Backbone很容易和其它第三方东西组合起来,因为没有约束和限制,喜欢访问DOM?随意。还想直接修改?随意。状态不一致会不会影响渲染?当然会,但渲染部分也全都是自己写的,会影响什么一清二楚
依赖DOM元素的第三方库,比如DnD,很难和重量级MV*框架组合起来,因为大而全的框架一般都有道德约束:强烈不建议直接修改DOM,而应该修改Model,让渲染机制去重新渲染DOM。用Backbone就不会有这样的问题,它很松散,很随意,自由可控
即便勉强组合起来了,如果发现第三方库无法满足需求,需要扩展改写,就会发现MV*框架的层层限制让一切都变得很困难,因为框架提供了很多未知的内部机制,可能牵一发而动全身,而Backbone几乎没有内置什么机制,Model、View想怎么捏就怎么捏
细节控制
用Backbone能保证一切都在掌控之中,每一个Model属性,每一个change
,每一次render()
都是完全可控的,Backbone似乎只提供了事件机制,其它的都是自己捏的。逻辑很清晰,没有那么多未知的内部机制,不用小心翼翼的遵守厚厚的最佳实践
发现存在多余的render()
,很容易在render()
里滤掉,发现内置的change
事件不好用,完全可以不用,轻松改用自定义事件……几乎所有细节都是可控的,Model、View一点都不神秘,也不特殊,都只是支持自定义事件的普通对象而已
简单干净
如果只想要个简单的数据绑定(Model、View),那Backbone无疑是最好的选择,因为它没有强加任何限制,也没有引入太多不需要的东西,恰好勉强能满足需求,又没有额外成本(学习成本、迁移成本等等)
P.S.其实第一版Backbone提供的东西已经足够了,后来添的Router和History倒是有些鸡肋,因为路由不需要非常灵活,但一定要足够方便,我们想要的路由就是URL与对应处理函数之间的联系,像Angular路由那样,而一贯追求通用的Backbone却提供了这样一个既不灵活(强约束,需要服务端支持)又不方便(默认root
为/
)的路由,用着当然难受