對Koa-middleware實現機制的深入分析

ssssyoki發表於2019-03-04

Koa是基於Node.js的下一代web開發框架,相比Express更輕,原始碼只有幾百行。與傳統的中介軟體不同,在Koa 1.x中採用了generator實現中介軟體,這需要開發者熟悉ES6中的generator,Promise相關知識。

Koa官方文件示例程式碼中,採用yield next為跳轉訊號,然後會逆序執行中介軟體剩下的程式碼邏輯。這其中的邏輯非常有趣,本文將對其進行深入的分析。

Section A:

Koa的中介軟體跑在co模組下,而co可以將非同步“變為”同步,從而實現用同步的方法寫非同步程式碼,避免了Node.js大量的回撥巢狀。現在我們從實現一個簡易的co方法開始探索其中的機制。

function co(generator){
  let g = generator();
  let next = function(data){
      let result = g.next(data);

      if(result.done){
          return ;
      };

      if(result.value instanceof Promise){
          result.value.then(function(d){
              next(d);
          },function(err){
              next(err);
          });
      }else{
          next();
      };
  };

  next();
};複製程式碼

首先需要了解generator相關知識,接下來我們逐步分析這段程式碼:

1.我們首先定義一個引數為generator的co函式。

2.當傳入generator後(即app.use(function *(){...}))定義next方法實現對generator(可以理解為狀態機)的狀態遍歷,由於每次遍歷器指向新的yield,返回結構如{value:`Promise`,`done`:`true/false`}的值,當done的值為false時遍歷狀態完畢並返回,若為true則繼續遍歷。其中內部的g.next(data)可以將上一個yield的返回值傳遞給外部。

3.同時,若generator中含有多個yield且遍歷未完成(即result.valuePromise物件 && result.done === false),resolve()所傳遞的資料可以在接下來then()方法中直接使用,即遞迴呼叫,直到result.done === true遍歷結束並退出。

這裡可能存在一個疑惑,在第一次呼叫next()方法時data為undefined,那是否會導致error產生呢?其實V8引擎在執行時,會自動忽略第一次呼叫next()時的引數,所以只有從第二次使用next()方法時引數才是有效的。

一言以蔽之,co實現了Promise遞迴呼叫generator的next方法。

Section B:

理解了co的執行原理後,再來理解middleware的機制就容易多了。

middleware實現了所謂“逆序”執行,其實就是每次呼叫use()方法時,將generator存入陣列(記為s)中儲存。

在執行的時候先定義一個執行索引(記為index)和跳轉標記(記為turn,也就是yield next中的next),再定義一個儲存generator函式物件的陣列(記為gs)。然後獲取當前中介軟體generator,接著獲取該generator的函式物件,將函式物件放在gs陣列內儲存,再執行generator的next()方法。

執行開始後,根據返回的value進行不同的處理,如果是標記turn(即執行到了yield next),說明該跳到下一個中介軟體了,此時令index++,然後從陣列g中獲取下一個中介軟體重複上一個中介軟體的執行流程。

當執行到的中介軟體沒有yield時,並且返回的donetrue時,逆序執行。從此前用於儲存generator函式物件的gs陣列中取出上一個generator物件,然後執行generator的next()方法,直到全部結束。

我們開啟Koa的application.js檔案:


/**
 * Use the given middleware `fn`.
 *
 * @param {GeneratorFunction} fn
 * @return {Application} self
 * @api public
 */

app.use = function(fn){
  if (!this.experimental) {
    // es7 async functions are not allowed,
    // so we have to make sure that `fn` is a generator function
    assert(fn && `GeneratorFunction` == fn.constructor.name, `app.use() requires a generator function`);
  }
  debug(`use %s`, fn._name || fn.name || `-`);
  this.middleware.push(fn);
  return this;
};複製程式碼

顯而易見,app.use()方法就是將generator傳入this.middleware陣列中。其他部分的邏輯原始碼註釋非常清晰,不再贅述。

我們再開啟Koa-compose模組的index.js檔案:


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

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;
  }
}複製程式碼

其中最關鍵的就是while語句。

將之前app.use()傳入並儲存在middleware中的generator逆序取出並執行,將每個generator執行後的結果(即generator() === iterator)作為引數傳入下一個(按陣列的順序則為前一個)generator中,在最後一個generator(陣列第一個)執行後得出的next變數(即第一個generator的iterator),執行yield *next(即執行第一個generator的iterator)將全部generator像連結串列般串聯起來。

根據yield *的特性,yield *next將依次執行所有套用的next(類似遞迴),從而形成所謂“正序執行再逆序執行”的流程。

從co到compose,程式碼只有短短几十行,但組合在一起卻非常精巧奇妙,值得細細品味。

相關文章