【源碼學習----koa】koa中間件核心(koa-compose)源碼解讀分析

最近經常使用koa進行服務端開發,迷戀上了koa的洋蔥模型,覺得這玩意太好用了。而且koa是以精簡爲主,沒有很多集成東西,所有的東西都需按需加載,這個更是太合我胃口了哈哈哈哈。

相對與express的中間件,express的中間件使用的是串聯,就像冰糖葫蘆一樣一個接着一個,而koa使用的V型結構(洋蔥模型),這將給我們的中間件提供更加靈活的處理方式。

基於對洋蔥模型的熱衷,所以對koa的洋蔥模型進行一探究竟,不管是koa1還是koa2的中間件都是基於koa-compose進行編寫的,這種V型結構的實現就來源於koa-compose。
附上源碼先:

function compose (middleware) {
  //  參數middleware 是一箇中間件數組,存放我們用app.use()一個個串聯起來的中間件
  //  判斷中間件列表是否爲數組,如果不爲數組,則拋出類型錯誤
  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!')
  }

  /**
 1. @param {Object} context
 2. @return {Promise}
 3. @api public
   */
  
  return function (context, next) {
    // 這裏next指的是洋蔥模型的中心函數
    // context是一個配置對象,保存着一些配置,當然也可以利用context將一些參數往下一個中間傳遞
     
    // last called middleware #
    let index = -1  // index是記錄執行的中間件的索引
    return dispatch(0)  // 執行第一個中間件  然後通過第一個中間件遞歸調用下一個中間件
    
    function dispatch (i) {
      // 這裏是保證同個中間件中一個next()不被調用多次調用 
      // 當next()函數被調用兩次的時候,i會小於index,然後拋出錯誤
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i] // 取出要執行的中間件
      if (i === middleware.length) fn = next  // 如果i 等於 中間件的長度,即到了洋蔥模型的中心(最後一箇中間件)
      if (!fn) return Promise.resolve()  // 如果中間件爲空,即直接resolve
      try {
        //  遞歸執行下一個中間件 (下面會重點分析這個)
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

看到這裏,如果下面的那些能夠理解,那麼下面的可以不用看的,還是不能理解的就繼續往下看,詳細一點的分析。

  1. 首先,我們用app.use()添加一箇中間件,在koa的源碼裏app.use()這個方法就是將一箇中間件push進middleware這個中間件列表裏。源碼裏是這麼寫的(這個比較簡單 不做分析):
 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;
  }

compose這個方法傳入一箇中間件列表middleware,這個列表就是我們使用use()添加進去的方法列表,首先會判斷列表是否爲數組,中間件是否爲方法,如果不是就直接拋出類型錯誤。

  1. compose返回的是一個函數,這裏使用閉包來緩存中間件列表,然後這個函數接收兩個參數,第一個參數是context,是一個存放配置信息的對象。第二份參數是一個next方法,也是洋蔥模型的中心或者說是V型模型的拐點。
  2. 創建一個index變量來保存執行的中間件索引,然後從第一個中間件開始開始遞歸執行。
      let index = -1
      return dispatch(0)
  1. dispatch方法就是執行中間件,先判斷索引,如果i小於index那麼說明在同一個中間件裏執行了兩次或兩次以上的next函數,如果i>index則說明該中間件還未執行,那麼將該中間件的所以記錄下來
        if (i <= index) return Promise.reject(new Error('next() called multiple times'))
        index = i
  1. 取出該中間件,如果i等於中間件的長圖,則說明執行到了洋蔥模型的中心,則最後一箇中間件,如果中間件爲空,那麼就直接resovle掉
        let fn = middleware[i]
        if(i === middleware.length){
          fn = next
        }
        if(!fn){
            return Promise.resolve()
        }
  1. 到了最刺激的部分了,也是有點繞的部分了,首先爲啥return的是一個Promise的對象(Promise.resolve也是一個promise對象)呢,因爲我們await next()的時候,await是等待且執行一個async函數的完成,async會默認返回一個promise對象,所以這裏return的是一個promise對象。我們在每個中間裏面await mext() next()指的就是下一個中間件,也就是
fn(context, function next () {
            return dispatch(i + 1)
          })

所以我們上一個中的await 等待的就是dispatch(i+1)的執行完成,dispatch返回的是Promise.resolve(fn(context, function next () { xxxx })),這樣來看雖然一開始只執行了dispatch(0),但是是由這個函數形成了一條執行鏈。
以三個中間件執行爲例,dispatch(0)執行後就形成:

Promise.resolve( // 第一個中間件
  function(context,next){  // 這裏的next第二個中間件也就是dispatch(1)
     // await next上的代碼 (中間件1)
    await Promise.resolve( // 第二個中間件
      function(context,next){  // 這裏的next第二個中間件也就是dispatch(2)
          // await next上的代碼 (中間件2)
        await Promise.resolve( // 第三個中間件
          function(context,next){  // 這裏的next第二個中間件也就是dispatch(3)
             // await next上的代碼 (中間件3)
            await Promise.resolve()
            // await next下的代碼 (中間件3)
          }
        )
          // await next下的代碼 (中間件2)
      }
    )
      // await next下的代碼 (中間件2)
  }
) 

先執行await上面的代碼,然後等待最後一箇中間件resolve一個個往上傳遞,這就形成了一個洋蔥模型。
最後附上測試代碼:

async function test1(ctx, next) {
    console.log('中間件1上');
    await next();
    console.log('中間件1下');
  };
  
  async function test2(ctx, next) {
    console.log('中間件2上');
    await next();
    console.log('中間件2下');
  };
  
  async function test3(ctx, next) {
    console.log('中間件3上');
    await next();
    console.log('中間件3下');
  };
  let middleware = [test1, test2, test3];
  
  let cp = compose(middleware);
  
  cp('ctx', function() {
    console.log('中心');
  });

OK,到這裏koa2的中間件核心(koa-compose)就解析完成了,一開始看的時候,也被繞了好久,多看幾遍多分析一步一步捋順。koa1的中間件等過幾天有時間再補上吧,koa1是基於generator,源碼比起koa2相對簡單。

最近在看koa2源碼,等有時間再繼續更新koa一些源碼的分析。

前端小學生(應屆畢業生),如有錯誤或者其他想法的。歡迎指正交流~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章