原文:http://www.html-js.com/article/JavaScript-version-100-lines-of-code-to-achieve-a-modern-version-of-Routerjavascript
當前處處可見單頁應用,而對於單頁應用來講咱們必須有一個有效的路由機制。像Emberjs就是創建在一個Router類上的框架。雖然我不是太確信這個是否是我喜歡的東東,可是確定的是AbsurdJS必須有一個內置的Router。和這個框架中的其餘功能同樣,這個Router應該很是小巧簡單的。讓咱們看下這個模塊應該是什麼樣子呢。編輯:github 原文連接:A modern JavaScript router in 100 lineshtml
需求java
這裏設計的router應該是這樣的:git
- 少於100行代碼
- 支持散列輸入的URL,好比http://site.com#products/list
- 可以支持History API
- 提供簡單可用的接口
- 不會自動運行
- 能夠監聽變化
- 採用單例模式
我決定只用一個router實例。這個多是一個糟糕的選擇,由於我曾經作過須要幾個router的項目,可是反過來講着畢竟不常見。若是咱們採用單例模式來實現咱們將不用在對象和對象之間傳遞router,同時咱們也不擔憂如何建立它。咱們只須要一個實例,那咱們就天然而然這樣建立了:github
var Router = { routes: [], mode: null, root: '/' }
這裏有3個屬性:正則表達式
- routes-它用來保存當前已經註冊的路由。
- mode-取值有hash和history兩個選項,用來判斷是否使用History API
- root-應用的根路徑。只有當咱們使用pushState咱們才須要它。
配置api
咱們須要一個方法去啓動router。雖然只要設定兩個屬性,可是咱們最好仍是使用一個方法來封裝下。數組
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; } }
只有在支持pushState的狀況下才會支持history模式,不然咱們就運行於hash模式下。root默認被設定爲‘/’。瀏覽器
得到當前URLapp
這是router中很是重要的一部分,由於它告訴咱們當前咱們在哪裏。由於咱們有兩個模式,因此咱們要一個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中的root部分,同時還須要經過正則(/\?(.*)$/)去刪除全部get的參數。hash模式下比較簡單。注意下方法clearSlashes,它是用來刪除斜線的。這很是有必要,由於咱們不想強制開發者使用固定格式的URL。全部他傳遞進去後都轉換爲一個值。
clearSlashes: function(path) { return path.toString().replace(/\/$/, '').replace(/^\//, ''); }
增長和刪除route
設計AbsurdJS的時候,我是儘可能把控制權交給開發者。在大多數router實現中,路由通常被設計成字符串,可是我傾向於正則表達式。這樣經過傳遞瘋狂的正則表達式,可使系統的可擴展性更強。
add: function(re, handler) { if(typeof re == 'function') { handler = re; re = ''; } this.routes.push({ re: re, handler: handler}); return this; }
這個方法用來填充routes數組。若是隻傳遞了一個方法,那咱們就把它當成一個默認路由處理器,而且把它當成一個空字符串。注意這裏大多數方法返回了this,這是爲了方便級聯調用。
remove: function(param) { for(var i=0, r; i<this.routes.length, r = this.routes[i]; i++) { if(r.handler === param || r.re === param) { this.routes.splice(i, 1); return this; } } return this; }
若是咱們傳遞一個合法的正則表達式或者handler給刪除方法,那就能夠執行刪除了。
flush: function() { this.routes = []; this.mode = null; this.root = '/'; return this; }
有時候咱們須要重置類,那咱們就須要一個flush方法來執行重置。
Check-in
當前咱們已經有增長和刪除URL的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或者直接把函數的參數賦值給fragment。而後咱們使用了一個循環來查找這個路由。若是沒有匹配上,那match就爲null,不然match的只應該是下面這樣的[」products/12/edit/22」, 「12」, 「22」, index: 1, input: 」/products/12/edit/22」]。他是一個對象數組,包含了匹配上的字符串和子字符串。這意味着若是咱們可以匹配第一個元素的話,咱們就能夠經過正則匹配動態的URL。例如:
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」]
這就是咱們可以處理動態URL的緣由。
監控變化
咱們不能一直運行check方法。咱們須要一個在地址欄發生變化的時候通知咱們的邏輯。我這裏說的變化包括觸發瀏覽器的返回按鈕。若是你接觸過History API的話你確定會知道這裏有個popstate 事件。它是當URL發生變化時候執行的一個回調。可是我發現一些瀏覽器在頁面加載時候不會觸發這個事件。這個瀏覽器處理不一樣讓我不得不去尋找另外一個解決方案。及時在mode被設定爲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用於執行比較。
改變URL
在咱們router的最後,咱們須要一個方法能夠改變當前的地址,同時也能夠觸發路由的回調。
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; }
一樣,我麼能這對不一樣的模式作了分支判斷。若是History API當前可用的話,咱們就是用pushState,不然咱們咱們就是用window.location。
最終代碼
下面是最終版本的router,並附了一個小例子:
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 === param) { 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');
總結
router類大概有90行代碼。它支持散列輸入的RUL和History API。若是你不想用整個框架的話我想這個仍是很是有用的。
這個類是AbsurdJS類的一部分,你能夠在這裏查看到這個類的說明。
源代碼能夠在github下載到。