時下流行的單頁的應用無處不在。有了這樣的應用意味着你須要一個堅實的路由機制。像Emberjs框架是真正創建在一個路由器類的頂部。我真不知道,這是我喜歡的一個概念,但我絕對相信AbsurdJS應該有一個內置的路由器。並且,與一切都在這個小庫,它應該是小的,簡單的類。讓咱們來看看這樣的模塊可能長什麼樣。正則表達式
路由應該是:api
建立一個路由實例多是一個糟糕的選擇,由於項目可能須要幾個路由,可是這是不尋常的應用程序。若是實現了單列模式,咱們將不須要從一個對象到另外一個對象傳遞路由,沒必要擔憂建立它。咱們但願只有一個實例,因此可能會自動建立它。數組
var Router = { routes: [], mode: null, root: '/' }
這裏有三個咱們所需的特性。瀏覽器
咱們須要一個路由器的方法。將該方法添加進去並傳遞兩個參數。app
var Router = { routes: [], mode: null, root: '/', config: function(options) { this.mode = options && options.mode && options.mode == 'history' && !!(history.pushState) ? 'history' : 'hash'; this.root = options && options.root ? '/' + this.clearSlashes(options.root) + '/' : '/'; return this; } }
mode至關於「history」只有當咱們要和固然只能是支持pushState。不然,咱們將在URL中的用hash。默認狀況下,root設置爲單斜線「/」。框架
這是路由中的重要部分,由於它將告訴咱們當前所處的位置。咱們有兩種模式,因此咱們須要一個if語句。函數
getFragment: function() { var fragment = ''; if(this.mode === 'history') { fragment = this.clearSlashes(decodeURI(location.pathname + location.search)); fragment = fragment.replace(/\?(.*)$/, ''); fragment = this.root != '/' ? fragment.replace(this.root, '') : fragment; } else { var match = window.location.href.match(/#(.*)$/); fragment = match ? match[1] : ''; } return this.clearSlashes(fragment); }
在這兩種狀況下,使用的是全局window.location的對象。在「history」的模式版本,須要刪除URL的根部。還應該刪除全部GET參數,這是用一個正則表達式(/\?(.*)$/)完成。得到hash的值更加容易。注意clearSlashes功能的使用。它的任務是去掉從開始和字符串的末尾刪除斜槓。這是必要的,由於咱們不但願強迫開發者使用的URL的特定格式。無論他經過它轉換爲相同的值。this
clearSlashes: function(path) { return path.toString().replace(/\/$/, '').replace(/^\//, ''); }
在開發AbsurdJS時,我老是的給開發者儘量多的控制。在幾乎全部的路由器實現的路由被定義爲字符串。不過,我更喜歡直接傳遞一個正則表達式。它更靈活,由於咱們可能作的很是瘋狂的匹配。url
add: function(re, handler) { if(typeof re == 'function') { handler = re; re = ''; } this.routes.push({ re: re, handler: handler}); return this; }
該函數填充路由數組,若是隻有一個函數傳遞,則它被認爲是默認路由,這僅僅是一個空字符串的處理程序。請注意,大多數函數返回this。這將幫助咱們的連鎖類的方法。code
remove: function(param) { for(var i=0, r; i<this.routes.length, r = this.routes[i]; i++) { if(r.handler === param || r.re.toString() === param.toString()) { this.routes.splice(i, 1); return this; } } return this; }
刪除只發生在經過一個傳遞匹配的正則表達式或傳遞handler參數給add方法。
flush: function() { this.routes = []; this.mode = null; this.root = '/'; return this; }
有時,咱們可能須要從新初始化類。因此上面的flush方法能夠在這種狀況下被使用。
好吧,咱們有添加和刪除URLs的API。咱們也可以獲得當前的地址。所以,下一個合乎邏輯的步驟是比較註冊入口。
check: function(f) { var fragment = f || this.getFragment(); for(var i=0; i<this.routes.length; i++) { var match = fragment.match(this.routes[i].re); if(match) { match.shift(); this.routes[i].handler.apply({}, match); return this; } } return this; }
經過使用getFragment方法或者接收它做爲函數的參數來得到fragment。以後對路由進行一個正常的循環,並試圖找到一個匹配。若是正則表達式不匹配,變量匹配該值爲NULL。或者,它的值像下面
["products/12/edit/22", "12", "22", index: 1, input: "/products/12/edit/22"]
它的類數組對象包含全部的匹配字符串和子字符串。這意味着,若是咱們轉移的第一個元素,咱們將獲得的動態部分的數組。例如:
Router .add(/about/, function() { console.log('about'); }) .add(/products\/(.*)\/edit\/(.*)/, function() { console.log('products', arguments); }) .add(function() { console.log('default'); }) .check('/products/12/edit/22');
腳本輸出:
products ["12", "22"]
這就是咱們如何處理動態 URLs.
固然,不能一直運行check方法。咱們須要一個邏輯,它會通知地址欄的變化。當發上改變,即便是點擊後退按鈕, URL改變將觸發popstate 事件。不過,我發現一些瀏覽器調度此事件在頁面加載。這與其餘一些分歧讓我想到了另外一種解決方案。由於我想有監控,即便模式設爲hash.我決定使用的setInterval.
listen: function() { var self = this; var current = self.getFragment(); var fn = function() { if(current !== self.getFragment()) { current = self.getFragment(); self.check(current); } } clearInterval(this.interval); this.interval = setInterval(fn, 50); return this; }
咱們須要保持最新url,以便咱們可以把它和最新的作對比。
在路由的最後須要一個函數,它改變了當前地址和觸發路由的處理程序。
navigate: function(path) { path = path ? path : ''; if(this.mode === 'history') { history.pushState(null, null, this.root + this.clearSlashes(path)); } else { window.location.href.match(/#(.*)$/); window.location.href = window.location.href.replace(/#(.*)$/, '') + '#' + path; } return this; }
一樣,咱們作法不一樣取決於咱們的mode屬性。若是History API可用咱們能夠用pushState,不然,用window.location就好了。
這個小例程是最終版本。
var Router = { routes: [], mode: null, root: '/', config: function(options) { this.mode = options && options.mode && options.mode == 'history' && !!(history.pushState) ? 'history' : 'hash'; this.root = options && options.root ? '/' + this.clearSlashes(options.root) + '/' : '/'; return this; }, getFragment: function() { var fragment = ''; if(this.mode === 'history') { fragment = this.clearSlashes(decodeURI(location.pathname + location.search)); fragment = fragment.replace(/\?(.*)$/, ''); fragment = this.root != '/' ? fragment.replace(this.root, '') : fragment; } else { var match = window.location.href.match(/#(.*)$/); fragment = match ? match[1] : ''; } return this.clearSlashes(fragment); }, clearSlashes: function(path) { return path.toString().replace(/\/$/, '').replace(/^\//, ''); }, add: function(re, handler) { if(typeof re == 'function') { handler = re; re = ''; } this.routes.push({ re: re, handler: handler}); return this; }, remove: function(param) { for(var i=0, r; i<this.routes.length, r = this.routes[i]; i++) { if(r.handler === param || r.re.toString() === param.toString()) { this.routes.splice(i, 1); return this; } } return this; }, flush: function() { this.routes = []; this.mode = null; this.root = '/'; return this; }, check: function(f) { var fragment = f || this.getFragment(); for(var i=0; i<this.routes.length; i++) { var match = fragment.match(this.routes[i].re); if(match) { match.shift(); this.routes[i].handler.apply({}, match); return this; } } return this; }, listen: function() { var self = this; var current = self.getFragment(); var fn = function() { if(current !== self.getFragment()) { current = self.getFragment(); self.check(current); } } clearInterval(this.interval); this.interval = setInterval(fn, 50); return this; }, navigate: function(path) { path = path ? path : ''; if(this.mode === 'history') { history.pushState(null, null, this.root + this.clearSlashes(path)); } else { window.location.href.match(/#(.*)$/); window.location.href = window.location.href.replace(/#(.*)$/, '') + '#' + path; } return this; } } // configuration Router.config({ mode: 'history'}); // returning the user to the initial state Router.navigate(); // adding routes Router .add(/about/, function() { console.log('about'); }) .add(/products\/(.*)\/edit\/(.*)/, function() { console.log('products', arguments); }) .add(function() { console.log('default'); }) .check('/products/12/edit/22').listen(); // forwarding Router.navigate('/about');
這個路由僅90行左右,它支持hash類型的URLs和一個新的History API,它真的是有用的若是你不想由於路由而引用一整個框架。
原文參考:http://krasimirtsonev.com/blog/article/A-modern-JavaScript-router-in-1...