大家用backbone、angular,可能都習慣了內置的路由,這兩個框架的路由都是非常優秀的,強大而簡單。
客戶端(瀏覽器)路由原理其實比較簡單,其實就是監聽hash的變化。
在之前的架構探討中,說到director.js這個路由類庫不好使,那么,在這一篇,我們嘗試自行實現一個簡潔而且非常好使的路由類庫。
原理先介紹,無非幾個步驟:
- 建立配置表(字符串路徑和函數的映射)
- 監聽路由(onhashchange)
- 處理路由變化,跟配置表的路徑做匹配
- 路徑轉化為正則表達式
- 正則exec,匹配+抽取參數
其中難點就在於路徑轉化為正則表達式,director沒做好就是這一步,而backbone則做得非常非常強大,那么我們可以嘗試把backbone這一塊代碼摳出來。
路由表:
var Route = root.Route = { init: function (map) { var defaultAction = map['*']; if(defaultAction){ Route.defaultAction = defaultAction; delete map['*']; } Route.routes = map; init(); onchange(); }, routes: {}, defaultAction: null };
監聽路由變化:
/** * 這段判斷,引用於director:https://github.com/flatiron/director */ function init(){ if ('onhashchange' in window && (document.documentMode === undefined || document.documentMode > 7)) { // At least for now HTML5 history is available for 'modern' browsers only if (this.history === true) { // There is an old bug in Chrome that causes onpopstate to fire even // upon initial page load. Since the handler is run manually in init(), // this would cause Chrome to run it twise. Currently the only // workaround seems to be to set the handler after the initial page load // http://code.google.com/p/chromium/issues/detail?id=63040 setTimeout(function() { window.onpopstate = onchange; }, 500); } else { window.onhashchange = onchange; } this.mode = 'modern'; } else { throw new Error('sorry, your browser doesn\'t support route'); } }
處理路由變化,先拼湊正則表達式:
/** * 引自backbone,非常牛逼的正則 * @param route * @returns {RegExp} */ function getRegExp(route){ var optionalParam = /\((.*?)\)/g; var namedParam = /(\(\?)?:\w+/g; var splatParam = /\*\w+/g; var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; route = route.replace(escapeRegExp, '\\$&') .replace(optionalParam, '(?:$1)?') .replace(namedParam, function(match, optional) { return optional ? match : '([^/?]+)'; }) .replace(splatParam, '([^?]*?)'); return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); }
從原來的:module2/:name變成標准的正則表達式,個中奧妙大家自行頓悟
循環匹配:
function onchange(onChangeEvent){ var newURL = onChangeEvent && onChangeEvent.newURL || window.location.hash; var url = newURL.replace(/.*#/, ''); var found = false; for (var path in Route.routes) { var reg = getRegExp(path); var result = reg.exec(url); if(result && result[0] && result[0] != ''){ var handler = Route.routes[path]; handler && handler.apply(null, result.slice(1)); found = true; } } if(!found && Route.defaultAction){ Route.defaultAction(); } }
然后。。。做個簡單的測試:
<script src="libs/backbone-route.js"></script> <script> Route.init({ 'module1': function(){ console.log(1); }, 'module2/:name/:age': function(){ console.log(2, arguments); }, 'module3(/:name)(/:age)': function(){ console.log('3', arguments); }, '*': function(){ console.log(404); } }); </script>
本文代碼:https://github.com/kenkozheng/HTML5_research/tree/master/backbone-route