Koa 應(yīng)用程序是一個(gè)包含一組中間件函數(shù)的對(duì)象敬鬓,它是按照類(lèi)似堆棧的方式組織和執(zhí)行的。
這是 koa 對(duì)自己的介紹笙各,其他 koa 依賴(lài)的庫(kù)其實(shí)都可以算是中間件钉答,koa-router 也不例外。
ps: 本文代碼中的中文解釋是對(duì)代碼的講解杈抢,省略號(hào)(...)代表省略部分代碼
文章最后有簡(jiǎn)版router的項(xiàng)目地址
對(duì) koa-router 的猜想
通過(guò) koa 最簡(jiǎn)單的 hellow world 例子可以看出原生對(duì)請(qǐng)求的處理方式:
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
要是我們想簡(jiǎn)單的實(shí)現(xiàn)路由的話数尿,可以添加一些判斷條件
app.use(async ctx => {
if (ctx.path === '/one' && ctx.method === 'get') {
ctx.body = 'Hello World';
} else {
ctx.status = 404;
ctx.body = '';
}
});
這樣的話能實(shí)現(xiàn)簡(jiǎn)單對(duì)路由的實(shí)現(xiàn),不過(guò)路由越多的話消耗的性能也就越大惶楼,而且不容易對(duì)特殊路由添加中間件右蹦。而更好的方法是使用面向?qū)ο蟮姆绞剑鶕?jù)請(qǐng)求的 path 和 method 返回相應(yīng)的中間件處理函數(shù)和執(zhí)行函數(shù)歼捐。
解讀思路
這里要介紹下我解讀 koa-router 源碼的方法何陆,我會(huì)先把 koa-router 的源碼下載到本地,然后通讀一遍(因?yàn)樵创a算是比較少的)豹储,從大體上知道 koa-router 執(zhí)行流程贷盲,然后通過(guò)單元測(cè)試去 debug 分析。
Router 執(zhí)行流程圖
我認(rèn)為 koa-router 最基本且核心的API有四個(gè):
- router.match
可以根據(jù)請(qǐng)求的 path 和 method 篩選出匹配的 route - router.register
注冊(cè) route - router.routes
返回用于 koa 加載的中間件剥扣,通過(guò) koa-compose 將middlewares 壓縮成一個(gè)函數(shù) - router.method(get巩剖、post等)
可以根據(jù)path、method 定義 router钠怯,并且可以將middleware綁定在路由上
解讀
我們可以結(jié)合代碼和單元測(cè)試對(duì)源碼進(jìn)行理解佳魔,由最簡(jiǎn)單的測(cè)試開(kāi)始debug:
it('router can be accecced with ctx', function (done) {
var app = new Koa();
var router = new Router();
router.get('home', '/', function (ctx) {
ctx.body = {
url: ctx.router.url('home')
};
});
console.log(router.routes()); // 這是我加的,查看最后加載的routes
app.use(router.routes());
request(http.createServer(app.callback()))
.get('/')
.expect(200)
.end(function (err, res) {
if (err) return done(err);
expect(res.body.url).to.eql("/");
done();
});
});
router.routes() 返回:
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;
...
ctx.router = router;
if (!matched.route) return next();
// 獲取已匹配的 routes (實(shí)例化 Layer 對(duì)象)
var matchedLayers = matched.pathAndMethod
...
// 若匹配了多個(gè) route呻疹,則將多個(gè)執(zhí)行函數(shù) push 進(jìn)一個(gè)數(shù)組
layerChain = matchedLayers.reduce(function(memo, layer) {
...
return memo.concat(layer.stack);
}, []);
return compose(layerChain)(ctx, next);
}
router.routes() 返回一個(gè) dispatch 函數(shù)吃引,從中可以看出請(qǐng)求進(jìn)來(lái)會(huì)經(jīng)過(guò) router.match(后面有分析)筹陵,然后將匹配到的 route 的執(zhí)行函數(shù) push 進(jìn)數(shù)組,并通過(guò) compose(koa-compose) 函數(shù)合并返回镊尺。
然后在打印出 compose(layerChain) 方法朦佩,可以看到其實(shí)最后請(qǐng)求執(zhí)行的函數(shù)是對(duì)ctx.body = {url: ctx.router.url('home')};
的 compose 封裝函數(shù),在效果上相當(dāng)于
app.use(ctx => {
ctx.body = {
url: ctx.router.url('home')
};
});
- Router 構(gòu)造函數(shù)
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 = {};
// 初始化定義 route 棧
this.stack = [];
};
- 分析 router.method 方法
// methods ['get', 'post', 'delete', 'put', 'patch', ...]
methods.forEach(function (method) {
Router.prototype[method] = function (name, path, middleware) {
var middleware;
if (typeof path === 'string' || path instanceof RegExp) {
// 若第二個(gè)參數(shù)是 string 或 正則表達(dá)式庐氮,則將后面的參數(shù)歸為 middleware
middleware = Array.prototype.slice.call(arguments, 2);
} else {
// 否則說(shuō)明沒(méi)有傳 name 參數(shù)语稠,將第一個(gè)參數(shù)置為path,之后的參數(shù)歸為 middleware
middleware = Array.prototype.slice.call(arguments, 1);
path = name;
name = null;
}
// 注冊(cè) route(下面會(huì)講到 register 方法)
this.register(path, [method], middleware, {
name: name
});
// 返回 Router 對(duì)象弄砍,可以鏈?zhǔn)秸{(diào)用
return this;
};
});
- 分析 router.register 方法
Router.prototype.register = function (path, methods, middleware, opts) {
opts = opts || {};
var stack = this.stack;
...
// create route
// 實(shí)例化一個(gè) Layer 對(duì)象仙畦,Layer 對(duì)象將 path 轉(zhuǎn)為 regexp,并增加了匹配 path 的可選 ops 參數(shù)
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
});
console.log(route);
/**
* Layer {
* ...省略部分屬性
* methods: [ 'HEAD', 'GET' ],
* stack: [ [Function] ],
* path: '/',
* regexp: { /^(?:\/(?=$))?$/i keys: [] } } // 用于匹配 path
*/
...
// 將注冊(cè)的 route 存放在 stack 隊(duì)列中
stack.push(route);
return route;
};
register 方法主要用于實(shí)例化 Layer 對(duì)象音婶,并支持多各 path 同時(shí)注冊(cè)慨畸、添加路由前綴等功能(展示代碼忽略)。
- 分析 router.match
Router.prototype.match = function (path, method) {
// 獲取已經(jīng)注冊(cè)的 routes (實(shí)例化Layer對(duì)象)
var layers = this.stack;
var layer;
var matched = {
path: [],
pathAndMethod: [],
route: false
};
// 循環(huán)查找能夠匹配的route
for (var len = layers.length, i = 0; i < len; i++) {
layer = layers[i];
debug('test %s %s', layer.path, layer.regexp);
// 根據(jù)layer.regexp.test(path) 匹配
if (layer.match(path)) {
matched.path.push(layer);
// todo ~操作符暫時(shí)沒(méi)懂
if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
matched.pathAndMethod.push(layer);
// 將匹配標(biāo)志 route 設(shè)為 true衣式,這里我覺(jué)得改為 hitRoute 更容易理解
if (layer.methods.length) matched.route = true;
}
}
}
return matched;
};
實(shí)現(xiàn)簡(jiǎn)版Router
通過(guò)上面的分析寸士,其實(shí)已經(jīng)講解了 koa-router 核心的部分:構(gòu)造 Router 對(duì)象 => 定義 router 入口 => 匹配路由 => 合并中間件和執(zhí)行函數(shù)輸出;這4個(gè)API可以處理簡(jiǎn)單的 restful 請(qǐng)求碴卧,額外的API例如重定向弱卡、router.use、路由前綴等在了解核心代碼后閱讀起來(lái)就簡(jiǎn)單很多了住册;簡(jiǎn)版其實(shí)就是上面api的精簡(jiǎn)版婶博,原理一致,可以到我的項(xiàng)目看下
simple-koa-router:https://github.com/masongzhi/simple-koa-router
總結(jié)
koa-router 幫我們定義并選擇相應(yīng)的路由荧飞,對(duì)路由添加中間件和一些兼容和驗(yàn)證的工作凡人;在 koa 中間件應(yīng)用的基礎(chǔ)上,比較容易理解中間件的實(shí)現(xiàn)垢箕,koa-router 為我們做了更好的路由層管理划栓,在設(shè)計(jì)上可以參考實(shí)現(xiàn)兑巾,同時(shí)研究?jī)?yōu)美源碼也是對(duì)自己的一種提升条获。