koa router 实现原理
本文两个目的
- 了解 path-to-regexp 使用
- koa-router 源码解析
path-to-regexp
path-to-regexp 用法简介。
如何使用其来匹配识别路由?
想想如果我们要识别路由的话,我们可以怎么做?
最直观肯定是路径字符串全匹配
'/string' => '/string'
当路由全匹配 /string 的时候我们可以做出一些反馈操作。例如执行一个 callback 等。
我们还可以利用正则匹配特性
这样子匹配模式显然可操作方式更多元,匹配路径也更多
例如对路径 path:
/^\/string\/.*?\/xixi$
// => '/string/try/xixi'
path-to-regexp 就是一种这样的工具
试想一下如果我们要对路径解析匹配,我们需要自己再去写正则表达式。从而达到匹配效果。
可以写吗?
肯定可以,可是太费时了。
path-to-regexp 它可以帮助我们简单地完成这种操作。
简介 path-to-regexp 的一些 api
how to use it ???
主要 api
const pathToRegexp = require('path-to-regexp')
// pathToRegexp(path, keys?, options?)
// pathToRegexp.parse(path)
// pathToRegexp.compile(path)
// pathToRegexp(path, keys?, options?)
// path 可以是 string/ 字符串数组 / 正则表达式
// keys 存放路径中找到的键数组
// options 是一些匹配规则的填充 例如是否为全匹配 分割符等
path-to-regexp api demo
// 一个 demo
如果我们要实现正常的匹配某些键值
eg:
/user/:name
我们实现这样子的正则如何实现
前部是全匹配, 后部用正则分组提取值
eg:
/\/user\/((?!\/).*?)\/?$/.exec('/user/zwkang')
查找匹配正则的字符串 返回一个数组 / 无值返回一个 null
pathToRegexp 就是干的这个活。生成需要的正则表达式匹配。当然里面还有一些封装操作,但是本质就是干的这个。
pathToRegexp('/user/:name').exec('/user/zwkang')
path
option ?
表示可有可无
pathToRegexp('/:foo/:bar?').exec('/test')
pathToRegexp('/:foo/:bar?').exec('/test/route')
* 代表来多少都可以
+ 代表一个或者多个
仔细看你可以发现 这些词跟正则中的量词几乎一致
也可以匹配未命名参数 存储 keys 时会根据序列下标存储
同时也支持正则表达式
parse 方法
对 path 生成匹配的 tokens 数组
也就是上文的 keys 数组
方法适用于 string 类型
Compile 方法
用 compile 传入一个 path 返回一个可以填充的函数 生成与 path 匹配的值
pathToRegexp.compile('/user/:id')({id: 123}) => "/user/123"
适用于字符串
pathToRegexp.tokensToRegExp(tokens, keys?, options?)
pathToRegexp.tokensToFunction(tokens)
名字上可以看出
一个将 tokens 数组转化为正则表达式
一个将 tokens 数组转化为 compile 方法生成的函数
捋一捋使用步骤
pathToRegexp = 返回 => regexp
parse = 解析 => path = 匹配 tokens=> keys token
compile => path => generator function => value => full path string
koa-router
不知道你是否曾使用过 koa-router
notic: 注意现在的 koa-router 的维护权限变更问题
router 实现实际上也是一种基于正则的访问路径匹配。
如果是使用 koa 原生代码
例子:
匹配路径 /simple 返回一个 body 为 {name:’zwkang’}的 body string
一个简单的例子,如
假设我们匹配路由 使用一个简单的中间件匹配 ctx.url
app.use(async (ctx, next) => {
const url = ctx.url
if(/^\/simple$/i.test(url)) {
ctx.body = {name: 'ZWkang'}
} else {
ctx.body = {
errorCode: 404,
message: 'NOT FOUND'
}
ctx.status = 404
}
return await next()})
测试代码
describe('use normal koa path', () => {it('use error path', (done) => {request(http.createServer(app.callback()))
.get('/simple/s')
.expect(404)
.end(function (err, res) {if (err) return done(err);
expect(res.body).to.be.an('object');
expect(res.body).to.have.property('errorCode', 404)
done();});
})
it('use right path', (done) => {request(http.createServer(app.callback()))
.get('/simple')
.expect(200)
.end(function (err, res) {if (err) return done(err);
expect(res.body).to.be.an('object');
expect(res.body).to.have.property('name', 'ZWkang')
done();});
})
})
以上我们自己实现 url 的模式就是这样,单一的匹配,如果多元化匹配,甚至匹配参数,需要考虑正则的书写。
缺点,较为单一,设定方法较为简陋,功能弱小
如果我们使用 koa-router 的话
// 一个简单的用法
it('simple use should work', (done) => {router.get('/simple', (ctx, next) => {
ctx.body = {path: 'simple'}
})
app.use(router.routes()).use(router.allowedMethods());
request(http.createServer(app.callback()))
.get('/simple')
.expect(200)
.end(function (err, res) {if (err) return done(err);
expect(res.body).to.be.an('object');
expect(res.body).to.have.property('path', 'simple');
done();});
})
题外话:app.callback()
上方测试代码的一些点解释
callback 是 koa 的运行机制。方法代表了啥?代表了其 setup 的过程
而我们的常用 listen 方法 实际上也是调用了 http.createServer(app.callback()) 这么一步唯一
让我们来看看这 koa-router 到底做了些什么
前置知识
以上面简单例子我们可以看出,理解 koa 运行机制,内部中间件处理模式。
从 demo 入手进行分析
调用 koa 时候调用的实例方法包括
router.allowedMethods ===> router.routes ===> router.get
考虑因为是 koa,use 调用, 那么我们可以肯定是标准的 koa 中间件模式
返回的函数类似于
async (ctx, next) => {
// 处理路由逻辑
// 处理业务逻辑
}
源码的 开头注释 给我们讲述了基本的一些用法
我们可以简单提炼一下
router.verb()
根据 http 方法指定对应函数
例如 router.get().post().put()
.all 方法支持所有 http 方法
当路由匹配时,ctx._matchedRoute 可以在这里获取路径,如果他是命名路由,这里可以得到路由名 ctx._matchedRouteName
请求匹配的时候不会考虑 querystring(?xxxx)
允许使用具名函数
在开发时候可以快速定位路由
* router.get('user', '/users/:id', (ctx, next) => {
* // ...
* });
*
* router.url('user', 3);
* // => "/users/3"
允许多路由使用
* router.get(
* '/users/:id',
* (ctx, next) => {* return User.findOne(ctx.params.id).then(function(user) {
* ctx.user = user;
* next();
* });
* },
* ctx => {* console.log(ctx.user);
* // => {id: 17, name: "Alex"}
* }
* );
允许嵌套路由
* var forums = new Router();
* var posts = new Router();
*
* posts.get('/', (ctx, next) => {...});
* posts.get('/:pid', (ctx, next) => {...});
* forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());
*
* // responds to "/forums/123/posts" and "/forums/123/posts/123"
* app.use(forums.routes());
允许路由前缀匹配
var router = new Router({prefix: '/users'});
router.get('/', ...); // responds to "/users"
router.get('/:id', ...); // responds to "/users/:id"
捕获命名的参数添加到 ctx.params 中
router.get('/:category/:title', (ctx, next) => {console.log(ctx.params);
// => {category: 'programming', title: 'how-to-node'}
});
代码整体分析
代码设计上有些点挺巧妙
- 职责的分离,上层 Router 做 http 层 method status 之类的处理以及 routers middlewares 相关的处理。低层 Layer.js 则关注在路由 path 的处理上
- middlerware 的设计
不妨先从 layer 文件理解。
layer.js
前面说了,这个文件主要是用来处理对 path-to-regexp 库的操作
文件只有 300 行左右 方法较少,直接截取方法做详细解释。
layer 构造函数
function Layer(path, methods, middleware, opts) {this.opts = opts || {};
this.name = this.opts.name || null; // 命名路由
this.methods = []; // 允许方法
// [{name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
this.paramNames = [];
this.stack = Array.isArray(middleware) ? middleware : [middleware]; // 中间件堆
// 初始化参数
// tips : forEach 第二个参数可以传递 this
// forEach push 数组以后 可以使用数组 [l-1] 进行判断末尾元素
// push 方法返回值是该数组 push 后元素个数
// 外部 method 参数传入内部
methods.forEach(function(method) {var l = this.methods.push(method.toUpperCase());
// 如果是 GET 请求 支持 HEAD 请求
if (this.methods[l-1] === 'GET') {this.methods.unshift('HEAD');
}
}, this);
// ensure middleware is a function
// 保证每一个 middleware 为函数
this.stack.forEach(function(fn) {var type = (typeof fn);
if (type !== 'function') {
throw new Error(methods.toString() + "`" + (this.opts.name || path) +"`: `middleware`"
+ "must be a function, not `" + type + "`"
);
}
}, this);
// 路径
this.path = path;
// 利用 pathToRegExp 生成路径的正则表达式
// 与 params 相关的数组回落入到我们的 this.paramNames 中
// this.regexp 一个生成用来切割的数组
this.regexp = pathToRegExp(path, this.paramNames, this.opts);
debug('defined route %s %s', this.methods, this.opts.prefix + this.path);
};
我们可以关注在输入与输出。
输入:path, methods, middleware, opts
输出:对象 属性包括(opts, name, methods, paramNames, stack, path, regexp)
我们之前说过了 layer 是根据 route path 做处理 判断是否匹配,连接库 path-to-regexp,这一点很重要。
stack 应该与传入的 middleware 一致。stack 是数组形式,以此可见我们的 path 对应的 route 是允许多个 的。
我们接下来关注下
根据 path-to-regexp 结合自身需要的 middleware,koa-router 给我们处理了什么封装
原型链上挂载方法有
params
// 获取路由参数键值对
Layer.prototype.params = function (path, captures, existingParams) {var params = existingParams || {};
for (var len = captures.length, i=0; i<len; i++) {if (this.paramNames[i]) { // 获得捕获组相对应
var c = captures[i]; // 获得参数值
params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c;
// 填充键值对
}
}
// 返回参数键值对对象
return params;
};
在构造函数初始化的时候,我们生成 this.regexp 的时候通过传入 this.paramNames 从而将其根据 path 解析出的 param 填出
输入:路径,捕获组,已存在的参数组
输出:一个参数键值对对象
处理方式很普通。因为 params 与 captures 是位置相对应的。所以直接可以循环即可。
match
// 判断是否匹配
Layer.prototype.match = function (path) {return this.regexp.test(path);
};
首先看的也是输入值与返回值
输入: path
输出: 是否匹配的 boolean
我们可以看这个 this.regexp 是属性值,证明我们是有能力随时改变 this.regexp 从而影响这个函数的返回值
captures
// 返回参数值
Layer.prototype.captures = function (path) {if (this.opts.ignoreCaptures) return []; // 忽略捕获返回空
// match 返回匹配结果的数组
// 从正则可以看出生成的正则是一段全匹配。/**
* eg:
* var test = []
* pathToRegExp('/:id/name/(.*?)', test)
*
* /^\/((?:[^\/]+?))\/name\/((?:.*?))(?:\/(?=$))?$/i
*
* '/xixi/name/ashdjhk'.match(/^\/((?:[^\/]+?))\/name\/((?:.*?))(?:\/(?=$))?$/i)
*
* ["/xixi/name/ashdjhk", "xixi", "ashdjhk"]
*/
return path.match(this.regexp).slice(1); // [value, value .....]
};
输入:path 路径
输出:捕获组数组
返回整个捕获组内容
url
Layer.prototype.url = function(params, options) {
var args = params;
console.log(this);
var url = this.path.replace(/\(\.\*\)/g, "");
var toPath = pathToRegExp.compile(url); //
var replaced;
if (typeof params != "object") {args = Array.prototype.slice.call(arguments);
if (typeof args[args.length - 1] == "object") {options = args[args.length - 1];
args = args.slice(0, args.length - 1);
}
}
var tokens = pathToRegExp.parse(url);
var replace = {};
if (args instanceof Array) {for (var len = tokens.length, i = 0, j = 0; i < len; i++) {if (tokens[i].name) replace[tokens[i].name] = args[j++];
}
} else if (tokens.some(token => token.name)) {replace = params; // replace = params} else {options = params; // options = params}
replaced = toPath(replace); // 默认情况下 replace 是默认传入的键值对 // 匹配过后就是完整的 url
if (options && options.query) {
// 是否存在 query
var replaced = new uri(replaced); //
replaced.search(options.query); // 添加 query 路由查询
return replaced.toString();}
return replaced; // 返回 URL 串
};
layer 实例的 url 方法
实际上一个例如 /name/:id
我们解析后会获得一个 {id: xxx} 的 params 对象
根据 /name/:id 跟 params 对象我们是不是可以反推出实际的 url?
这个 url 方法提供的就是这种能力。
param
Layer.prototype.param = function(param, fn) {
var stack = this.stack;
var params = this.paramNames;
var middleware = function(ctx, next) {return fn.call(this, ctx.params[param], ctx, next);
};
middleware.param = param;
var names = params.map(function(p) {return String(p.name);
});
var x = names.indexOf(param); // 获得 index
if (x > -1) {stack.some(function(fn, i) {
// param handlers are always first, so when we find an fn w/o a param property, stop here
// if the param handler at this part of the stack comes after the one we are adding, stop here
// 两个策略
// 1. param 处理器总是在最前面的,当前 fn.param 不存在。则直接插入 [a,b] mid => [mid, a, b]
// 2. [mid, a, b] mid2 => [mid, mid2, a, b]保证按照 params 的顺序排列
// 保证在正常中间件前
// 保证按照 params 顺序排列
if (!fn.param || names.indexOf(fn.param) > x) {
// 在当前注入中间件
stack.splice(i, 0, middleware);
return true; // 停止 some 迭代。}
});
}
return this;
};
这个方法的作用是在当前的 stack 中添加针对单个 param 的处理器
实际上就是对 layer 的 stack 进行一个操作
setPrefix
Layer.prototype.setPrefix = function(prefix) {
// 调用 setPrefix 相当于将 layer 的一些构造重置
if (this.path) {
this.path = prefix + this.path;
this.paramNames = [];
this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);
}
return this;
};
对当前的 path 加上前缀并且重置当前的一些实例属性
safeDecodeURIComponent
function safeDecodeURIComponent(text) {
try {return decodeURIComponent(text);
} catch (e) {return text;}
}
保证 safeDecodeURIComponent 不会抛出任何错误
Layer 总结。
layer 的 stack 主要是存储实际的 middleware[s]。
主要的功能是针对 pathToRegexp 做设计。
提供能力给上层的 Router 做调用实现的。
Router
Router 主要是对上层 koa 框架的响应(ctx, status 等处理),以及链接下层 layer 实例。
Router 构造函数
function Router(opts) {
// 自动 new
if (!(this instanceof Router)) {return new Router(opts);
}
this.opts = opts || {};
// methods 用于对后面 allowedMethod 做校验的
this.methods = this.opts.methods || [
"HEAD",
"OPTIONS",
"GET",
"PUT",
"PATCH",
"POST",
"DELETE"
]; // 初始化 http 方法
this.params = {}; // 参数键值对
this.stack = []; // 存储路由实例}
methods.forEach(function(method) {
// 给原型上附加所有 http method 方法
Router.prototype[method] = function(name, path, middleware) {
var middleware;
// 兼容参数
// 允许 path 为字符串或者正则表达式
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;
}
// 注册到当前实例上
// 主要是设置一个通用的 install middleware 的方法。(mark. tag: function)
this.register(path, [method], middleware, {name: name});
// 链式调用
return this;
};
});
给 Router 原型注册上
http method 的方法,如:Router.prototype.get = xxx
当我们使用实例的时候可以更方便准确使用
router.get(‘name’, path, cb)
这里的 middleware 显然是可以多个。例如 router.get(name, path, cb)
我们可以留意到,这里的主要是调用了另一个方法
notic:
register 方法。而这个方法的入参,我们可以留意下。与 Layer 实例初始化入参极为相似。
带着疑惑我们可以进入到 register 方法内。
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;
}
var route = new Layer(path, methods, middleware, {
end: opts.end === false ? opts.end : true, // 需要明确声明为 end
name: opts.name, // 路由的名字
sensitive: opts.sensitive || this.opts.sensitive || false, // 大小写区分 正则加 i
strict: opts.strict || this.opts.strict || false, // 非捕获分组 加(?:)
prefix: opts.prefix || this.opts.prefix || "", // 前缀字符
ignoreCaptures: opts.ignoreCaptures || false // 给 layer 使用 忽略捕获
});
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);
// 当前 Router 实例 stack push 单个 layer 实例
stack.push(route);
return route;
};
我们可以看到整个 register 方法,是设计给注册 单一路径 的。
针对多路径在forEach 调用 register 方法。这种写法在 koa-router 实现里并不少见。。
看了 register 方法,我们的疑惑得到了证实,果然入参大多是用来初始化 layer 实例的。
初始化 layer 实例后,我们将它放置到 router 实例下的 stack 中。
根据一些 opts 再进行处理判断。不多大抵是无伤大雅的。
这样一来我们就知道了register 的用法。
- 初始化 layer 实例
- 将其注册到 router 实例中。
我们知道我们调用 router 实例时候。
要使用中间件 我们往往需要完成两步
- use(router.routes())
- use(router.allowedMethods())
我们知道一个极简的中间件调用形式总是
app.use(async (ctx, next) => {await next()
})
我们的不管 koa-body 还是 koa-router
传入 app.use 总是一个
async (ctx, next) => {await next()
}
这样的函数,是符合 koa 中间件需求的。
带着这样的想法
我们可以来到 routes 方法中一探究竟。
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;
// matched 已经是进行过处理了 获得了 layer 对象承载
var matched = router.match(path, ctx.method);
var layerChain, layer, i;
// 考虑多个 router 实例的情况
if (ctx.matched) {
// 因为 matched 总是一个数组
// 这里用 apply 类似于 concat
ctx.matched.push.apply(ctx.matched, matched.path);
} else {
// 匹配的路径
ctx.matched = matched.path;
}
// 当前路由
ctx.router = router;
// 如果存在匹配的路由
if (!matched.route) return next();
// 方法与路径都匹配的 layer
var matchedLayers = matched.pathAndMethod;
// 最后一个 layer
var mostSpecificLayer = matchedLayers[matchedLayers.length - 1];
//
ctx._matchedRoute = mostSpecificLayer.path;
// 如果 layer 存在命名
if (mostSpecificLayer.name) {ctx._matchedRouteName = mostSpecificLayer.name;}
// 匹配的 layer 进行 compose 操作
// update capture params routerName 等
// 例如我们使用了多个路由的话。// => ctx.capture, ctx.params, ctx.routerName => layer Stack[s]
// => ctx.capture, ctx.params, ctx.routerName => next layer Stack[s]
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);
};
dispatch.router = this;
return dispatch;
};
我们知道路由匹配的本质是实际路由与定义路径相匹配。
那么 routes 生成的中间件实际上就是在考虑做这种匹配的处理。
从返回值我们可以看到
=> dispatch 方法。
这个 dispacth 方法实际上就是我们前面说的极简方式。
function dispatch(ctx, next) {}
可以说是相差无几。
我们知道 stack 当前存储的是多个 layer 实例。
而根据路径的匹配,我们可以知道
一个后端路径,简单可以分为 http 方法,与路径定义匹配。
例如:/name/:id
这个时候来了个请求 /name/3
是不是匹配了。(params = {id: 3})
但是请求方法如果是 get 呢? 定义的这个 /name/:id 是个 post 的话。
则此时虽然路径匹配,但是实际并不能完全匹配。
原型方法 match
Router.prototype.match = function(path, method) {
var layers = this.stack;
var layer;
var matched = {path: [],
pathAndMethod: [],
route: false
};
for (var len = layers.length, i = 0; i < len; i++) {layer = layers[i];
debug("test %s %s", layer.path, layer.regexp);
if (layer.match(path)) {
// 如果路径匹配
matched.path.push(layer);
// matched 中压入 layer
if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
// 校验方法
matched.pathAndMethod.push(layer);
// 路径与方法中都压入 layer
if (layer.methods.length) matched.route = true;
// 证明没有支持的方法。route 为 true 后面跳过中间件处理
}
}
}
return matched;
};
看看这个 match 方法吧。
对 stack 中的 layaer 进行判断。
返回的 matched 对象中
path 属性: 仅仅路径匹配即可。
pathAndMethod 属性: 仅仅 http 方法与路径匹配即可。
route 属性:需要 layer 的方法长度不为 0(有定义方法。)
所以 dispatch 中我们首先
ctx.matched = matched.path
得到路径匹配的 layer
实际中间件处理的,是 http 方法且路径匹配的 layer
这种情况下。而实际上,所谓中间件就是一个个数组
它的堆叠方式可能是多维的,也可能是一维的。
如果一个 route 进行了匹配
ctx._matchedRoute 代表了它的路径。
这里 ctx._matchedRoute 是方法且路径匹配数组的 layer 的最后一个。
相信取最后一个大家也知道为什么。多个路径,除开当前处理,在下一个中间件处理时候,总是返回最后一个即可。
最后将符合的 layer 组合起来
例如 如果有多个 layer 的情况下,layer 也有多个 stack 的情况下
// 例如我们使用了多个路由的话。// => ctx.capture, ctx.params, ctx.routerName => layer Stack[?s]
// => ctx.capture, ctx.params, ctx.routerName => next layer Stack[?s]
运行顺序就会如上所示
相当于在将多个 layer 实例的 stack 展平,且在每一个 layer 实例前,添加 ctx 属性进行使用。
最后用 compose 将这个展平的数组一起拿来使用。
其实在这里我们可以留意到,所谓的中间件也不过是一堆数组罢了。
但是这里的在每个 layer 实例前使用 ctx 属性倒是个不错的想法。
对中间件的操作例如 prefix 等。就是不断的对内部的 stack 位置属性的调整。
allowedMethods 方法
Router.prototype.allowedMethods = function(options) {options = options || {};
var implemented = this.methods;
// 返回一个中间件用于 app.use 注册。return function allowedMethods(ctx, next) {return next().then(function() {var allowed = {};
// 判断 ctx.status 或者状态码为 404
console.log(ctx.matched, ctx.method, implemented);
if (!ctx.status || ctx.status === 404) {
// routes 方法生成的 ctx.matched
// 就是筛选出来的 layer 匹配组
ctx.matched.forEach(function(route) {route.methods.forEach(function(method) {allowed[method] = method;
});
});
var allowedArr = Object.keys(allowed);
// 实现了的路由匹配
if (!~implemented.indexOf(ctx.method)) {// 位运算符 ~(-1) === 0 !0 == true
// options 参数 throw 如果为 true 的话则直接扔出错误
// 这样可以给上层中间价做处理
// 默认是抛出一个 HttpError
if (options.throw) {
var notImplementedThrowable;
if (typeof options.notImplemented === "function") {notImplementedThrowable = options.notImplemented(); // set whatever the user returns from their function
} else {notImplementedThrowable = new HttpError.NotImplemented();
}
throw notImplementedThrowable;
} else {
// 否则跑出 501
// 501=> 服务器未实现方法
ctx.status = 501;
ctx.set("Allow", allowedArr.join(","));
}
// 如果允许的话
} else if (allowedArr.length) {
// 对 options 请求进行操作。// options 请求与 get 请求类似,但是请求没有请求体 只有头。// 常用语查询操作
if (ctx.method === "OPTIONS") {
ctx.status = 200;
ctx.body = "";
ctx.set("Allow", allowedArr.join(","));
} else if (!allowed[ctx.method]) {
// 如果允许方法
if (options.throw) {
var notAllowedThrowable;
if (typeof options.methodNotAllowed === "function") {notAllowedThrowable = options.methodNotAllowed(); // set whatever the user returns from their function
} else {notAllowedThrowable = new HttpError.MethodNotAllowed();
}
throw notAllowedThrowable;
} else {
// 405 方法不被允许
ctx.status = 405;
ctx.set("Allow", allowedArr.join(","));
}
}
}
}
});
};
};
这个方法主要是默认的给我们路由中间件添加 404 405 501 的这些状态控制。
我们也可以在高层中间件统一处理也可以。
使用位运算符 +indexOf 也是一种常见的用法。
全文总结
至此整篇的 koa-router 源码基本就解析完毕了。
虽然 Router 的源码还有很多方法本文没有写出,但是大多都是给上层提供 layer 实例的方法连接,欢迎到 github 链接从源码处查看。
总的来说能吸收的点可能是挺多的。
如果看完了整篇。
- 相信你对 koa middleware 使用应该是更得心应手了。
- 相信你对 koa-router 的源码架构具体方法实现应该是有所了解。
- 学习如何阅读源码,构建测试用例,了解入参与输出。
我的博客 zwkang.com
源码地址(注释解析版) koa-router 分支