博客
博客
文章目录
  1. koa-router
    1. 如何使用 koa-router
    2. koa-router 的实例化
    3. router 的请求方法
    4. 路由层的构造
    5. routes 中间件
    6. 请求过滤

koa-router 源码解析

koa-router

koa-router 应该是最常使用的 koa 的路由库,其源码比较简单,而且有十分详细的注释与使用案例。使用方式也比 tj 大神的 koa-route 要简洁。

如何使用 koa-router

按照惯例,先看看 koa-router 的使用方法。

var Koa = require('koa');
var Router = require('koa-router');

var app = new Koa ();
var router = new Router ();

router
.get ('/', (ctx, next) => {
ctx.body = 'Hello World!';
})
.post ('/users', (ctx, next) => {
//...
})
.put ('/users/:id', (ctx, next) => {
//...
})
.del ('/users/:id', (ctx, next) => {
//...
})
.all ('/users/:id', (ctx, next) => {
//...
});

app
.use (router.routes ())
.use (router.allowedMethods ());

koa-router 的实例化

首先对 Router 进行实例化,然后在实例化的对象进行路由的注册,最后通过 routesallowedMethods 方法在 koa 上添加中间件。

还有一点需要注意的是 router 对象是支持链式调用,也就是每个方法最后都会 return this;

var methods = require('methods');

// 构造函数
function Router(opts) {
if (!(this instanceof Router)) {
return new Router (opts);
}

this.opts = opts || {};
this.methods = this.opts.methods || [
'HEAD',
'OPTIONS',
'GET',
'PUT',
'PATCH',
'POST',
'DELETE'
];

this.params = {};
this.stack = [];
};

// 原型上注册 http 相关请求的方法
methods.forEach (function (method) {
Router.prototype [method] = function (name, path, middleware) {
var middleware;
// 参数校验,判断是否传入 name,并且将 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;
}
// 注册路由
this.register (path, [method], middleware, {
name: name
});

// 返回 this,方便链式调用
return this;
};
});

// 为 delete 定义别名
Router.prototype.del = Router.prototype ['delete'];

router 的请求方法

这里的 methods 是 node 所支持的 http 请求的方法 (require ('http').METHODS),这里一共有二十多种请求方法。

methods

但是可以看前面构造函数定义的 this.methods 只有 7 种请求方法,这是 HTTP1.1 协议中通用的请求方法(除了没有 CONNECT)。这里定义的七种方法会在 allowedMethods 方法进行过滤,这个后面讲到 allowedMethods 方法的时候再细讲。

[
'HEAD',
'OPTIONS',
'GET',
'PUT',
'PATCH',
'POST',
'DELETE'
]

这些请求方法首先进行了一些参数校验,最后会调用 register 方法进行路由的注册。

// 注册路由 
this.register (path, [method], middleware, {
name: name
});

register = function (path, methods, middleware, opts) {}

这里 register 接受的 methods 参数是一个数组,表示一个路由可以绑定多个请求方法,所以 koa-router 还支持一个 all 方法,该方法会对一个路由注册所有的请求方法,即调用 register 的时候传入 methods。

Router.prototype.all = function (name, path, middleware) {
this.register (path, methods, middleware, {
name: name
});
return this;
};

同时 path 参数也支持数组的方式,如果想要更加灵活的注册路由,可以不调用这些请求方法,而是直接使用 register。

var Koa = require('koa');
var Router = require('koa-router');

var app = new Koa ();
var router = new Router ();

//register 不支持链式调用
router.register (
['/test1', '/test2'],
['get', 'post'],
(ctx, next) => {
ctx.body = 'Hello World!';
});

app.use (router.routes ())

下面直接看看 register 的源码部分:

Router.prototype.register = function (path, methods, middleware, opts) {
opts = opts || {};

var router = this;
var stack = this.stack; // 存储路由表的栈

// 路径支持数组的形式
if (Array.isArray (path)) {
path.forEach (function (p) {
router.register.call (router, p, methods, middleware, opts);
});

return this;
}

// 创建一个路由层,进行 Layer 实例化
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);
}

// 添加参数中间件
Object.keys (this.params).forEach (function (param) {
route.param (param, this.params [param]);
}, this);

stack.push (route);

return route;
};

首先进行 path 参数校验,如果是数组,进行循环调用。

if (Array.isArray (path)) {
path.forEach (function (p) {
router.register.call (router, p, methods, middleware, opts);
});

return this;
}

然后对 Layer 进行实例化,并放入到 stack 栈中,这个 Layer 的实例就是最终的路由层。

var stack = this.stack; // 存储路由表的栈 
var route = new Layer (path, methods, middleware, opts);
stack.push (route);

路由层的构造

下面是 Layer 精简版的构造函数,关于 Layer 实例化的对象,我们只需要关心它的 match 方法,该方法使用了进行当前路径与路由进行匹配的。

