Koa 應用程序是一個包含一組中間件函數的對象,它是按照相似堆棧的方式組織和執行的。git
這是 koa 對本身的介紹,其餘 koa 依賴的庫其實均可以算是中間件,koa-router 也不例外。github
ps: 本文代碼中的中文解釋是對代碼的講解,省略號(...)表明省略部分代碼 文章最後有簡版router的項目地址正則表達式
經過 koa 最簡單的 hellow world 例子能夠看出原生對請求的處理方式:api
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
複製代碼
要是咱們想簡單的實現路由的話,能夠添加一些判斷條件數組
app.use(async ctx => {
if (ctx.path === '/one' && ctx.method === 'get') {
ctx.body = 'Hello World';
} else {
ctx.status = 404;
ctx.body = '';
}
});
複製代碼
這樣的話能實現簡單對路由的實現,不過路由越多的話消耗的性能也就越大,並且不容易對特殊路由添加中間件。而更好的方法是使用面向對象的方式,根據請求的 path 和 method 返回相應的中間件處理函數和執行函數。bash
這裏要介紹下我解讀 koa-router 源碼的方法,我會先把 koa-router 的源碼下載到本地,而後通讀一遍(由於源碼算是比較少的),從大致上知道 koa-router 執行流程,而後經過單元測試去 debug 分析。restful
我認爲 koa-router 最基本且核心的API有四個:app
咱們能夠結合代碼和單元測試對源碼進行理解,由最簡單的測試開始debug:koa
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() 返回:async
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 (實例化 Layer 對象)
var matchedLayers = matched.pathAndMethod
...
// 若匹配了多個 route,則將多個執行函數 push 進一個數組
layerChain = matchedLayers.reduce(function(memo, layer) {
...
return memo.concat(layer.stack);
}, []);
return compose(layerChain)(ctx, next);
}
複製代碼
router.routes() 返回一個 dispatch 函數,從中能夠看出請求進來會通過 router.match(後面有分析),而後將匹配到的 route 的執行函數 push 進數組,並經過 compose(koa-compose) 函數合併返回。
而後在打印出 compose(layerChain) 方法,能夠看到其實最後請求執行的函數是對ctx.body = {url: ctx.router.url('home')};
的 compose 封裝函數,在效果上至關於
app.use(ctx => {
ctx.body = {
url: ctx.router.url('home')
};
});
複製代碼
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 = [];
};
複製代碼
// 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) {
// 若第二個參數是 string 或 正則表達式,則將後面的參數歸爲 middleware
middleware = Array.prototype.slice.call(arguments, 2);
} else {
// 不然說明沒有傳 name 參數,將第一個參數置爲path,以後的參數歸爲 middleware
middleware = Array.prototype.slice.call(arguments, 1);
path = name;
name = null;
}
// 註冊 route(下面會講到 register 方法)
this.register(path, [method], middleware, {
name: name
});
// 返回 Router 對象,能夠鏈式調用
return this;
};
});
複製代碼
Router.prototype.register = function (path, methods, middleware, opts) {
opts = opts || {};
var stack = this.stack;
...
// create route
// 實例化一個 Layer 對象,Layer 對象將 path 轉爲 regexp,並增長了匹配 path 的可選 ops 參數
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
*/
...
// 將註冊的 route 存放在 stack 隊列中
stack.push(route);
return route;
};
複製代碼
register 方法主要用於實例化 Layer 對象,並支持多各 path 同時註冊、添加路由前綴等功能(展現代碼忽略)。
Router.prototype.match = function (path, method) {
// 獲取已經註冊的 routes (實例化Layer對象)
var layers = this.stack;
var layer;
var matched = {
path: [],
pathAndMethod: [],
route: false
};
// 循環查找可以匹配的route
for (var len = layers.length, i = 0; i < len; i++) {
layer = layers[i];
debug('test %s %s', layer.path, layer.regexp);
// 根據layer.regexp.test(path) 匹配
if (layer.match(path)) {
matched.path.push(layer);
// todo ~操做符暫時沒懂
if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
matched.pathAndMethod.push(layer);
// 將匹配標誌 route 設爲 true,這裏我以爲改成 hitRoute 更容易理解
if (layer.methods.length) matched.route = true;
}
}
}
return matched;
};
複製代碼
經過上面的分析,其實已經講解了 koa-router 核心的部分:構造 Router 對象 => 定義 router 入口 => 匹配路由 => 合併中間件和執行函數輸出;這4個API能夠處理簡單的 restful 請求,額外的API例如重定向、router.use、路由前綴等在瞭解核心代碼後閱讀起來就簡單不少了;簡版其實就是上面api的精簡版,原理一致,能夠到個人項目看下
simple-koa-router:github.com/masongzhi/s…
koa-router 幫咱們定義並選擇相應的路由,對路由添加中間件和一些兼容和驗證的工做;在 koa 中間件應用的基礎上,比較容易理解中間件的實現,koa-router 爲咱們作了更好的路由層管理,在設計上能夠參考實現,同時研究優美源碼也是對本身的一種提高。