Backbone.History和Backbone.Routerjavascript
history和router都是控制路由的,作一個單頁應用,要控制前進後退,就能夠用到他們了。css
History類用於監聽URL的變化,和觸發Action方法,他能夠添加對url的監聽,html
Router類用於定義和解析路由規則,並將URL映射到Action。html5
router和history一些我的的註解java
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" /> <title>backbone</title> <style type="text/css"> *{padding:0;margin:0;} .wrap{width:960px; margin: 100px auto; padding: 20px 0;} ul{ list-style: none;} </style> </head> <body> <div class="wrap"> <div id="a1"></div> <div id="a2"></div> <div id="a3"></div> </div> <script src="http://files.cnblogs.com/wtcsy/jquery.js"></script> <script src="http://files.cnblogs.com/wtcsy/underscore.js"></script> <script src="http://files.cnblogs.com/wtcsy/events.js"></script> <script> (function(){ // Backbone.History // ---------------- // Cached regex for stripping a leading hash/slash and trailing space. var routeStripper = /^[#\/]|\s+$/g; // Cached regex for stripping leading and trailing slashes. var rootStripper = /^\/+|\/+$/g; // Cached regex for stripping urls of hash. var pathStripper = /#.*$/; // Handles cross-browser history management, based on either // [pushState](http://diveintohtml5.info/history.html) and real URLs, or // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) // and URL fragments. If the browser supports neither (old IE, natch), // falls back to polling. 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; } }; // Has the history handling already been started? History.started = false; _.extend(History.prototype, Backbone.Events, { // The default interval to poll for hash changes, if necessary, is // twenty times a second. interval: 50, // Are we at the app root? atRoot: function() { var path = this.location.pathname.replace(/[^\/]$/, '$&/'); return path === this.root && !this.location.search; }, // Gets the true hash value. Cannot use location.hash directly due to bug // in Firefox where location.hash will always be decoded. getHash: function(window) { var match = (window || this).location.href.match(/#(.*)$/); return match ? match[1] : ''; }, // Get the pathname and search params, without the root. getPath: function() { var path = decodeURI(this.location.pathname + this.location.search); var root = this.root.slice(0, -1); if (!path.indexOf(root)) path = path.slice(root.length); return path.slice(1); }, // Get the cross-browser normalized URL fragment from the path or hash. getFragment: function(fragment) { if (fragment == null) { if (this._hasPushState || !this._wantsHashChange) { fragment = this.getPath(); } else { fragment = this.getHash(); } } //var routeStripper = /^[#\/]|\s+$/g; return fragment.replace(routeStripper, ''); }, // Start the hash change handling, returning `true` if the current URL matches // an existing route, and `false` otherwise. 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._hasHashChange = 'onhashchange' in window; this._wantsPushState = !!this.options.pushState; this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); this.fragment = this.getFragment(); // Add a cross-platform `addEventListener` shim for older browsers. var addEventListener = window.addEventListener || function (eventName, listener) { return attachEvent('on' + eventName, listener); }; // Normalize root to always include a leading and trailing slash. // var routeStripper = /^[#\/]|\s+$/g; this.root = ('/' + this.root + '/').replace(rootStripper, '/'); // Proxy an iframe to handle location events if the browser doesn't // support the `hashchange` event, HTML5 history, or the user wants // `hashChange` but not `pushState`. if (!this._hasHashChange && this._wantsHashChange && (!this._wantsPushState || !this._hasPushState)) { var iframe = document.createElement('iframe'); iframe.src = 'javascript:0'; iframe.style.display = 'none'; iframe.tabIndex = -1; var body = document.body; // Using `appendChild` will throw on IE < 9 if the document is not ready. this.iframe = body.insertBefore(iframe, body.firstChild).contentWindow; this.navigate(this.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) { addEventListener('popstate', this.checkUrl, false); } else if (this._wantsHashChange && this._hasHashChange && !this.iframe) { addEventListener('hashchange', this.checkUrl, false); } else if (this._wantsHashChange) { this._checkUrlInterval = setInterval(this.checkUrl, this.interval); } }, // Add a route to be tested when the fragment changes. Routes added later // may override previous routes. route: function(route, callback) { this.handlers.unshift({route: route, callback: callback}); }, // Checks the current URL to see if it has changed, and if it has, // calls `loadUrl`, normalizing across the hidden iframe. checkUrl: function(e) { var current = this.getFragment(); if (current === this.fragment && this.iframe) { current = this.getHash(this.iframe); } if (current === this.fragment) return false; if (this.iframe) this.navigate(current); this.loadUrl(); }, // Attempt to load the current URL fragment. If a route succeeds with a // match, returns `true`. If no defined routes matches the fragment, // returns `false`. loadUrl: function(fragment) { fragment = this.fragment = this.getFragment(fragment); return _.any(this.handlers, function(handler) { if (handler.route.test(fragment)) { handler.callback(fragment); return true; } }); }, // Save a fragment into the hash history, or replace the URL state if the // 'replace' option is passed. You are responsible for properly URL-encoding // the fragment in advance. // // The options object can contain `trigger: true` if you wish to have the // route callback be fired (not usually desirable), or `replace: true`, if // you wish to modify the current URL without adding an entry to the history. navigate: function(fragment, options) { if (!History.started) return false; if (!options || options === true) options = {trigger: !!options}; var url = this.root + (fragment = this.getFragment(fragment || '')); // Strip the hash for matching. // var pathStripper = /#.*$/; fragment = fragment.replace(pathStripper, ''); if (this.fragment === fragment) return; this.fragment = fragment; // Don't include a trailing slash on the root. if (fragment === '' && url !== '/') url = url.slice(0, -1); // If pushState is available, we use it to set the fragment as a real URL. if (this._hasPushState) { 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); if (this.iframe && (fragment !== this.getHash(this.iframe))) { // 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) this.iframe.document.open().close(); this._updateHash(this.iframe.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); }, // Update the hash location, either replacing the current entry, or adding // a new one to the browser history. _updateHash: function(location, fragment, replace) { if (replace) { var href = location.href.replace(/(javascript:|#).*$/, ''); location.replace(href + '#' + fragment); } else { // Some browsers require that `hash` contains a leading #. location.hash = '#' + fragment; } } }); Backbone.history = new History; //Backbone.history.start() //Backbone.history.navigate // Backbone.Router // --------------- // Routers map faux-URLs to actions, and fire events when routes are // matched. Creating a new one sets its `routes` hash, if not set statically. var Router = Backbone.Router = function(options) { options || (options = {}); if (options.routes) this.routes = options.routes; this._bindRoutes(); this.initialize.apply(this, arguments); }; // Cached regular expressions for matching named param parts and splatted // parts of route strings. var optionalParam = /\((.*?)\)/g; var namedParam = /(\(\?)?:\w+/g; var splatParam = /\*\w+/g; var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; // Set up all inheritable **Backbone.Router** properties and methods. _.extend(Router.prototype, Backbone.Events, { // Initialize is an empty function by default. Override it with your own // initialization logic. initialize: function(){}, // 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; }, // Convert a route string into a regular expression, suitable for matching // against the current location hash. _routeToRegExp: function(route) { route = route.replace(escapeRegExp, '\\$&') //把正則裏面須要轉移的字符進行轉移 .replace(optionalParam, '(?:$1)?') //把捕獲變成非捕獲 而且變成惰性匹配 .replace(namedParam, function(match, optional) { return optional ? match : '([^/?]+)'; }) //若是是:\w+格式轉化成([^/?]+) 若是是非捕獲格式(?: 則不進行轉換 .replace(splatParam, '([^?]*?)'); //把這種*\w+格式替換成 ([^?]*?) return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); }, // Simple proxy to `Backbone.history` to save a fragment into the history. navigate: function(fragment, options) { Backbone.history.navigate(fragment, options); return this; }, // Execute a route handler with the provided parameters. This is an // excellent place to do pre-route setup or post-route cleanup. execute: function(callback, args, name) { if (callback) callback.apply(this, args); }, // Bind all defined routes to `Backbone.history`. We have to reverse the // order of the routes here to support behavior where the most general // routes can be defined at the bottom of the route map. _bindRoutes: function() { if (!this.routes) return; this.routes = _.result(this, 'routes'); var route, routes = _.keys(this.routes); while ((route = routes.pop()) != null) { this.route(route, this.routes[route]); } }, // Given a route, and a URL fragment that it matches, return the array of // extracted decoded parameters. Empty or unmatched parameters will be // treated as `null` to normalize cross-browser behavior. _extractParameters: function(route, fragment) { var params = route.exec(fragment).slice(1); return _.map(params, function(param, i) { // Don't decode the search params. if (i === params.length - 1) return param || null; return param ? decodeURIComponent(param) : null; }); } }); Backbone.Router.extend = function(protoProps, staticProps) { var parent = this; var child; // The constructor function for the new subclass is either defined by you // (the "constructor" property in your `extend` definition), or defaulted // by us to simply call the parent's constructor. if (protoProps && _.has(protoProps, 'constructor')) { child = protoProps.constructor; } else { child = function(){ return parent.apply(this, arguments); }; } // Add static properties to the constructor function, if supplied. //將靜態方法和 parent上的靜態方法一塊兒擴展到child上面去 _.extend(child, parent, staticProps); // Set the prototype chain to inherit from `parent`, without calling // `parent`'s constructor function. //建立一個新的構造含糊Surrogate ; //this.constructor = child的意思是 Surrogate實例化後的對象 讓對象的構造函數指向child // Surrogate的原型就是parent的原型 // 而後實例化給child的原型, // 這裏不是直接從new parent給child.prototype 而是建立一個新的構造函數,我也不知道爲啥要這樣 var Surrogate = function(){ this.constructor = child; }; Surrogate.prototype = parent.prototype; child.prototype = new Surrogate; // Add prototype properties (instance properties) to the subclass, // if supplied. // 把第一個參數上的屬性擴展到child.prototype if (protoProps) _.extend(child.prototype, protoProps); // Set a convenience property in case the parent's prototype is needed // later. // 拿一個屬性引用父的原型, 以避免之後要用到. child.__super__ = parent.prototype; return child; } })(); </script> </body> </html>
history怎麼實現一個頁面裏面讓瀏覽器前進後退了?用到了如下的方法jquery
onhashchange pushstate正則表達式
onhashchange 給window綁定onhashchange事件,當描點變化的時候,觸發事件,而後就能夠改變頁面了express
但onhashchange是從ie8開始可以支持,若是要作le6,7的兼容則必須用其餘的辦法,backbone的實現是建立一個隱藏的iframe,同時改變瀏覽器的url上的錨點和iframe的錨點,用一個定時器不停的監聽瀏覽器url的變化瀏覽器
pushstate,html5裏面新方法,給window綁定onpopstate方法,查看history.state的值,若是本身經過history.pushState改變了history.state的值,就當作該url以前已經存在,若是history.state是undefined,表示是新地址,要用history.pushState加到history裏面去。app
下面3個小demo分別是實現用onhashchange,pushstate和隱藏iframe實現瀏覽器前進和後退
onhashchange方式
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Lottery Demo</title> <style type="text/css"> *{padding:0;margin:0;} </style> </head> <body> <div id="wrap"></div> <script type="text/javascript" src="jquery.js"></script> <script type="text/javascript"> var urlHash = { "one" : "我是第一頁", "two" : "我是第二頁", "three" : "我是第三頁" } function c(){ var hash = location.hash.replace("#",""); if(hash in urlHash){ $("#wrap").html(urlHash[hash]); }else{ $("#wrap").html("該錨點沒有對應的頁面"); } } function n(fragment){ location.hash = fragment; } window.attachEvent ? window.attachEvent('onhashchange', c) : window.addEventListener("hashchange",c,false); n("one"); //用n函數跳轉描點 </script> </body> </html>
pushstate
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Lottery Demo</title> <style type="text/css"> *{padding:0;margin:0;} </style> </head> <body> <div id="wrap"></div> <script type="text/javascript" src="jquery.js"></script> <script type="text/javascript"> var urlHash = { "one" : "我是第一頁", "two" : "我是第二頁", "three" : "我是第三頁" } function n(fragment){ //history.pushState({lk:fragment,text:fragment}, "", fragment); var text =urlHash[fragment] || "沒有對應的頁面"; history.pushState({lk:fragment,text:text}, "", fragment); $("#wrap").html(history.state.text); } window.addEventListener("popstate",function(){ if(history.state){ $("#wrap").html(history.state.text); } },false); n("one"); //用n函數跳轉描點 </script> </body> </html>
用iframe的方式
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Lottery Demo</title> <style type="text/css"> *{padding:0;margin:0;} </style> </head> <body> <div id="wrap"></div> <script type="text/javascript" src="jquery.js"></script> <script type="text/javascript"> var urlHash = { "one" : "我是第一頁", "two" : "我是第二頁", "three" : "我是第三頁" } var iframe = document.createElement('iframe') iframe.src = 'javascript:0'; iframe.style.display = 'none'; iframe.tabIndex = -1; var iframeWindow = document.body.insertBefore(iframe, document.body.firstChild).contentWindow; var currFragment = null; function n(fragment){ if(currFragment == fragment || fragment=="")return; currFragment = fragment; window.location.hash = '#' + fragment; window.iframeWindow.document.open().close(); window.iframeWindow.location.hash = '#' + fragment; var text =urlHash[fragment] || "沒有對應的頁面"; $("#wrap").html(text); } setInterval(function(){ var fragment = window.location.hash.replace("#","") n(fragment) }, 50); //用n函數跳轉描點 </script> </body> </html>
Backbone.History
Backbone.History是一個類,Backbone.history纔是Backbone.History實例化後的對象,即Backbone.history = new Backbone.History;
Backbone.History有如下比較重要的方法
start
要啓動Backbone的路由功能,首先必須調用Backbone.history.start(),start方法作了如下一些事情
首頁設置 History.started=true 表示該路由功能已經啓動
而後看當前瀏覽器是否支持onhashChange,參數入的參數是但願經過hash的方式仍是但願經過pushstate的方式改變url
根據傳入的root來設置url路徑將怎麼改變,默認以當前的路徑
若是瀏覽器不支持onhashchange(不支持hashchange確定也不會支持pushstate的,由於onhashchange出現的時候,尚未出現pushstate),建立一個iframe
若是是pushstate方式 綁定window的popstate事件去監聽url改變
若是是hashchange方式 綁定window的hashchange事件去監聽url改變
若是不支持onhashchange,則設置一個定時器爲監聽url的改變
var r = Backbone.history; r.navigate("one",true);//這裏錨點是不會改變的 由於沒有調用start方法 r.start(); r.navigate("one",true);//錨點能夠改變了
getFragment
獲取當前的錨點或者是地址(地址,暫且這麼說吧,由於pushstate方式就是後面那串地址不一樣)
route (reg,callback)
添加對路由的監聽事件,第一個參數是個正則,第2個參數是回調函數。以{route: route, callback: callback}的形式存在this.handlers中,當路由改變的時候,會遍歷this.handlers,若是符合其中的route正則,則會執行相關回調
var r = Backbone.history; r.route(/^a/,function(){alert("a")}); //監聽描點若是是以a開頭,則彈出a r.route(/^[^a]/,function(){alert("not a")}) //監聽描點若是不是以a開頭,則彈出not a r.start() r.navigate("aasd/asdf/ss",true) r.navigate("bxx/asdf/ss",true)
navigate (fragment, [options])
第一個參數個地址,須要進入的地址,第2個參數能夠是對象,也能夠是布爾值,true,表示路由要改變,若是有對應的回調事件也要執行,false表示不須要執行對應的回調事件,第2個參數當是對象的時候,trigger值得意義和布爾值的時候同樣,replace值表示修改路由,可是不觸發相應的回調事件,replace是不會記錄在history裏面,返回的時候不會有替換以前的那個地址.在而後改變url地址,設置了pushstate的用pushstate方式,支持onhashchang的直接用location.href來改變描點,不支持onhashchange的,除了改變location.href還要改變iframe的location.href。最後在觸發相關監聽路由的回調事件
在調用navigate方法的時候就會調用一次回調事件了,監聽的時候也會調用一次回調事件的,因此在監聽的時候會判斷是否是同一個路由,同一個就不執行回調了,監聽執行額回調事件也是在瀏覽器的前進和後退的時候在會執行,只用調用navigate,監聽是不會執行回調的,是navigate方法裏面主動調用的
一些例子
要啓用路由功能必定要先調用start
var r = Backbone.history; r.start(); r.navigate("aaa",true) //r.navigate("aaa",{trigger:true}) 這種方式和上面那種方式是同樣的
第二個參數若是設置成false,路由會改變,可是監聽函數是不會觸發的
var r = Backbone.history; r.start(); r.route(/^a/,function(){alert("a")}) r.route(/^b/,function(){alert("b")}) r.navigate("aaaa",true) //能夠觸發監聽回調 r.navigate("aaaa",false) //觸發不了監聽回調
若是第二個參數是一個對象,且設置了replace爲true,改變路由後,以前的路由是不會記錄在history裏面的,並且監聽路由的回調也不會執行
var r = Backbone.history; r.start(); r.route(/a^/,function(){alert(1)}) r.navigate("as",{trigger:true,replace:true}) //監聽路由的回調是不會執行的,且history中不會記錄以前的路由
因此第二個參數false和replace的卻別就在於histroy中是否記錄以前的路由,他們的相同點是都不會觸發監聽回調
Backbone.Router
Backbone.Router基本上對Backbone.history一些方法的封裝了.
route router.route(route, name, [callback])
route方法就是添加路由,路由對應的回調,他有3個參數,第一個參數就是路由了,第2個參數是方法名,若是傳遞了,當匹配域名的時候就會從Router這個對象裏面去掉用該方法名,第3個參數也是回調,不設置第2個參數的時候就會調用第3個參數的回調
route的執行過程以下,首先判斷route參數是否是正則表達式,若是不是則轉成正則表達式,轉換過程下
首先,把字符串route裏面須要轉義的字符所有轉義,轉義規則以下
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; route = route.replace(escapeRegExp, '\\$&');
須要轉義的字符有\,-,{,},[,],+,.,\,?
而後把捕獲變成非捕獲,而且變成惰性匹配,由於這個正則後面要用到的是test方法,根本就不須要捕獲,捕獲會耗去更多的內存.
var optionalParam = /\((.*?)\)/g; route = route.replace(optionalParam, '(?:$1)?')
而後在把 :\w+格式 轉化成([^/?]+),路由上面的傳參數的格式/:xxx,這個主要是把參數轉換成([^/?]+),這個這則的意思就是不能是/和?的任意字符,由於參數的規則就是route/:arguments,若是帶了/可能就路徑錯了,若是有?那就多是url的參數
var namedParam = /(\(\?)?:\w+/g; route = route.replace(namedParam, function(match, optional) { return optional ? match : '([^/?]+)'; })
在而後 把點*以及*後面的字符串轉化成([^?]*?),惰性匹配非?字符串 這個替換是幹嗎用的還沒看懂
var splatParam = /\*\w+/g; route = route.replace(splatParam, '([^?]*?)');
最後給route字符串加上開頭的符號和結尾的符號'^' + route + '(?:\\?([\\s\\S]*))?$',意思是說開頭必須也route開頭,後面能夠跟參數,而後實例化這個字符串爲正則對象而且返回
處理完正則以後,經過Backbone.history.route方法來監聽該路由正則,若是在前進後退的時候路由匹配該正則就是執行回調
navigate (fragment, [options])
這個直接調用的Backbone.history.navigate(fragment, options);
var r = new Backbone.Router; r.route("aa/:a/:a",function(){ alert(Array.prototype.join.apply(arguments)) }) Backbone.history.start() r.navigate("aa/bb/cc",true) //傳入bb cc 兩個參數,能夠打印出來
routes _bindRoutes
若是設置了routes,能夠用_bindRoutes一次性綁定對routes裏面的路由監聽,_bindRoutes裏面就是遍歷routes,調用this,route進行綁定
Backbone.Router = Backbone.Router.extend({ alertA : function(){alert("a")} }) var r = new Backbone.Router({ routes : { "aa" : "alertA", "bb" : function(){alert("b")}, } }) Backbone.history.start() //先擴展alertA方法 而後經過routes批量綁定,若是value對應的是字符串,則會在實例化的r上找該方法,若是是函數就執行該函數 r.navigate("aa",true); r.navigate("bb",true)