談談Koa 中的next

前言

最近在試着把本身寫的 koa-vuessr-middleware 應用在舊項目中時,由於舊項目Koa 版本爲1.2,對中間件的支持不一致,在轉化以後好奇地讀了一下源碼,整理了一下對Koa 中next 在兩個版本中的意義及相互轉換的理解javascript


正文

1.x 中的next

從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


2.x 中的next

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 都充當了一個串聯各個中間件的角色,其設計思路和實現無不展示了做者的功底之強,十分值得回味學習

相關文章
相關標籤/搜索