中間件執行模塊koa-Compose源碼分析

原文博客地址,歡迎學習交流: 點擊預覽

讀了下Koa的源碼,寫的至關的精簡,遇處處理中間件執行的模塊koa-Compose,決定學習一下這個模塊的源碼。css

閱讀本文能夠學到:node

  • Koa中間件的加載
  • next參數的來源
  • 中間件控制權執行順序

先上一段使用Koa啓動服務的代碼:
放在文件app.jsgit

const koa = require('koa');  // require引入koa模塊
const app = new koa();    // 建立對象
app.use(async (ctx,next) => {
    console.log('第一個中間件')
    next();
})
app.use(async (ctx,next) => {
    console.log('第二個中間件')
    next();
})

app.use((ctx,next) => {
    console.log('第三個中間件')
    next();
})

app.use(ctx => {
    console.log('準備響應');
    ctx.body = 'hello'
})

app.listen(3000)

以上代碼,可使用node app.js啓動,啓動後能夠在瀏覽器中訪問http://localhost:3000/
訪問後,會在啓動的命令窗口中打印出以下值:github

第一個中間件
第二個中間件
第三個中間件
準備響應

代碼說明:數據庫

  • app.use()方法,用來將中間件添加到隊列中
  • 中間件就是傳給app.use()做爲的參數的函數
  • 使用app.use()將函數添加至隊列之中後,當有請求時,會依次觸發隊列中的函數,也就是依次執行一個個中間件函數,執行順序按照調用app.use()添加的順序。
  • 在每一箇中間件函數中,會執行next()函數,意思是把控制權交到下一個中間件(其實是調用next函數後,會調用下一個中間件函數,後面解析源碼會有說明),若是不調用next()函數,不能調用下一個中間件函數,那麼隊列執行也就終止了,在上面的代碼中表現就是不能響應客戶端的請求了。
app.use(async (ctx,next) => {
    console.log('第二個中間件')
    // next(); 註釋以後,下一個中間件函數就不會執行
})

內部過程分析

  • 內部利用app.use()添加到一個數組隊列中:
// app.use()函數內部添加
this.middleware.push(fn);
// 最終this.middleware爲:
this.middleware = [fn,fn,fn...]

具體參考這裏Koa的源碼use函數:https://github.com/koajs/koa/blob/master/lib/application.js#L104segmentfault

  • 使用koa-compose模塊的compose方法,把這個中間件數組合併成一個大的中間件函數
const fn = compose(this.middleware);

具體參考這裏Koa的源碼https://github.com/koajs/koa/blob/master/lib/application.js#L126api

  • 在有請求後後會執行這個中間件函數fn,進而會把全部的中間件函數依次執行
這樣片面的描述可能會不知所云,能夠跳過不看,只是讓諸位知道Koa執行中間件的過程
本篇主要是分析 koa-compose的源碼,以後分析整個Koa的源碼後會作詳細說明

因此最主要的仍是使用koa-compose模塊來控制中間件的執行,那麼來一探究竟這個模塊如何進行工做的數組

koa-compose

koa-compose模塊能夠將多箇中間件函數合併成一個大的中間件函數,而後調用這個中間件函數就能夠依次執行添加的中間件函數,執行一系列的任務。promise

源碼地址:https://github.com/koajs/compose/blob/master/index.js瀏覽器

先從一段代碼開始,建立一個compose.js的文件,寫入以下代碼:

const compose = require('koa-compose');

function one(ctx,next){
    console.log('第一個');
    next(); // 控制權交到下一個中間件(其實是能夠執行下一個函數),
}
function two(ctx,next){
    console.log('第二個');
    next();
}
function three(ctx,next){
    console.log('第三個');
    next();
}
// 傳入中間件函數組成的數組隊列,合併成一箇中間件函數
const middlewares = compose([one, two, three]);
// 執行中間件函數,函數執行後返回的是Promise對象
middlewares().then(function (){
    console.log('隊列執行完畢');    
})

可使用node compose.js運行此文件,命令行窗口打印出:

第一個
第二個
第三個
隊列執行完畢

中間件這兒的重點,是compose函數。compose函數的源代碼雖然很簡潔,但要理解明白着實要下一番功夫。
如下爲源碼分析:

'use strict'

/**
 * Expose compositor.
 */
