koa-router源码学习
基本用法
const koa = require('koa');
const Router = require('koa-router');
const app = new koa();
const router = new Router();
app.use(router.routes()).use(router.allowedMethods());
app.listen(8080);
router.get( '/user', (ctx, next) => {
ctx.body = 'hello word';
next();
});
上面是koa-router的基本用法,创建了一个简单的路由。
如果不使用koa-router的话,需要自己来判断路径,如下所示,可以来判断url,然后调用响应的方法
const koa = require('koa');
const app = new koa();
app.listen(8080);
app.use(((context, next) => {
const path = context.path;
switch (path) {
case '/': // do some things;break;
case '/demo': // do some things;break;
}
}));
当然这种方式代码复杂,不易维护,可以使用koa-router来简化代码。
源码学习
在koa-router中,有两个很重要的对象,几个重要的方法
对象:
- Router对象:对外抛出的路由对象,封装了路由的方法和属性,提供给开发者使用
- Layer对象:Layer对象未对外抛出,是koa-router内部的一个单位路由层的对象
这里我对Layer进行了自定义命名:单位路由层,指的是由method,path,middleware等组成的一个路由层,而整个路由是由多个路由层组成的。例如
router.get( '/user', (ctx, next) => {
ctx.body = 'hello word';
next();
});
// 它就是一个路由层,是一种匹配模式,当path,method匹配的时候,调用指定的middleware
方法:
ps:router指的是Router的实例
- methods.forEach:将http的请求方法挂载到Router.prototype上,让router能使用对应的http方法来生成单位路由层
- register:根据path, methods, middleware等生成layer实例,存储在router的stack属性中
- routes:Router的主方法,返回一个koa中间件函数,在此中间件内调用其他函数,完成路由匹配,并完成路由链的调用,也可以使用middleware方法完成该操作
- match:Router的核心方法,用于判断当前请求和路由是否匹配
- compose:koa-compsoe模块的方法,在koa源码学习中遇到过,是koa中间件的核心函数
上面介绍了koa-router的主要对象和方法,下面我们通过创建一个简单的路由和一次HTTP请求,来了解Router内部的函数调用及实现细节
引入
const Router=require('koa-router');
在加载koa-router模块的时候,导出了Router对象,并执行了methods.forEach函数
该函数的主要作用是,从将从http模块取得的方法挂载在Router.prototype上
调用methods.forEach
methods.forEach(function (method) {
// 这里是挂载请求方法,常用的就 get,post,put,delete,options等
Router.prototype[method] = function (name, path, middleware) {
var middleware;
// 这里主要是判断参数的个数,让path和middleware取得正确的值
if (typeof path === 'string' || path instanceof RegExp) {
middleware = Array.prototype.slice.call(arguments, 2);
} else {
middleware = Array.prototype.slice.call(arguments, 1);
path = name;
name = null;
}
// 调用Router实例方法register
this.register(path, [method], middleware, {
name: name
});
return this;
};
});
调用Router.prototype.register方法
实例化layer实例,并将实例存储router.stack中
Router.prototype.register = function (path, methods, middleware, opts) {
opts = opts || {};
var router = this;
var stack = this.stack;
// support array of paths
// 判断路径的类型,如果路径为数组(即多重路径的话),递归调用
if (Array.isArray(path)) {
path.forEach(function (p) {
router.register.call(router, p, methods, middleware, opts);
});
return this;
}
// create route
var route = new Layer(path, methods, middleware, {
end: opts.end === false ? opts.end : true,
name: opts.name,
sensitive: opts.sensitive || this.opts.sensitive || false,
strict: opts.strict || this.opts.strict || false,
prefix: opts.prefix || this.opts.prefix || "",
ignoreCaptures: opts.ignoreCaptures
});
if (this.opts.prefix) {
route.setPrefix(this.opts.prefix);
}
// add parameter middleware
Object.keys(this.params).forEach(function (param) {
route.param(param, this.params[param]);
}, this);
stack.push(route);
return route;
};
调用Router.prototype.routes方法
示例代码
app.use(router.routes());
router.get( '/user', (ctx, next) => {
ctx.body = 'hello word';
next();
});
该方法返回一个中间件函数
该函数主要是调用match函数,取得url路径匹配结果
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); // 在此调用match函数
var layerChain, layer, i;
// 多次调用的时候,将match.path合并为一个数组
if (ctx.matched) {
ctx.matched.push.apply(ctx.matched, matched.path);
} else {
ctx.matched = matched.path;
}
ctx.router = router;
if (!matched.route) return next();
var matchedLayers = matched.pathAndMethod
var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
ctx._matchedRoute = mostSpecificLayer.path;
if (mostSpecificLayer.name) {
ctx._matchedRouteName = mostSpecificLayer.name;
}
// 将所有符合的layer组合起来
layerChain = matchedLayers.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);
ctx.routerName = layer.name;
return next();
});
return memo.concat(layer.stack);
}, []);
return compose(layerChain)(ctx, next); // 通过compose组成中间件链,然后调用
};
调用Router.prototype.match函数
Router.prototype.match = function (path, method) {
var layers = this.stack;
var layer;
var matched = {
path: [],
pathAndMethod: [],
route: false
};
// 将所有的单位路由曾循环,判断是否满足当前url
for (var len = layers.length, i = 0; i < len; i++) {
layer = layers[i];
debug('test %s %s', layer.path, layer.regexp);
// 当正则匹配成功后,则将当前layer加入match.path数组中
if (layer.match(path)) {
matched.path.push(layer);
if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
matched.pathAndMethod.push(layer);
if (layer.methods.length) matched.route = true;
}
}
}
return matched;
};
这里值得注意的是layer.match方法,如上示例代码生成的正则为:
/^\/hello(?:\/(?=$))?$/i
// ?:为非捕获性分组
// ?=为正则前瞻
(?:\/(?=$))?$ 代表的意思是:以/结束,出现0次或1次