最近在試着把本身寫的 koa-vuessr-middleware 應用在舊項目中時,由於舊項目Koa 版本爲1.2,對中間件的支持不一致,在轉化以後好奇地讀了一下源碼,整理了一下對Koa 中next 在兩個版本中的意義及相互轉換的理解javascript
從Koa 的 application.js 中找到中間件部分的代碼,能夠看出,use 傳入的中間件被放入一個middleware 緩存隊列中,這個隊列會經由 koa-compose
進行串聯html
app.use = function(fn){
// ...
this.middleware.push(fn);
return this;
};
// ...
app.callback = function(){
// ...
var fn = this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));
// ...
};
複製代碼
而進入到koa-compose
中,能夠看到compose 的實現頗有意思(不管是在1.x 仍是在2.x 中,2.x 能夠看下面的)vue
function compose(middleware){
return function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
return yield *next;
}
}
// 返回一個generator 函數
function *noop(){}
複製代碼
從代碼中能夠看出來,其實next
自己就是一個generator, 而後在遞減的過程當中,實現了中間件的先進後出。換句話說,就是中間件會從最後一個開始,一直往前執行,然後一箇中間件獲得generator
對象(即next
)會做爲參數傳給前一箇中間件,而最後一箇中間件的參數next 是由noop
函數生成的一個generatorjava
可是若是在generator 函數內部去調用另外一個generator函數,默認狀況下是沒有效果的,compose 用了一個yield *
表達式,關於yield *
,能夠看看 阮一峯老師的講解;git
Koa 到了2.x,代碼愈加精簡了,基本的思想仍是同樣的,依然是緩存中間件並使用compose 進行串聯,只是中間件參數從一個next
變成了(ctx, next)
,且中間件再不是generator函數而是一個 async/await 函數了es6
use(fn) {
// ...
this.middleware.push(fn);
return this;
}
// ...
callback() {
const fn = compose(this.middleware);
// ..
}
複製代碼
同時, compose 的實現也變了,相較於1.x 顯得複雜了一些,用了四層return,將關注點放在dispatch
函數上:github
function compose (middleware) {
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
複製代碼
神來之筆在於Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
這一句,乍看一下有點難懂,實際上fn(context, dispatch.bind(null, i + 1))
就至關於一箇中間件,而後遞歸調用下一個中間件,咱們從dispatch(0)
開始將它展開:緩存
// 執行第一個中間件 p1-1
Promise.resolve(function(context, next){
console.log('executing first mw');
// 執行第二個中間件 p2-1
await Promise.resolve(function(context, next){
console.log('executing second mw');
// 執行第三個中間件 p3-1
await Promise(function(context, next){
console.log('executing third mw');
await next()
// 回過來執行 p3-2
console.log('executing third mw2');
}());
// 回過來執行 p2-2
console.log('executing second mw2');
})
// 回過來執行 p1-2
console.log('executing first mw2');
}());
複製代碼
執行順序能夠理解爲如下的樣子:app
// 執行第一個中間件 p1-1
first = (ctx, next) => {
console.log('executing first mw');
next();
// next() 即執行了第二個中間件 p2-1
second = (ctx, next) => {
console.log('executing second mw');
next();
// next() 即執行了第三個中間件 p3-1
third = (ctx, next) => {
console.log('executing third mw');
next(); // 沒有下一個中間件了, 開始執行剩餘代碼
// 回過來執行 p3-2
console.log('executing third mw2');
}
// 回過來執行 p2-2
console.log('executing second mw2');
}
// 回過來執行 p1-2
console.log('executing first mw2');
}
複製代碼
從上面咱們也能看出來,若是咱們在中間件中沒有執行 await next()
的話,就沒法進入下一個中間件,致使運行停住。在2.x 中,next
再也不是generator,而是以包裹在Promise.resolve
中的普通函數等待await 執行。koa
Koa 的中間件在1.x 和2.x 中是不徹底兼容的,須要使用koa-convert
進行兼容,它不但提供了從1.x 的generator轉換到2.x 的Promise 的能力,還提供了從2.x 回退到1.x 的兼容方法,來看下核心源碼:
function convert (mw) {
// ...
const converted = function (ctx, next) {
return co.call(ctx, mw.call(ctx, createGenerator(next)))
}
// ...
}
function * createGenerator (next) {
return yield next()
}
複製代碼
以上是從1.x 轉化爲2.x 的過程,先將next 轉化爲generator,而後使用mw.call(ctx, createGenerator(next))
返回一個遍歷器(此處傳入的是* (next) => ()
所以mw 爲generator 函數),最後使用co.call
去執行generator 函數返回一個Promise
,關於co
的解讀能夠參考Koa 生成器函數探尋;
接下來咱們來看看回退到1.x 版本的方法
convert.back = function (mw) {
// ...
const converted = function * (next) {
let ctx = this
yield Promise.resolve(mw(ctx, function () {
// ..
return co.call(ctx, next)
}))
}
// ...
}
複製代碼
在這裏,因爲2.x 的上下文對象ctx 等同於1.x 中的上下文對象,即this,在返回的generator 中將this 做爲上下文對象傳入2.x 版本中間件的ctx 參數中,並將中間件Promise化並使用yield 返回
總的來講,在 1.x 和2.x 中,next 都充當了一個串聯各個中間件的角色,其設計思路和實現無不展示了做者的功底之強,十分值得回味學習