// 暴露compose函數
module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */
// compose函數須要傳入一個數組隊列 [fn,fn,fn,fn]
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
   */

   // compose函數調用後,返回的是如下這個匿名函數
   // 匿名函數接收兩個參數,第一個隨便傳入,根據使用場景決定
   // 第一次調用時候第二個參數next其實是一個undefined,由於初次調用並不須要傳入next參數
   // 這個匿名函數返回一個promise
  return function (context, next) {
    // last called middleware #
    //初始下標爲-1
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      // 若是傳入i爲負數且<=-1 返回一個Promise.reject攜帶着錯誤信息
      // 因此執行兩次next會報出這個錯誤。將狀態rejected,就是確保在一箇中間件中next只調用一次
      

      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      // 執行一遍next以後,這個index值將改變
      index = i
      // 根據下標取出一箇中間件函數
      let fn = middleware[i]
      // next在這個內部中是一個局部變量,值爲undefined
      // 當i已是數組的length了,說明中間件函數都執行結束,執行結束後把fn設置爲undefined
      // 問題:原本middleware[i]若是i爲length的話取到的值已是undefined了,爲何要從新給fn設置爲undefined呢?
      if (i === middleware.length) fn = next

      //若是中間件遍歷到最後了。那麼。此時return Promise.resolve()返回一個成功狀態的promise
      // 方面以後作調用then
      if (!fn) return Promise.resolve()

      // try catch保證錯誤在Promise的狀況下可以正常被捕獲。

      // 調用後依然返回一個成功的狀態的Promise對象
      // 用Promise包裹中間件,方便await調用
      // 調用中間件函數,傳入context(根據場景不一樣能夠傳入不一樣的值,在KOa傳入的是ctx)
      // 第二個參數是一個next函數,可在中間件函數中調用這個函數
      // 調用next函數後,遞歸調用dispatch函數,目的是執行下一個中間件函數
      // next函數在中間件函數調用後返回的是一個promise對象
      // 讀到這裏不得不佩服做者的高明之處。
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

補充說明:

  • 根據以上的源碼分析獲得,在一箇中間件函數中不能調用兩次next(),不然會拋出錯誤
function one(ctx,next){
    console.log('第一個');
    next();
    next();
}

拋出錯誤:

next() called multiple times
  • next()調用後返回的是一個Promise對象,能夠調用then函數
function two(ctx,next){
    console.log('第二個');
    next().then(function(){
        console.log('第二個調用then後')
    });
}
  • 中間件函數能夠是async/await函數,在函數內部能夠寫任意的異步處理,處理獲得結果後再進行下一個中間件函數。

建立一個文件問test-async.js,寫入如下代碼:

const compose = require('koa-compose');

// 獲取數據
const getData = () => new Promise((resolve, reject) => {
    setTimeout(() => resolve('獲得數據'), 2000);
});

async function one(ctx,next){
    console.log('第一個,等待兩秒後再進行下一個中間件');
    // 模擬異步讀取數據庫數據
    await getData()  // 等到獲取數據後繼續執行下一個中間件
    next()
}
function two(ctx,next){
    console.log('第二個');
    next()
}
function three(ctx,next){
    console.log('第三個');
    next();
}

const middlewares = compose([one, two, three]);

middlewares().then(function (){
    console.log('隊列執行完畢');    
})

可使用node test-async.js運行此文件,命令行窗口打印出:

第一個,等待兩秒後再進行下一個中間件
第二個
第三個
第二個調用then後
隊列執行完畢

在以上打印輸出過程當中,執行第一個中間件後,在內部會有一個異步操做,使用了async/await後獲得同步操做同樣的體驗,這步操做多是讀取數據庫數據或者讀取文件,讀取數據後,調用next()執行下一個中間件。這裏模擬式等待2秒後再執行下一個中間件。

更多參考了async/await: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/await

執行順序

調用next後,執行的順序會讓人產生迷惑,建立文件爲text-next.js,寫入如下代碼:

const koa = require('koa');
const app = new koa();
app.use((ctx, next) => {
  console.log('第一個中間件函數')
  next();
  console.log('第一個中間件函數next以後');
})
app.use(async (ctx, next) => {
  console.log('第二個中間件函數')
  next();
  console.log('第二個中間件函數next以後');
})
app.use(ctx => {
  console.log('響應');
  ctx.body = 'hello'
})

app.listen(3000)

以上代碼,可使用node text-next.js啓動,啓動後能夠在瀏覽器中訪問http://localhost:3000/
訪問後,會在啓動的命令窗口中打印出以下值:

第一個中間件函數
第二個中間件函數
響應
第二個中間件函數next以後
第一個中間件函數next以後

是否是對這個順序產生了深深地疑問,爲何會這樣呢?

當一箇中間件調用 next() 則該函數暫停並將控制傳遞給定義的下一個中間件。當在下游沒有更多的中間件執行後,堆棧將展開而且每一箇中間件恢復執行其上游行爲。
過程是這樣的:

  • 先執行第一個中間件函數,打印出 '第一個中間件函數'
  • 調用了next,再也不繼續向下執行
  • 執行第二個中間件函數,打印出 '第二個中間件函數'
  • 調用了next,再也不繼續向下執行
  • 執行最後一箇中間件函數,打印出 '響應'
  • ...
  • 最後一箇中間函數執行後,上一個中間件函數收回控制權,繼續執行,打印出 '第二個中間件函數next以後'
  • 第二個中間件函數執行後,上一個中間件函數收回控制權,繼續執行,打印出 '第一個中間件函數next以後'

借用一張圖來直觀的說明:
圖片描述

具體看別人怎麼理解next的順序:https://segmentfault.com/q/1010000011033764

最近在看Koa的源碼,以上屬於我的理解,若有誤差歡迎指正學習,謝謝。

參考資料:https://koa.bootcss.com/
https://cnodejs.org/topic/58fd8ec7523b9d0956dad945

相關文章
相關標籤/搜索