自己动手:一个 JavaScript 路由器





5.00/5 (12投票s)
构建您自己的 JavaScript 路由器
引言
如果您曾经构建过单页 AJAX 站点,您可能遇到过导航方面的一些问题。首先,标准的浏览器历史记录是损坏的。其次,由于没有经验,我开始使用看起来像这样的 URL:
<a href='javascript:void(0);' onclick='somefunction(this);'>SomeSubPage</a>
这很糟糕,尽管爬虫还没有完全理解 AJAX 站点,但这肯定会破坏它们。此外,在 Android 浏览器等移动浏览器中,click 事件会在 touchEnd
之后触发,导致令人恼火的 300 毫秒延迟。
我们需要找到一种方法来拥有有效的 href
属性,但又不会重新加载页面。进入 URL 的 #(hash) 部分。更改 URL 的 hash 不会重新加载页面。但历史记录会受到影响。因此,我们显然可以绑定到 onhashchanged
事件并解析 URL,运行一些 AJAX,然后更新页面。虽然对于小型网站来说这已经足够了,但如果事情变得更复杂,那就行不通了。我们需要某种 URL 路由。
市面上有一些不错的选择。例如 crossroads、director 和 backbone.js 也包含出色的路由功能。但是,直接使用现成的库一点乐趣都没有。我鼓励使用框架,但在这样做之前,请务必先尝试自己动手制作一个。这样,如果出现问题,您将获得更好的理解。
我们的链接会是什么样子?
<a href="#home">home</a>
<a href="#home/about">about</a>
<a href="#products/">products</a>
所以,如果点击了“主页”,我们希望运行一个完全是“主页”的路由。此路由应从后端服务器获取 /home。
onhashchanged 事件
首先,我们需要检测 URL hash 是否已更改。这是通过绑定到 onhashchanged
事件来完成的。
if ("onhashchange" in window) { // event supported?
window.onhashchange = function () {
console.log(window.location.hash.split('#')[1]);
}
else {
//Support starts at IE8, however be very careful to have a correct DOCTYPE,
//because any IE going in Quircksmode will report having the event, but never fires it.
console.log("hashchanging not supported!")
}
请注意,在 IE 中写入 console.log 会在日志未打开时引发异常。谁想出这个主意的!所以,让我们编写一个小的日志函数来防止这种情况发生。
function log(message) {
try {
console.log(message)
}
catch(err) {
//no action. probably just IE
}
}
路由
我们将把所有内容放在 router.js 中。在其中,我们将从一个匿名函数开始。首先,我们将重点关注 Route 原型。这是实际匹配传入路由的部分。一个路由有三个主要参数。
route
:我们将用来匹配传入路由的路由fn
:当路由匹配时我们将调用的回调函数scope
:我们将在此范围内触发回调函数
它至少需要一个函数,称为 matches
。
(function() {}
var Route=function() {
//We're assuming an object which contains our configuration.
//This would be in arguments[0]
//The route against which we'll match incoming routes
this.route=arguments[0].route;
//The callback function we'll call when the route matches
this.fn=arguments[0].fn;
//The scope in which we'll fire the callback function
this.scope=arguments[0].scope;
}
Route.prototype.matches=function(route) {
//this is somewhat to simple
if(route==this.route) {
return true;
}
else {
return false;
}
}
//We'll create the alias for route in the window object
window["Route"]=Route;
)();
路由器
我们的路由原型有点简单。它只能匹配精确的路由。但我们将先设置骨架,然后再使其更有用。为了完成我们的骨架,我们将需要 Router 原型本身。这至少需要两个函数:
registerRoute
:添加一个新路由以匹配传入的路由applyRoute
:匹配所有已注册路由与传入路由,并在callbackfunction
为true
时触发
(function() {
var Router=function() {
this.routes=new Array();
}
var Router.prototype={
//Here we use a somewhat different style of create the prototype
//than for the Route prototype. Both ways are valid.
//I'm using them mixed here, but It's probably wise not to do that.
//And stick to a single pattern. Here I'm doing it to show both possibilities
registerRoute: function(route, fn, paramObject) {
//We'll have route and function as named parameters
//and all the future optional parameters in a single object.
//Right now we just have scope as a optional parameters
var scope=paramObject?paramObject.scope?paramObject.scope:{}:{};
return this.routes[this.routes.length]=new Route({
route: route,
fn: fn,
scope: scope
});
},
applyRoute: function(route) {
//iterate all routes
for(var i=0, j=this.routes.length;i <j; i++) {
var sRoute=this.routes[i];
//match route
if(sRoute.matches(route)) {
//if true call callback function with the proper scope
sRoute.fn.apply(sRoute.scope);
}
}
}
}
//We'll create an alias for router in the window object
window["Router"]=Router;
//We'll create an instance of router in the window object
window["router"]=new Router();
)();
现在我们有了一个路由器。一个非常非常简单的路由器。它只能匹配精确的路由。但我们可以通过它进行一些测试。
router.registerRoute("home", function() {
log("call home");
});
router.registerRoute("about", function() {
log("call about");
});
router.applyRoute("home"); //call home
router.applyRoute("about"); //call about
router.applyRoute("products"); //no reaction
现在我们需要更改我们的 onhashchange
事件处理程序。
if ("onhashchange" in window) { // event supported?
window.onhashchange = function () {
//we cut of the actual hash
router.applyRoute(window.location.hash.split('#')[1]);
}
else {
//Support starts at IE8, however be very careful to have a correct DOCTYPE,
//because any IE going in Quircksmode will report having the event, but never fires it.
console.log("hashchanging not supported!")
}
现在我们可以将链接放在我们的页面中,这些链接将使用这些路由。
<a href="#home" >Home </a>
<a href="#about" >About</a>
matches 函数
我们现在可能有一个路由器了。但它有点没用。我们需要更复杂的匹配过程。我们希望创建如下路由:
- products/{productid}
- products/:productid
- home/{subpage}
对于 products/{productsid},我们希望 product IDs 作为我们函数的变量传入。:productsid:
也应该在为空时调用产品的回调。home 路由之后可能只跟着 about 或 contact。
所以,让我们让 Route
对象中的 matches
函数更智能一些。
首先,我们需要检查在 Route
类构造函数中传入的路由。
var Route=function() {
this.route=arguments[0].route;
this.fn=arguments[0].fn;
this.scope=arguments[0].scope ? arguments[0].scope : null;
this.rules=arguments[0].rules ? arguments[0].rules: {};
this.routeArguments=Array();
this.optionalRouteArguments=Array();
//Create the route arguments if they exist
this.routeParts=this.route.split("/");
for(var i=0, j=this.routeParts.length; i<j; i++) {
var rPart=this.routeParts[i]
//See if there are pseudo macro's in the route
//will fetch all {id} parts of the route. So the manditory parts
if(rPart.substr(0,1)=="{" && rPart.substr(rPart.length-1, 1) == "}") {
var rKey=rPart.substr(1,rPart.length-2);
this.routeArguments.push(rKey);
}
//will fetch all :id: parts of the route. So the optional parts
if(rPart.substr(0,1)==":" && rPart.substr(rPart.length-1, 1) == ":") {
var rKey=rPart.substr(1,rPart.length-2);
this.optionalRouteArguments.push(rKey);
}
}
}
现在我们将路由的每个部分拆分开来检查。
Route.prototype.matches=function(route) {
//We'd like to examen every individual part of the incoming route
var incomingRouteParts=route.split("/");
//This might seem strange, but assuming the route is correct
//makes the logic easier, than assuming it is wrong.
var routeMatches=true;
//if the route is shorter than the route we want to check it against we can immidiatly stop.
if(incomingRouteParts.length < this.routeParts.length-this.optionalRouteArguments.length) {
routeMatches false;
}
else {
for(var i=0, j=incomingRouteParts.length; i<j && routeMatches; i++) {
//Lets cache the variables, to prevent variable lookups by the JavaScript engine
var iRp=incomingRouteParts[i];//current incoming Route Part
var rP=this.routeParts[i];//current routePart
if(typeof rP=='undefined') {
//The route almost certainly doesn't match it
//is longer than the route to check against
routeMatches=false;
}
else {
var cP0=rP.substr(0,1); //char at position 0
var cPe=rP.substr(rP.length-1, 1);//char at last position
if((cP0!="{" && cP0!=":") && (cPe != "}" && cPe != ":")) {
//This part of the route to check against is not a pseudo macro,
//so it has to match exactly
if(iRp != rP) {
routeMatches=false;
}
}
else {
//Since this is a pseudo macro and there was a value at this place.
//The route is correct.
routeMatches=true;
}
}
}
}
}
return routeMatches;
}
测试我们到目前为止所做的
我们可以创建路由,并且可以测试它们是否匹配。所以,让我们做一些测试。
让我们在我们的 html body 中添加几行代码。
<script>
router.registerRoute("home/:section:", function() {
console.log("home/:section: route true");
});
router.registerRoute("products/{productid}", function() {
console.log("products/{productid} route true");
});
</script>
<a href="#home">Home</a>
<a href="#home/about">About</a>
<a href="#home/contact">Contact</a>
<a href="#products">Products</a>
<a href="#products/5">Product 5</a>
这很棒。路由 0、1、2 和 4 是正确的。路由 3 不对,因为 productid
是必需的。但 product id 是什么?或者 home 路由中的 section 是什么?它的创建方式是我们现在可以确定一个路由是否正确,然后呢?显然,我们需要能够将这些值作为返回函数的输入值的功能。
获取伪宏在路由中的值
我们必须在 Route
原型中引入一个新函数:getArgumentsValues
。它应该返回一个包含这些值的数组。我们将按照出现的顺序将它们发送到我们的回调函数。但首先是函数本身。
Route.prototype.getArgumentsValues=function(route) {
//Split the incoming route
var rRouteParts=route.split("/");
//Create an array for the values
var rArray=new Array();
for(var i=0, j=this.routeParts.length; i < j; i++) {
var rP=this.routeParts[i];//current routePart
var cP0=rP.substr(0,1); //char at position 0
var cPe=rP.substr(rP.length-1, 1);//char at last position
if((cP0=="{" || cP0==":" ) && (cPe == "}" || cPe == ":")) {
//if this part of the route was a pseudo macro,
//either manditory or optional add this to the array
rArray[rArray.length]=rRouteParts[i];
}
}
return rArray;
}
现在路由器是启动我们函数的那一个。所以,让我们改变它执行该部分的代码。
//Change this part;
if(sRoute.matches(route)) {
//if true call callback function with the proper scope
sRoute.fn.apply(sRoute.scope);
}
//Into this;
if(sRoute.matches(route)) {
//if true call callback function with the proper scope and send in the variables
sRoute.fn.apply(sRoute.scope, sRoute.getArgumentsValues(route));
}
现在我们可以将我们的测试代码更改为这样:
router.registerRoute("home/:section:", function(section) {
console.log("home/:section: route true, section=" + section);
});
router.registerRoute("products/{productid}", function(productid) {
console.log("products/{productid} route true, productid=" + productid);
});
最后一次打磨刀刃
我仍然不是 100% 满意。原因是我想在将伪宏的值发送到回调函数之前检查它们是否有效。当然,我可以检查 product ID 是否实际上是一个数字,但我宁愿如果它不是数字,路由就失败。到目前为止,我们都是自己想出功能需求的,但我的脑海里有一些路由器。其中一个就是 crossroads。所以,让我们看看他们是如何解决这个问题的。让我们看看:http://millermedeiros.github.com/crossroads.js/#route-rules。
它说:验证规则可以是数组、RegExp 或函数
- 如果规则是数组,crossroads 将尝试将请求段与数组中的项进行匹配;如果找到项,则参数有效。
- 如果规则是 RegExp,crossroads 将尝试将其与请求段进行匹配。
- 如果规则是函数,crossroads 将根据函数返回的值进行验证(应返回布尔值)。
这应该不难。
规则
我们先从路由器开始。
registerRoute: function(route, fn, paramObject) {
var scope=paramObject?paramObject.scope?paramObject.scope:{}:{};
//Add this line
var rules=paramObject?paramObject.rules?paramObject.rules:null:null;
return this.routes[this.routes.length]=new Route({
route: route,
fn: fn,
scope: scope,
rules: rules
})
},
现在我们将更改我们 Route
的构造函数以使用 rules 对象。
var Route=function() {
this.route=arguments[0].route;
this.fn=arguments[0].fn;
this.scope=arguments[0].scope ? arguments[0].scope : null;
this.rules=arguments[0].rules ? arguments[0].rules: {};
//the rest of the constructor
最后,我们将更改我们的 matches
函数。
//This is the part we need to change
else {
//Since this is a pseudo macro and there was a value at this place. The route is correct.
routeMatches=true;
}
//the change
else {
//Since this is a pseudo macro and there was a value at this place. The route is correct.
//But a rule might change that
if(this.rules!=null) {
var rKey=rP.substr(1,rP.length-2);
//RegExp will return as object. One more test required
if(this.rules[rKey] instanceof RegExp) {
routeMatches=this.rules[rKey].test(iRp);
}
//Array will return as object
if(this.rules[rKey] instanceof Array) {
if(this.rules[rKey].indexOf(iRp) < 0) {
routeMatches=false;
}
}
if(this.rules[rKey] instanceof Function) {
//getArgumentsObject see example package
routeMatches=this.rules[rKey](iRp, this.getArgumentsObject(route), this.scope);
}
}
}
现在,我们可以添加一个 rules
对象,将 productid
设置为数字。
router.registerRoute("home/:section:", function(section) {
console.log("home/:section: route true, section=" + section);
});
router.registerRoute("products/{productid}", function(productid) {
console.log("products/{productid} route true, productid=" + productid);
},
{
rules: {
productid: new RegExp('^[0-9]+$');
}
});
结论
路由器是复杂的东西,但只要有一些常识,我们就可以自己做一个。也许使用现成的产品更好,但我是一个 DIY 的信徒。很多时候,我会 DIY 一些功能,然后扔掉它并引入一个库。但不同之处在于,从那时起,库就不再是神秘的黑匣子了。此外,有时我只需要库提供的功能的 10%。为什么我要带着其他 90% 的负重呢?
源 zip 文件包含 router.js,您可以使用它来构建简单的 AJAX 站点。或者也许只是看一下并学习,然后直接包含 crossroads.js。
希望大家喜欢这篇文章。祝您编码愉快!
历史
- 2013-02-13:修正了文章中的几个拼写错误。下载未更改。