版本7.4.0
比較長,建議一邊看一邊打斷點,光看一遍估計是不知道在說什麼的,因爲它的思路不是面向過程的那種一條線,而是互相穿插,繞來繞去的。
koa-router的核心其實是path-to-regexp這個庫,不過koa-router做了非常多的參數處理和封裝,主要提供的功能有:基礎的rest方法的註冊、嵌套路由、路由中間層、參數處理等,我們一個一個來看。
先來看看koa-router的基本使用
var Koa = require('koa');
var Router = require('koa-router');
var app = new Koa();
var router = new Router();
router.get('/a', (ctx, next) => {
console.log(ctx.router)
next();
ctx.body = 'aaa';
}, (ctx, next) => {
console.log('a2')
});
// 命名路由
router.get('b', '/b', (ctx, next) => {
console.log(ctx.router)
ctx.body = 'bbb';
});
app
.use(router.routes());
app.listen(3000);
這裏比較關鍵的步驟如下:
1.實例化了Router
2.調用實例的get方法,定義了兩個路由a和b
3.調用實例的routes方法,將返回註冊爲應用的中間件
我們要說的,除了上述幾個點以外,還有register、use、param函數,嵌套的路由如何實現,路由的前綴形式等
構造和verb的註冊
我們按順序來看,首先是構造函數,構造函數沒幹啥,初始化了opts、methods、params、stack幾個變量,緊跟着通過methods.forEach在prototype上定義了get、post等方法,methods的列表可以自己看一下,在這裏https://github.com/jshttp/methods/blob/master/index.js。這些方法進行了參數處理後直接調用了register函數
function Router(opts) {
if (!(this instanceof Router)) {
return new Router(opts);
}
this.opts = opts || {};
// this.methods只在allowedMethods裏面用到了,如果不提供methods參數,下面這些就是默認實現的方法
this.methods = this.opts.methods || [
'HEAD',
'OPTIONS',
'GET',
'PUT',
'PATCH',
'POST',
'DELETE'
];
// params保存參數處理相關內容,stack保存Layer實例
this.params = {};
this.stack = [];
};
// 這個methods和上面的this.methods要區分一下
methods.forEach(function (method) {
Router.prototype[method] = function (name, path, middleware) {
var middleware;
if (typeof path === 'string' || path instanceof RegExp) {
// 注意這兩個slice將middleware變成了數組,註冊的時候可以傳多個方法
// 可以看看上面第一個例子裏的/a路徑註冊的處理函數
middleware = Array.prototype.slice.call(arguments, 2);
} else {
middleware = Array.prototype.slice.call(arguments, 1);
path = name;
name = null;
}
// 調用了register方法
this.register(path, [method], middleware, {
name: name
});
// 這一句保證了鏈式調用
return this;
};
});
register
在看register方法的代碼前我們來看一看register方法的簡單使用,注意register支持傳入數組形式的path,而第二個參數methods只能是數組形式,
router.register(['/c', '/cc'], ['GET', 'POST'], (ctx, next) => {
console.log(ctx.router)
ctx.body = 'register';
});
register方法代碼如下,比較重要的幾個地方,一是通過path.forEach將傳入的數組形式的路徑,分別單獨調用register,二是通過Layer構造了一個route對象,並且push到stack裏,三是對param的處理
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是數組,每個路徑單獨調用register方法
path.forEach(function (p) {
router.register.call(router, p, methods, middleware, opts);
});
return this;
}
// 調用Layer的構造函數
// 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) {
// 處理實例化的時候傳入的prefix選項,參見下文的嵌套路由部分,setPrefix是Layer的函數
route.setPrefix(this.opts.prefix);
}
// add parameter middleware
// 對param的處理
Object.keys(this.params).forEach(function (param) {
route.param(param, this.params[param]);
}, this);
// 所有註冊的路由全都存在stack裏面,每個path作爲一個元素,一個path可能有多個methods和多個處理函數
stack.push(route);
return route;
};
因爲register裏面有用到,所以我們來看一看Layer的構造函數,注意這裏的this.methods和this.stack和前面不一樣了,是Layer自己定義的
function Layer(path, methods, middleware, opts) {
this.opts = opts || {};
this.name = this.opts.name || null;
this.methods = [];
this.paramNames = [];
this.stack = Array.isArray(middleware) ? middleware : [middleware];
// register的methods參數必須是數組是因爲這裏直接拿來就forEach
methods.forEach(function(method) {
var m = this.methods.push(method.toUpperCase());
// 如果註冊了一個get請求,那麼添加一個對應的head請求的處理
if (this.methods[m-1] === 'GET') {
this.methods.unshift('HEAD');
}
}, this);
// ensure middleware is a function
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;
// 生成路徑匹配用的正則表達式
// paramNames是pathToRegExp來賦值的,如果路徑是/foo/:bar這樣的,paramNames裏面就會有值
// 如果是/foo/bar,paramNames就是空數組
this.regexp = pathToRegExp(path, this.paramNames, this.opts);
debug('defined route %s %s', this.methods, this.opts.prefix + this.path);
};
實際上Layer返回的route是這樣麼一個對象(下面是以本文第一個例子裏的b的那個路由爲例得到的值),其中opts是一些pathToRegExp接受的選項,regex是pathToRegExp的返回值,stack是我們註冊路由的處理函數,建議自己打斷點看一下
{
methods: ['HEAD', 'GET'],
name: null,
opts: Object
paramNames: [],
path: '/b',
regex: Object,
stack: [middleware]
}
到這裏爲止關於register的函數我們看完了,可能貼的代碼有點多不方便看,但是最核心的部分其實只有一個pathToRegExp,另外都是一些參數的處理。
routes
回看一眼本文開頭的例子,所有路由註冊完畢以後,我們會調用routes方法,除去allowedMethods,實際上看完routes方法我們就算完成了一個簡單的流程。
在看routes方法之前,我們可以確定的是,routes方法執行後返回的一定是koa中間件的格式,也就是一個函數,並且有ctx和next兩個參數。
routes方法的代碼如下,核心就是dispatch函數,我們的所有路由都會經過這個函數,裏面調用了Router.prototype.match,用我們stack裏面的全部middleware的regexp去匹配請求路徑,匹配成功的middleware的處理函數先轉成數組,再通過compose轉換成單個函數執行。這裏還是比較繞的,建議自己打斷點看一下每一步的結果。
// middleware和routes是一樣的
Router.prototype.routes = Router.prototype.middleware = function () {
var router = this;
// 最終的處理路由的地方就是這個函數
var dispatch = function dispatch(ctx, next) {
debug('%s %s', ctx.method, ctx.path);
// ctx.path是koa賦值的,ctx.routerPath不知道是哪裏來的
var path = router.opts.routerPath || ctx.routerPath || ctx.path;
// 調用了match方法,根據this.stack裏面註冊的路由生成的正則,一個一個匹配,
// 返回是否匹配成功和匹配到的Layer實例
var matched = router.match(path, ctx.method);
var layerChain, layer, i;
if (ctx.matched) {
ctx.matched.push.apply(ctx.matched, matched.path);
} else {
ctx.matched = matched.path;
}
// 在處理函數裏可以獲取ctx.router
ctx.router = router;
// route標誌不爲true就執行下一個中間件,沒有匹配成功或者是use方法註冊的路由都會導致route標誌爲false
if (!matched.route) return next();
// pathAndMethod是我們註冊的middleware,
// 往ctx裏放兩個值_matchedRoute和_matchedRouteName
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);
// 對param的處理
ctx.params = layer.params(path, ctx.captures, ctx.params);
ctx.routerName = layer.name;
return next();
});
return memo.concat(layer.stack);
}, []);
// 通過compose將數組形式的layers變成一個嵌套的函數並執行,這裏return的是compose後執行的結果
// 是一個Promise.resolve或者Promise.reject
return compose(layerChain)(ctx, next);
};
dispatch.router = this;
return dispatch;
};
這裏還是要貼一下match的代碼,因爲下面講 match的時候發現有挺多細節和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);
if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
matched.pathAndMethod.push(layer);
// 只有當註冊的methods不爲空,纔會將route標誌位設爲true,這個標誌位在上面的routes方法裏有用到,
// 如果只使用use方法來註冊某個路由,這個判斷就不會通過,導致註冊的中間件不會執行
// 但是如果使用verb註冊過某個路由,use也註冊了這個路由或者use註冊了一個不帶路由的中間件,use註冊的中間件就會執行
if (layer.methods.length) matched.route = true;
}
}
}
return matched;
};
這樣基本的流程我們就說完了,基本流程用一個圖表示就是:verb註冊 => register => 調用routes()
接下來我們說些進階的使用和實現
嵌套路由
嵌套路由應該還是比較常用的,我們先來看一個例子
var test = new Router({
// 注意這裏
prefix: '/test'
});
var forums = new Router();
test.get('/test', (ctx, next) => {
console.log('test');
ctx.body = 'test';
});
// 注意這裏
test.prefix('/prefix');
// 注意這裏
forums.use('/forums/:fid?', test.routes(), test.allowedMethods());
app.use(forums.routes());
app.listen(3001);
這個例子裏,要注意三個地方,第一個是test在實例化的時候,傳入了prefix前綴,第二個是test調用了prefix方法,爲自己註冊的router添加了prefix這個前綴,第三個是forums,它調用了use方法,傳入了test定義的router,對/forums/123/prefix/test/test
和/forums/prefix/test/test
這樣的路徑會有響應。
實例化時傳入的參數,是在register函數裏處理的,調用了Layer的setPrefix方法,可以回看上文的register那一節;prefix方法也是調用了Layer的setPrefix方法,其實還是pathToRegExp幫忙處理的,代碼如下
Layer.prototype.setPrefix = function (prefix) {
if (this.path) {
this.path = prefix + this.path;
this.paramNames = [];
this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);
}
return this;
};
然後我們來看看use方法
use
除了在嵌套路由的時候用到,use還可以爲路由添加類似中間層的處理,是比較重要的一個函數,但是也有一些坑,use的用法我所知道的有這麼幾種,
// 直接傳處理函數,只有當前這個router實例中的某個路徑匹配成功,纔會執行處理函數
router.use((ctx, next)=>{
...
})
// 傳路徑加處理函數,路徑可以是字符串或者是數組
router.use(['/users', '/id'], (ctx, next)=>{
...
})
// 傳一個其他router實例的中間件,注意這個例子最終的效果不是分別添加了/users和/id前綴,而是添加了/id/users前綴,還將中間件註冊了兩次
// 原因是use操作的是routes函數返回的結果,也就是dispatch函數,而dispatch和router實例是綁定的
router.use(['/users', '/id'], otherRouter.routes())
// 上面提到的問題也會導致下面這個例子和想象的結果不太一樣,會發現無法響應other路徑,查看middleware會發現/prefix/other路徑的中間件註冊了兩次
otherRouter.get('/other', (ctx, next)=>{
...
})
app.use(otherRouter.routes());
router.use('/prefix', otherRouter.routes());
app.use(router.routes());
use的代碼如下,上面例子裏關於dispatch和router綁定的問題有點難講清楚,大概和複雜類型的變量,賦值給了另一個變量後進行操作,發現原來的變量也變了差不多的意思
Router.prototype.use = function () {
var router = this;
var middleware = Array.prototype.slice.call(arguments);
var path;
// 處理第一個參數是路徑數組的情況,和register一樣如果是數組的話每個元素單獨調用一回
// support array of paths
if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') {
middleware[0].forEach(function (p) {
router.use.apply(router, [p].concat(middleware.slice(1)));
});
return this;
}
// 處理第一個參數是路徑的情況
var hasPath = typeof middleware[0] === 'string';
if (hasPath) {
path = middleware.shift();
}
middleware.forEach(function (m) {
// 如果有router,其實就是已經調用過routes方法,如果沒有,就調用register方法
if (m.router) {
// 處理前綴
m.router.stack.forEach(function (nestedLayer) {
if (path) nestedLayer.setPrefix(path);
if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix);
router.stack.push(nestedLayer);
});
// 對param的處理
if (router.params) {
Object.keys(router.params).forEach(function (key) {
m.router.param(key, router.params[key]);
});
}
} else {
// register的定義function (path, methods, middleware, opts),方便對比,methods傳的是空數組
router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath });
}
});
return this;
};
參數處理
在前面的代碼中我們已經看到過很多次param和params這樣的變量或者函數,這幾個變量和函數是爲了處理路徑裏的變量,比如下面例子裏的user,這種方式提供了可以抽離的參數校驗功能
router
.param('user', (id, ctx, next) => {
ctx.user = users[id];
if (!ctx.user) return ctx.status = 404;
return next();
})
.get('/users/:user', ctx => {
ctx.body = ctx.user;
})
.get('/users/:user/friends', ctx => {
return ctx.user.getFriends().then(function(friends) {
ctx.body = friends;
});
})
param函數往params變量中添加處理函數,register和use函數中都循環調用了Layer的param函數,routes函數中調用了Layer實例的params方法,這一塊說實話還沒怎麼看明白,也比較細節,看到這裏基本上使用過程中發現什麼問題可以找得到是哪裏,所以就不深入了(😂),比較重要的應該只有Layer.prototype.param。
allowedMethods
這篇文章沒想到會寫這麼長,也沒想到koa-router有那麼多細節,感覺80%的代碼都貼上來了,這裏還想講一個函數,最後一個,allowedMethods,先看例子
router
.get('a', '/a', (ctx, next) => {
console.log('a1');
next();
}, (ctx, next) => {
console.log('a2');
next();
});
app.use(router.routes());
app.use(router.allowedMethods());
要注意的是,如果中間件的next鏈斷掉了,上面例子中的某一個next沒有寫,都會造成allowedMethods不會執行,因爲allowedMethods的實現裏有一句next().then()
,另外的部分都還好理解,就不貼了
終於寫完了,下一篇應該會寫body吧,敬請期待