var pathToRegExp = require('path-to-regexp');
function Layer(path, methods, middleware, opts) {
this.opts = opts || {};
this.name = this.opts.name || null;
this.methods = [];
this.paramNames = [];
this.stack = Array.isArray (middleware) ? middleware : [middleware];

this.methods = methods.map (function(method) {
return method.toUpperCase (); // 将方法名转成大写
});

this.path = path;
// 根据路由路径生成正则
this.regexp = pathToRegExp (path, this.paramNames, this.opts);
};

Layer.prototype.match = function (path) {
return this.regexp.test (path);
};

这里的 pathToRegExp 方法,主要作用是将一个路由路径转成一个正则表达式,很多路由库都会依赖这方法,具体使用方式如下:

var params = []
var regexp = pathToRegExp ('/user/:id/:name?', params)

得到的结果:

regexp = /^\/user\/([^\/]+?)(?:\/([^\/]+?))?(?:\/)?$/i
params = [
{
name: 'id',
prefix: '/',
delimiter: '/',
optional: false,
repeat: false,
partial: false,
pattern: '[^\\/]+?'
},
{
name: 'name',
prefix: '/',
delimiter: '/',
optional: true,
repeat: false,
partial: false,
pattern: '[^\\/]+?'
}
]

'/user/1001/shenfq'.match (regexp) ===>
[
'/user/1001/shenfq',
'1001',
'shenfq'
]

routes 中间件

到这里,我们的流程已经把所有的路由实例全部存储到了 stack 栈中,接下来看看 routes 方法生成的中间件怎么进行路由匹配的。

Router.prototype.routes = Router.prototype.middleware = function () {
var router = this;

var dispatch = function dispatch(ctx, next) {
//...
};

dispatch.router = this;

return dispatch;
};

既然是生成 koa 的中间,那么 routes 方法必定是返回一个函数。看上面代码,routes 返回了一个 dispatch 方法,该方法属于 koa 中间件的标准写法,接受了两个参数(ctx、next)。具体代码如下:

var compose = require('koa-compose');
var dispatch = function dispatch(ctx, next) {
// 获取当前请求的路径
var path = ctx.routerPath || ctx.path;
// 根据路径匹配对应路由
// { route: false, pathAndMethod: [] }
var matched = router.match (path, ctx.method);
var layerChain, layer, i;

ctx.router = router;

// 如果没有匹配到路由,直接 return
if (!matched.route) return next ();

var matchedLayers = matched.pathAndMethod
// 将所有匹配到的路由的所有回调中间件,集合到一个数组中
layerChain = matchedLayers.reduce (function(memo, layer) {
// 中间件的合并
return memo.concat (layer.stack);
}, []);
// 通过 compose 构造路由层的洋葱模型
return compose (layerChain)(ctx, next);
};

具体执行逻辑,我们可以用一张图来描述一下(精简了部分代码)。

routes

其中比较重要的就是关于路由的匹配部分,会遍历所有之前通过请求方法注册的路由层,然后找到路径和请求方法同时匹配的路由层进行返回。

Router.prototype.match = function (path, method) {
var layers = this.stack;
var layer;
var matched = {
pathAndMethod: [],
route: false // 是否匹配到路由
};
// 遍历路由层
for (var len = layers.length, i = 0; i < len; i++) {
layer = layers [i];

if (layer.match (path)) { // 判断当前路径是否与路由正则匹配
// 判断请求方法是否与注册的请求方法匹配
if (layer.methods.length === 0 || ~layer.methods.indexOf (method)) {
matched.pathAndMethod.push (layer);
if (layer.methods.length) matched.route = true;
}
}
}

return matched;
};

请求过滤

根据 koa-router 的官方文档,我们在注册好路由之后,需要使用 routesallowedMethods 方法添加中间件,前面已经介绍了 routes 主要是根据请求路径进行路由匹配,下面介绍 allowedMethods 方法,该方法主要用于请求的过滤和错误处理。

代码如下:

Router.prototype.allowedMethods = function (options) {
options = options || {};
var implemented = this.methods;

return function allowedMethods(ctx, next) {
return next ().then (function() {
if (!ctx.status || ctx.status === 404) {
// 如果当前请求不属于常规请求方法,返回 501
if (!~implemented.indexOf (ctx.method)) {
ctx.status = 501;
} else {
if (ctx.method === 'OPTIONS') {
//options 请求,返回成功,且内容为空
ctx.status = 200;
ctx.body = '';
} else {
// 如果路径被匹配,但是请求方法为匹配,返回 405
ctx.status = 405;
}
}
}
});
};
};

这里主要是对 options 请求的处理,还有一些请求返回特殊状态码(501、405)。

状态码 405 Method Not Allowed 表明服务器禁止了使用当前 HTTP 方法的请求。需要注意的是,GET 与 HEAD 两个方法不得被禁止,当然也不得返回状态码 405。

HTTP 501 Not Implemented 服务器错误响应码表示请求的方法不被服务器支持,因此无法被处理。

支持一下
扫一扫,支持一下
  • 微信扫一扫
  • 支付宝扫一扫