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
在构造函数初始化的时候,我们生成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分支