Koa-Router源码解析(下)
发布于 2 年前 作者 hyj1991 2366 次浏览 最后一次编辑是 1 年前 来自 分享

Router中间件的用法

使用Router中间件的方法很简单,一行代码即可搞定:

app.use(router[‘routes']());

那么这一行代码如何实现了Koa框架的路由匹配呢,下面我们详细剖析下router.routes方法。首先老规矩,贴上源码:

Router.prototype.routes = Router.prototype.middleware = function () {
	var router = this;
	var dispatch = function dispatch(ctx, next) {
	debug('%s %s', ctx.method, ctx.path);
	var path = router.opts.routerPath || ctx.routerPath || ctx.path;
	var matched = router.match(path, ctx.method);
	var layerChain, layer, i;
	if (ctx.matched) {
  		ctx.matched.push.apply(ctx.matched, matched.path);
	} else {
  		ctx.matched = matched.path;
	}
	if (!matched.route) return next();
	layerChain = matched.pathAndMethod.reduce(function(memo, layer) {
   memo.push(function(ctx, next) {
   ctx.captures = layer.captures(path, ctx.captures);
    ctx.params = layer.params(path, ctx.captures, ctx.params);
    return next();
  });
  return memo.concat(layer.stack);
}, []);
return compose(layerChain)(ctx, next);
};
dispatch.router = this;
return dispatch;
};

我们对源码来进行分行逐模块的解析:

首先我们对整个router函数进行变形抽象:

Router.prototype.routes = Router.prototype.middleware = function () {
var dispatch = function dispatch(ctx, next) {
	//路由代码处理逻辑
} 
return dispatch;
}

可以很清晰看到,这个路由函数是一个偏函数,返回的dispatch是真正的路由中间件处理函数。

获取路由的匹配

var router = this;
var path = router.opts.routerPath || ctx.routerPath || ctx.path;
var matched = router.match(path, ctx.method);

以上的代码即是路由的匹配部分,可以看到,第一行,使用ctx.path获取到当前http请求的path(这里是因为koa框架的context模块对http的request和response进行了委托,所以使用ctx.path或者ctx.request.path都可以获取到当前http请求的path) 而第二行:

var matched = router.match(path, ctx.method);

则是路由匹配的重点。 这里的router为this,而在原型方法里的this绑定到Router.prototype上,所以router.mach即为Router.prototype.match方法。

Router.prototype.match方法解析

Router.prototype.match = function (path, method) {
//this.stack即为所有开发者注册的路由,看过上一节的可以明白,其实this.stack为一个数组,里面的每一个元素是Layer的实例,即一个经过包装的router实例。这里就不详细展开了,有疑问的可以参看上一节
var layers = this.stack;
var layer;
var matched = {
	path: [],
	pathAndMethod: [],
	route: false
};
//这里对this.stack存储的路由包装数组进行for循环遍历匹配
for (var len = layers.length, i = 0; i < len; i++) {
	//第i个经过包装的路由
	layer = layers[i];
	debug('test %s %s', layer.path, layer.regexp);
	//当前第i个经过包装的路由,包含了mach方法,其实就是字符串的正则匹配,,由于包装过的路由,会把开发者编写的路由path转化为正则表达式,所以此处即为用该正则表达式匹配当前收到的http请求的path,如果匹配到,则第一次命中
	if (layer.match(path)) {
  	//path命中,则将path存入matched对象的path属性数组中
  	matched.path.push(layer);
  	//当前第i个经过包装的路由,其内的methods属性包含了开发者编写的所有允许的方法,例如router.get(‘/index’,new Function),则layer.methods=[‘get’],所以此处即为第二次匹配,即在path匹配到的情况下,还需要http请求的method也命中,才认为真正的命中路由。Tips:~为在js中的按位非,而-1在计算机里采用补码表示,-1的补码为1111111...1111,所以~(-1)即为000000...000,即为0;所以会有~Array.indexOf(item)可以用来判断item在Array里面是否存在。这属于js的逼格写法。
  if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
    	//第二次命中,则将真正匹配到的路由函数放入matched的pathAndMethod属性数组中去。
    	matched.pathAndMethod.push(layer);
    	if (layer.methods.length) matched.route = true;
  	}
}}
return matched;
};

以上对于match方法的注释,我们可以比较容易明白,返回的matched对象中,pathAndMethod属性数组中包含了我们所需要的路由处理函数。

生成真正的路由处理方法

layerChain = matched.pathAndMethod.reduce(function(memo, layer) {
  memo.push(function(ctx, next) {
    ctx.captures = layer.captures(path, ctx.captures);
    ctx.params = layer.params(path, ctx.captures, ctx.params);
    return next();
  });
  return memo.concat(layer.stack);
}, []);
return compose(layerChain)(ctx, next);

通过上一节的分析,我们可以明白,如果路由命中,则matched.pathAndMethod的值为一个数组,数组里面的每一个元素为命中当前http请求的框架使用者编写的经过包装的路由。 由Array.reduce方法可知,第一个memo初始化为[],第一次memo首先push进了一个给ctx添加参数的中间件,接着调用Array.concat方法,将包装后的路由的stack属性中的值push进这个数组,而这里的stack属性由上一节可知,为框架使用者编写的路由处理函数(比如router.get(‘/index’, function(ctx, next){}),中的匿名函数)。 所以很容易明白layerChain得到的额值是一个数组,元素形式为: ctx添加参数的中间件——开发者编写路由处理函数——ctx添加参数中间件——开发者编写路由函数… 以上的例子是在有多个路由匹配到的情况下,例如:

router.all(‘*’,  function(ctx,  next){});
router.get(‘/index’,  function(ctx, next){});

那么最后,通过koa-compose方法,将layerChain转化为koa框架可执行的中间件,最后使用一个function(ctx, next){}(ctx, next)这种IIFE的写法立即执行了该中间件组合。 那么上述这样的一个过程,最终实现了Koa-Router中间件对每一个进来的http请求的路由处理。

Koa2.0中间件编写的一个暗坑:

大家发现没,在reduce方法中,ctx添加参数的中间件,使用了return next(),而不是单纯的next(),这里面隐藏了一个我们编写koa中间件的暗坑: 因为koa2.0,本质上可以写成:

let resuts = Promise.resolve(function(){
	return/yield/await Promise.resolve(function(){
		//…
	}())
}());
results.then().catch();

的形式,这里的next其实指向的是下一个中间件处理,由于这里的使用普通函数编写的中间件(即中间件非async函数非co.wrap(generator函数)),所以此处不加return,就会变成:

let resuts = Promise.resolve(function(){
	Promise.resolve(function(){
		//…
	}())
}());
results.then().catch();

很显然,少了一个return后,中间件可能无法正常结束。这也是我们使用koa2.0框架时,自己编写中间件可能会遇到的一个暗坑,开发者往往忽略这个细节,并且出现异常时难以排查出错误,从而失去对框架的信心。

写在最后

以上对于Koa-Router的流程和源码解析就全部写完了,文章写于回老家的动车上,略显匆忙,如果大家还有疑问,欢迎留言讨论~

回到顶部