寫在前面
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() 發出請求
用圖來說是這樣的:
[caption id="attachment_1178" align="alignnone" width="814"]
backbone-0.1.0[/caption]
最底層最重要的部分是 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 參數並執行路由回調、觸發路由事件
用圖來描述是這樣:
[caption id="attachment_1179" align="alignnone" width="761"]
backbone-1.3.3[/caption]
把路由分為建立路由表(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 為 /)的路由,用著當然難受
暫無評論,快來發表你的看法吧