时下流行的单页的应用无处不在。有了这样的应用意味着你需要一个坚实的路由机制。像Emberjs框架是真正建立在一个路由器类的顶部。我真不知道,这是我喜欢的一个概念,但我绝对相信AbsurdJS应该有一个内置的路由器。而且,与一切都在这个小库,它应该是小的,简单的类。让我们来看看这样的模块可能长什么样。
要求
路由应该是:
- 在一百行以内。
- 支持hash类型的 URLs如: like http://site.com#products/list.
- 支持History API。
- 提供易用的API.
- 不自动运行。
- 只在需要的情况下监听变化。
单列模式
创建一个路由实例可能是一个糟糕的选择,因为项目可能需要几个路由,但是这是不寻常的应用程序。如果实现了单列模式,我们将不需要从一个对象到另一个对象传递路由,不必担心创建它。我们希望只有一个实例,所以可能会自动创建它。
var Router = {
routes: [],
mode: null,
root: '/'
}
这里有三个我们所需的特性。
- routes:保存当前已注册的路由。
- mode: 显示“hash”或者“history”取决于我们是否运用History API.
- root: 应用的根路径,只在用pushState的情况下需要。
认证
我们需要一个路由器的方法。将该方法添加进去并传递两个参数。
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设置为单斜线“/”。
获得当前URL
这是路由中的重要部分,因为它将告诉我们当前所处的位置。我们有两种模式,所以我们需要一个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的特定格式。不管他通过它转换为相同的值。
clearSlashes: function(path) {
return path.toString().replace(/\/$/, '').replace(/^\//, '');
}
添加和删除路由
在开发AbsurdJS时,我总是的给开发者尽可能多的控制。在几乎所有的路由器实现的路由被定义为字符串。不过,我更喜欢直接传递一个正则表达式。它更灵活,因为我们可能做的非常疯狂的匹配。
add: function(re, handler) {
if(typeof re == 'function') {
handler = re;
re = '';
}
this.routes.push({ re: re, handler: handler});
return this;
}
该函数填充路由数组,如果只有一个函数传递,则它被认为是默认路由,这仅仅是一个空字符串的处理程序。请注意,大多数函数返回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;
}
删除只发生在通过一个传递匹配的正则表达式或传递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,以便我们能够把它和最新的做对比。
更改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...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。