談談Koa 中的next
最近在試著把自己寫的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 都充當了一個串聯各個中介軟體的角色,其設計思路和實現無不展現了作者的功底之強,十分值得回味學習