前言
首先發一點牢騷,互聯網公司變化就是快,我去一個團隊往往就一年時間該團隊就得解散,這不,公司居然把無線團隊解散了,我只能說,我那個去!!!
我去年還到處讓人來呢,一個興興向榮的團隊說沒就沒了啊!我找誰哭去......
於是我們團隊一個大哥說他去哪哪就解散,我老大說他去哪哪就倒霉,如此看來,不是我們導致團隊解散,而是所有的團隊變化都快啊。。。。。。
於是換了個團隊,近幾周情緒較低落啊,但是低落也不能不干實事,所以在此收拾心情明天開始好好干事情吧!
在去年的時候,我們初略的學習了下backbone,一方面是因為期間比較忙,另一方面是因為沒有使用backbone開發項目,所以到路由一節就有點斷斷續續了
於是這個星期正好有空,我們來回看下backbone路由相關的知識點,這里以之前的demo為例
pushState
在介紹backbone路由之前,我們簡單說一下pushState,pushState的出現與Ajax有莫大的關系,無刷新操作帶來了很多優點,但是也會造成一些問題
這些問題中有兩個問題讓人比較頭疼:
① SEO
② 瀏覽器后退
SEO不在我們今天的考慮范疇,所以我們來看看讓人頭疼的瀏覽器后退問題,這里就不得不提pushState了
pushState的提出便是為了即實現局部刷新,又同時改變瀏覽器URL,並且很友好的支持瀏覽器前進后退功能
之前我們為了實現同樣的功能會注冊hashChange事件,劫持瀏覽器的跳轉,但是這樣做不太標准,於是我們來見識一下pushState
window.history.pushState({
title: title,
url: url,
XXX: xxx
}, document.title, url)
與之對應的是popstate事件,該事件會在瀏覽器前進后退時候觸發,他會捕捉當前URL中的data,這里以一個例子說明
首先第一步,為簡單Ajax:
<html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> </head> <body> <ul id="sec"> <li data-key="1">北京</li> <li data-key="2">上海</li> <li data-key="3">成都</li> </ul> <h1 id="city"> </h1> <script src="../../jquery-1.7.1.js" type="text/javascript"></script> <script type="text/javascript"> $('#sec li').on('click', function (e) { var el = $(e.target); $.get('./Handler.ashx', { id: el.attr('data-key') }, function (data) { $('#city').html(data); var s = ''; }); }); // window.addEventListener("popstate", function (e) { // var s = ''; // }); </script> </body> </html>
對應服務器程序:

public class Handler : IHttpHandler { public void ProcessRequest (HttpContext context) { context.Response.ContentType = "text/plain"; string name = "無名"; string id = "0"; id = context.Request.QueryString["id"]; if (id == "1") name = "北京"; if (id == "2") name = "上海"; if (id == "3") name = "成都"; context.Response.Write(name); } public bool IsReusable { get { return false; } } }
這個時候便是點擊一個項目便從服務器取得一個項目顯示,這里有兩個新需求:
① url需要跟着變
② 可前進后退,於是我們這么修改
1 $('#sec li').on('click', function (e) { 2 var el = $(e.target); 3 $.get('./Handler.ashx', { id: el.attr('data-key') }, function (data) { 4 $('#city').html(data); 5 //新增pushState邏輯 6 var state = {}; 7 state.title = data; 8 document.title = data; 9 history.pushState(state, data, location.href.split("?")[0] + '?cityId=' + el.attr('data-key')); 10 var s = ''; 11 }); 12 });
這個時候每次點擊URL就會跟着改變了,但是並不會跳轉哦
最后加上前進后退邏輯
1 $('#sec li').on('click', function (e) { 2 var el = $(e.target); 3 $.get('./Handler.ashx', { id: el.attr('data-key') }, function (data) { 4 $('#city').html(data); 5 //新增pushState邏輯 6 var state = {}; 7 state.title = data; 8 document.title = data; 9 history.pushState(state, data, location.href.split("?")[0] + '?cityId=' + el.attr('data-key')); 10 var s = ''; 11 }); 12 }); 13 window.addEventListener("popstate", function (e) { 14 var state = history.state;//e.state 15 document.title = state.title; 16 $('#city').html(state.title); 17 var s = ''; 18 });
如此便能簡單達到目的,對於其講解暫時到此,為什么這里會提到pushState呢,因為在單頁應用中,路由的處理是一個問題,而pushState是個很好的處理方案
但不是沒有問題的,pushState只支持IE10,所以意味着直接拋棄了winphone為IE9的手機,有幾個人投訴winphone不能訪問就得扣工資
popstate與hashChange
這里我們順帶說下popstate與hashChange直接的關系,事實上他們之間沒有關系
http://www.google.com/path?query=querystring#hashstring
① http://為協議
② www.google.com為host
③ path為path,也就是我們的子目錄,每個子目錄可以干很多事情
④ #hashstring為hash相關
①-③的變化會觸發服務器請求,會刷新頁面,hash改變不會觸發服務器請求
所以最初的單頁應用,很多都是以hashChange為主,Backbone也不例外初始狀態下是監控hashChange
而pushState的提出多少改變了這一現狀,我們可以改變前面的東西而不引起服務器請求,pushState基本就是干這個事情的
popstate 為1-3環節的變化引起的回調
hashChange 為hash變化引起的回調,是不同滴
一個單頁應用一般只會注冊一個事件,我們之前是hashChange,今天試一試pushState
backbone簡單應用
了解了pushState后,再讓我們回到今天的正題,其實,我們團隊自己也有一套單頁應用框架,前段時間與backbone做了一次競品分析
事實上的結果是使用backbone代碼量會少一點,所以backbone仍然有一定優勢
PS:當時沒好意思給老板匯報給糊弄過去了
下載源碼:02backbone.zip
這里還是簡單做一下解釋:
這里主要定義了兩個視圖,index(列表)以及detail,以及對應的model信息,這些我們不予關注,我們將關注點放到路由一塊
1 var App = Backbone.Router.extend({ 2 routes: { 3 "": "index", // #index 4 "index": "index", // #index 5 "detail": "detail" // #detail 6 }, 7 index: function () { 8 var index = new Index(this.interface); 9 10 }, 11 detail: function () { 12 var detail = new Detail(this.interface); 13 14 }, 15 initialize: function () { 16 17 }, 18 interface: { 19 forward: function (url) { 20 window.location.href = ('#' + url).replace(/^#+/, '#'); 21 } 22 23 } 24 }); 25 26 var app = new App(); 27 Backbone.history.start();
由於app中initialize什么都沒有干,所以實例化並未執行任何處理,入口在history.start處
Backbone.history
Backbone全局有一個處理hash的實例,其構造函數為History
var History = Backbone.History = function () { this.handlers = []; _.bindAll(this, 'checkUrl'); // Ensure that `History` can be used outside of the browser. if (typeof window !== 'undefined') { this.location = window.location; this.history = window.history; } };
我們調用的start其實是他的一個原型方法,這里便用到了pushState

start: function (options) { if (History.started) throw new Error("Backbone.history has already been started"); History.started = true; // Figure out the initial configuration. Do we need an iframe? // Is pushState desired ... is it available? this.options = _.extend({}, { root: '/' }, this.options, options); this.root = this.options.root; this._wantsHashChange = this.options.hashChange !== false; this._wantsPushState = !!this.options.pushState; this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); var fragment = this.getFragment(); var docMode = document.documentMode; var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); // Normalize root to always include a leading and trailing slash. this.root = ('/' + this.root + '/').replace(rootStripper, '/'); if (oldIE && this._wantsHashChange) { this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow; this.navigate(fragment); } // Depending on whether we're using pushState or hashes, and whether // 'onhashchange' is supported, determine how we check the URL state. if (this._hasPushState) { Backbone.$(window).on('popstate', this.checkUrl); } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) { Backbone.$(window).on('hashchange', this.checkUrl); } else if (this._wantsHashChange) { this._checkUrlInterval = setInterval(this.checkUrl, this.interval); } // Determine if we need to change the base url, for a pushState link // opened by a non-pushState browser. this.fragment = fragment; var loc = this.location; var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root; // If we've started off with a route from a `pushState`-enabled browser, // but we're currently in a browser that doesn't support it... if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) { this.fragment = this.getFragment(null, true); this.location.replace(this.root + this.location.search + '#' + this.fragment); // Return immediately as browser will do redirect to new url return true; // Or if we've started out with a hash-based route, but we're currently // in a browser where it could be `pushState`-based instead... } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) { this.fragment = this.getHash().replace(routeStripper, ''); this.history.replaceState({}, document.title, this.root + this.fragment + loc.search); } if (!this.options.silent) return this.loadUrl(); },
好的代碼是會檢測兼容性的,若是支持便使用pushState,不支持便還是使用hashChange
if (this._hasPushState) { Backbone.$(window).on('popstate', this.checkUrl); } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) { Backbone.$(window).on('hashchange', this.checkUrl); } else if (this._wantsHashChange) { this._checkUrlInterval = setInterval(this.checkUrl, this.interval); }
值得注意的是,要使用的話調用方法有所不同
Backbone.history.start({ pushState: true });
所以默認情況使用的是hashChange,我們這先繼續往下看,統一的入口是loadUrl
loadUrl: function (fragmentOverride) { var fragment = this.fragment = this.getFragment(fragmentOverride); var matched = _.any(this.handlers, function (handler) { if (handler.route.test(fragment)) { handler.callback(fragment); return true; } }); return matched; },
這里會的handlers事實上是Router里面定義的集合
這個值是實例化Router時候導入的,這里暫時不予關注
this._bindRoutes();
因為首次url為'',所以會執行相關的路由回調,這里是執行實例化Index view的操作
我們點擊列表項會觸發hashChange,最后又會執行loadUrl,此時因為hash已經變成了detail,所以會執行對應的回調
所以,一輪下來,我們發現Backbone初始化狀態並未使用pushState,而是簡單的hashChange處理路由,開啟pushState規則會有一點點變化
我去有道雲筆記看了下,他也是使用Backbone的項目,並且啟用了pushState,但是其操作中后退功能做的並不好,事實上他后退沒有操作
我們這里要使用pushState,對forward接口的代碼需要做一定調整,第一個要調整的就是“#”,我們得使用標准的“?”
我們這里直接使用Backbone自帶的navigation了,開啟pushState的情況下,每次會在后面加一個“/view”
然后設置trigger為true,便會觸發檢查url找到對應路由后會執行相關回調
最開始,我這里開啟pushState不成功,是因為要設置root的關系......不正在使用下,就算看到源碼也不知道在干什么
if (!fragment.indexOf(root)) fragment = fragment.substr(root.length);
如此一來,我們整個邏輯也就結束了
階段總結
Backbone路由一塊事實上分為兩個大塊,Router以及History
用戶在Router中定義相關規則,然后開啟history.start進行路由監控,一來就會執行默認的回調
所以,Router本身除了定義路由規則,全部調用的是Backbone.history的方法
Backbone.History的主要關注點事實上是 popstate(hashChange)的回調checkUrl,無論我們觸發navigate或者點擊瀏覽器后退
皆是進入此入口,再回調至Router定義的回調,從而實現相關邏輯
片面想法
我其實也是初次實際使用Backbone的路由功能,Backbone的Router以及History事實上作為了控制器而存在
但是,我對這樣的應用卻有不一樣的想法
首先,全部的路由都定義到Router里面有點過重,雖然現在不會有過於復雜的路由配置,難保以后不會出現
其次,作為控制器而言,若是用戶不做一定擴展的話,很多特性加不上去,比如我要實現View以及View之間的通信就缺少依據
最后,以上皆是我移動片面的想法,初學者嘛......
我們這個周末學習了Backbone,卻忘了發出來,現在發出來,希望對各位有幫助