深刻理解 Koa2 中間件機制

咱們知道,Koa 中間件是以級聯代碼(Cascading) 的方式來執行的。相似於回形針的方式,可參照下面這張圖:node

今天這篇文章就來分析 Koa 的中間件是如何實現級聯執行的。 在 koa 中,要應用一箇中間件,咱們使用 app.use():git

app
  .use(logger())
  .use(bodyParser())
  .use(helmet())
複製代碼

先來看看use() 是什麼,它的源碼以下:github

use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }
複製代碼

這個函數的做用在於將調用 use(fn) 方法中的參數(無論是普通的函數或者是中間件)都添加到 this.middlware 這個數組中。api

Koa2 中,還對 Generator 語法的中間件作了兼容,使用 isGeneratorFunction(fn) 這個方法來判斷是否爲 Generator 語法,並經過 convert(fn) 這個方法進行了轉換,轉換成 async/await 語法。而後把全部的中間件都添加到了 this.middleware ,最後經過 callback() 這個方法執行。callback() 源碼以下:數組

/** * Return a request handler callback * for node's native http server. * * @return {Function} * @api public */

  callback() {
    const fn = compose(this.middleware);

    if (!this.listeners('error').length) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }
複製代碼

源碼中,經過 compose() 這個方法,就能將咱們傳入的中間件數組轉換並級聯執行,最後 callback() 返回this.handleRequest()的執行結果。返回的是什麼內容咱們暫且不關心,咱們先來看看 compose() 這個方法作了什麼事情,能使得傳入的中間件可以級聯執行,並返回 Promise閉包

compose() 是 koa2 實現中間件級聯調用的一個庫,叫作 koa-compose。源碼很簡單,只有一個函數,以下:app

/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /** * @param {Object} context * @return {Promise} * @api public */

  return function (context, next) {
    // 記錄上一次執行中間件的位置 #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      // 理論上 i 會大於 index,由於每次執行一次都會把 i遞增,
      // 若是相等或者小於,則說明next()執行了屢次
      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, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
複製代碼

能夠看到 compose() 返回一個匿名函數的結果,該匿名函數自執行了 dispatch() 這個函數,並傳入了0做爲參數。koa

來看看 dispatch(i) 這個函數都作了什麼事? i 做爲該函數的參數,用於獲取到當前下標的中間件。在上面的 dispatch(0) 傳入了0,用於獲取 middleware[0] 中間件。異步

首先顯示判斷 i<==index,若是 true 的話,則說明 next() 方法調用屢次。爲何能夠這麼判斷呢?等咱們解釋了全部的邏輯後再來回答這個問題。async

接下來將當前的 i 賦值給 index,記錄當前執行中間件的下標,並對 fn 進行賦值,得到中間件。

index = i;
let fn = middleware[i]
複製代碼

得到中間件後,怎麼使用?

try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
複製代碼

上面的代碼執行了中間件 fn(context, next),並傳遞了 contextnext 函數兩個參數。context 就是 koa 中的上下文對象 context。至於 next 函數則是返回一個 dispatch(i+1) 的執行結果。值得一提的是 i+1 這個參數,傳遞這個參數就至關於執行了下一個中間件,從而造成遞歸調用。 這也就是爲何咱們在本身寫中間件的時候,須要手動執行

await next()
複製代碼

只有執行了 next 函數,才能正確得執行下一個中間件。

所以每一箇中間件只能執行一次 next,若是在一箇中間件內屢次執行 next,就會出現問題。回到前面說的那個問題,爲何說經過 i<=index 就能夠判斷 next 執行屢次?

由於正常狀況下 index 一定會小於等於 i。若是在一箇中間件中調用屢次 next,會致使屢次執行 dispatch(i+1)。從代碼上來看,每一箇中間件都有屬於本身的一個閉包做用域,同一個中間件的 i 是不變的,而 index 是在閉包做用域外面的。

當第一個中間件即 dispatch(0)next() 調用時,此時應該是執行 dispatch(1),在執行到下面這個判斷的時候,

if (i <= index) return Promise.reject(new Error('next() called multiple times'))
複製代碼

此時的 index的值是0,而 i 的值是1,不知足 i<=index 這個條件,繼續執行下面的 index=i 的賦值,此時 index 的值爲1。可是若是第一個中間件內部又多執行了一次 next()的話,此時又會執行 dispatch(2)。上面說到,同一個中間件內的 i 的值是不變的,因此此時 i 的值依然是1,因此致使了 i <= index 的狀況。

可能會有人有疑問?既然 async 自己返回的就是 Promise,爲何還要在使用 Promise.resolve() 包一層呢。這是爲了兼容普通函數,使得普通函數也能正常使用。

再回到中間件的執行機制,來看看具體是怎麼回事。 咱們知道 async 的執行機制是:只有當全部的 await 異步都執行完以後才能返回一個 Promise。因此當咱們用 async 的語法寫中間件的時候,執行流程大體以下:

  1. 先執行第一個中間件(由於compose 會默認執行 dispatch(0)),該中間件返回 Promise,而後被 Koa 監聽,執行對應的邏輯(成功或失敗)
  2. 在執行第一個中間件的邏輯時,遇到 await next()時,會繼續執行 dispatch(i+1),也就是執行 dispatch(1),會手動觸發執行第二個中間件。這時候,第一個中間件 await next() 後面的代碼就會被 pending,等待 await next() 返回 Promise,纔會繼續執行第一個中間件 await next() 後面的代碼。
  3. 一樣的在執行第二個中間件的時候,遇到 await next() 的時候,會手動執行第三個中間件,await next() 後面的代碼依然被 pending,等待 await 下一個中間件的 Promise.resolve。只有在接收到第三個中間件的 resolve 後纔會執行後面的代碼,而後第二個中間會返回 Promise,被第一個中間件的 await 捕獲,這時候纔會執行第一個中間件的後續代碼,而後再返回 Promise
  4. 以此類推,若是有多箇中間件的時候,會依照上面的邏輯不斷執行,先執行第一個中間件,在 await next() 出 pending,繼續執行第二個中間件,繼續在 await next() 出 pending,繼續執行第三個中間,直到最後一箇中間件執行完,而後返回 Promise,而後倒數第二個中間件才執行後續的代碼並返回Promise,而後是倒數第三個中間件,接着一直以這種方式執行直到第一個中間件執行完,並返回 Promise,從而實現文章開頭那張圖的執行順序。

經過上面的分析以後,若是你要寫一個 koa2 的中間件,那麼基本格式應該就長下面這樣:

async function koaMiddleware(ctx, next){
    try{
        // do something
        await next()
        // do something
    }
    .catch(err){
        // handle err
    }    
}
複製代碼

最近正在使用 koa2 + React 寫一個博客,有興趣的同窗能夠前往 GitHub 地址查看:koa-blog-api

相關文章
相關標籤/搜索