天天看點

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

koa是基于node.js的下一代web開發架構,相比express更輕,源碼隻有幾百行。與傳統的中間件不同,在koa 1.x中采用了generator實作中間件,這需要開發者熟悉es6中的generator,promise相關知識。

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

在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.value 是 promise 對象

&& 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 時,并且傳回的 done 為 true 時,逆序執行。從此前用于儲存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} 

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,代碼隻有短短幾十行,但組合在一起卻非常精巧奇妙,值得細細品味。

作者:佚名

來源:51cto

繼續閱讀