前言
最近在試著把自己寫的 koa-vuessr-middleware 應用在舊專案中時,因為舊專案Koa 版本為1.2,對中介軟體的支援不一致,在轉化之後好奇地讀了一下原始碼,整理了一下對Koa 中next 在兩個版本中的意義及相互轉換的理解
正文
1.x 中的next
從Koa 的 application.js 中找到中介軟體部分的程式碼,可以看出,use 傳入的中介軟體被放入一個middleware 快取佇列中,這個佇列會經由 koa-compose
進行串聯
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 可以看下面的)
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
函式生成的一個generator
但是如果在generator 函式內部去呼叫另一個generator函式,預設情況下是沒有效果的,compose 用了一個yield *
表示式,關於yield *
,可以看看 阮一峰老師的講解;
2.x 中的next
Koa 到了2.x,程式碼越發精簡了,基本的思想還是一樣的,依然是快取中介軟體並使用compose 進行串聯,只是中介軟體引數從一個next
變成了(ctx, next)
,且中介軟體再不是generator函式而是一個 async/await 函式了
use(fn) {
// ...
this.middleware.push(fn);
return this;
}
// ...
callback() {
const fn = compose(this.middleware);
// ..
}
複製程式碼
同時, compose 的實現也變了,相較於1.x 顯得複雜了一些,用了四層return,將關注點放在dispatch
函式上:
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');
}());
複製程式碼
執行順序可以理解為以下的樣子:
// 執行第一個中介軟體 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 的中介軟體在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 都充當了一個串聯各個中介軟體的角色,其設計思路和實現無不展現了作者的功底之強,十分值得回味學習