koa-router源碼學習
基本用法
const koa = require('koa');
const Router = require('koa-router');
const app = new koa();
const router = new Router();
app.use(router.routes()).use(router.allowedMethods());
app.listen(8080);
router.get( '/user', (ctx, next) => {
ctx.body = 'hello word';
next();
});
上面是koa-router的基本用法,創建了一個簡單的路由。
如果不使用koa-router的話,需要自己來判斷路徑,如下所示,可以來判斷url,然後調用響應的方法
const koa = require('koa');
const app = new koa();
app.listen(8080);
app.use(((context, next) => {
const path = context.path;
switch (path) {
case '/': // do some things;break;
case '/demo': // do some things;break;
}
}));
當然這種方式代碼複雜,不易維護,可以使用koa-router來簡化代碼。
源碼學習
在koa-router中,有兩個很重要的對象,幾個重要的方法
對象:
- Router對象:對外拋出的路由對象,封裝了路由的方法和屬性,提供給開發者使用
- Layer對象:Layer對象未對外拋出,是koa-router內部的一個單位路由層的對象
這裏我對Layer進行了自定義命名:單位路由層,指的是由method,path,middleware等組成的一個路由層,而整個路由是由多個路由層組成的。例如
router.get( '/user', (ctx, next) => {
ctx.body = 'hello word';
next();
});
// 它就是一個路由層,是一種匹配模式,當path,method匹配的時候,調用指定的middleware
方法:
ps:router指的是Router的實例
- methods.forEach:將http的請求方法掛載到Router.prototype上,讓router能使用對應的http方法來生成單位路由層
- register:根據path, methods, middleware等生成layer實例,存儲在router的stack屬性中
- routes:Router的主方法,返回一個koa中間件函數,在此中間件內調用其他函數,完成路由匹配,並完成路由鏈的調用,也可以使用middleware方法完成該操作
- match:Router的核心方法,用於判斷當前請求和路由是否匹配
- compose:koa-compsoe模塊的方法,在koa源碼學習中遇到過,是koa中間件的核心函數
上面介紹了koa-router的主要對象和方法,下面我們通過創建一個簡單的路由和一次HTTP請求,來了解Router內部的函數調用及實現細節
引入
const Router=require('koa-router');
在加載koa-router模塊的時候,導出了Router對象,並執行了methods.forEach函數
該函數的主要作用是,從將從http模塊取得的方法掛載在Router.prototype上
調用methods.forEach
methods.forEach(function (method) {
// 這裏是掛載請求方法,常用的就 get,post,put,delete,options等
Router.prototype[method] = function (name, path, middleware) {
var middleware;
// 這裏主要是判斷參數的個數,讓path和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;
}
// 調用Router實例方法register
this.register(path, [method], middleware, {
name: name
});
return this;
};
});
調用Router.prototype.register方法
實例化layer實例,並將實例存儲router.stack中
Router.prototype.register = function (path, methods, middleware, opts) {
opts = opts || {};
var router = this;
var stack = this.stack;
// support array of paths
// 判斷路徑的類型,如果路徑爲數組(即多重路徑的話),遞歸調用
if (Array.isArray(path)) {
path.forEach(function (p) {
router.register.call(router, p, methods, middleware, opts);
});
return this;
}
// create route
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);
}
// add parameter middleware
Object.keys(this.params).forEach(function (param) {
route.param(param, this.params[param]);
}, this);
stack.push(route);
return route;
};
調用Router.prototype.routes方法
示例代碼
app.use(router.routes());
router.get( '/user', (ctx, next) => {
ctx.body = 'hello word';
next();
});
該方法返回一箇中間件函數
該函數主要是調用match函數,取得url路徑匹配結果
var dispatch = 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); // 在此調用match函數
var layerChain, layer, i;
// 多次調用的時候,將match.path合併爲一個數組
if (ctx.matched) {
ctx.matched.push.apply(ctx.matched, matched.path);
} else {
ctx.matched = matched.path;
}
ctx.router = router;
if (!matched.route) return next();
var matchedLayers = matched.pathAndMethod
var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
ctx._matchedRoute = mostSpecificLayer.path;
if (mostSpecificLayer.name) {
ctx._matchedRouteName = mostSpecificLayer.name;
}
// 將所有符合的layer組合起來
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); // 通過compose組成中間件鏈,然後調用
};
調用Router.prototype.match函數
Router.prototype.match = function (path, method) {
var layers = this.stack;
var layer;
var matched = {
path: [],
pathAndMethod: [],
route: false
};
// 將所有的單位路由曾循環,判斷是否滿足當前url
for (var len = layers.length, i = 0; i < len; i++) {
layer = layers[i];
debug('test %s %s', layer.path, layer.regexp);
// 當正則匹配成功後,則將當前layer加入match.path數組中
if (layer.match(path)) {
matched.path.push(layer);
if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
matched.pathAndMethod.push(layer);
if (layer.methods.length) matched.route = true;
}
}
}
return matched;
};
這裏值得注意的是layer.match方法,如上示例代碼生成的正則爲:
/^\/hello(?:\/(?=$))?$/i
// ?:爲非捕獲性分組
// ?=爲正則前瞻
(?:\/(?=$))?$ 代表的意思是:以/結束,出現0次或1次