原文: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下载到。