koa compose源碼解析

koa compose 源碼解析

koa -基於 Node.js 的下一代 web 開發框架。css

它最大的特色就是獨特的中間件流程控制,是一個典型的洋蔥模型。koa 和 koa2 中間件的思路是同樣的,可是實現方式有所區別,koa2 在 Node7.6 以後更是能夠直接用 async/await 來替代 generator 使用中間件,本文以最後一種狀況舉例。node

本文主要是對 compose 模塊的源碼解讀git

源碼解讀前準備

瞭解洋蔥模型

下面的圖是網上找的,很清晰的代表了一個請求是如何通過中間件最後生成響應的,這種模式中開發和使用中間件都是很是方便的。 咱們都知道在函數式編程的思想中,compose 是將多個函數合併成一個函數(g() + h() => g(h())),koa 中的 compose 則是將 koa/koa-router 各個中間件合併執行,結合 next() 就造成了下圖所示的洋蔥模型github

koa 示例測試,查看洋蔥模型的執行順序和優勢

  • 執行順序測試 咱們建立一個 koa 應用
const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
    console.log('第一個中間件函數')
    await next();
    console.log('第一個中間件函數next以後!');
})
app.use(async (ctx, next) => {
    console.log('第二個中間件函數')
    await next();
    console.log('第二個中間件函數next以後!');
})

app.use(async ctx => {
    ctx.body = 'Hello World';
});

app.listen(3000);   
複製代碼

執行命令 node demo1.jsweb

執行結果以下所示:編程

爲何會是上面這種結果呢,咱們帶着這些疑問一塊兒去繼續往下看,看到最後確定會能理解。api

注意:在使用app.use將給定的中間件添加到應用程序時,middlewar(其實就是一個函數)接收兩個參數:ctxnext。其中next也是一個函數。數組

compose 源碼解讀

compose 代碼以下,去掉註釋,代碼就 25 行,細讀確實是很精妙的代碼,雖然看着很短,但粗看幾層 return ,仍是會有點繞。bash

module.exports = compose

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

function compose (middleware) {
    //傳入的 middleware 參數必須是數組
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  //middleware 數組的元素必須是函數
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise} 返回一個閉包函數,函數的返回是一個Promise 對象, 保持對 middleware 的引用。
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      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, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
複製代碼

咱們首先去掉條件判斷,看下最裏面的實際返回閉包

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
複製代碼

fn = middleware[i] 也就是某一箇中間件,很顯然上述代碼遍歷中間件數組middleware,依次拿到中間件fn,並執行:

fn(context, dispatch.bind(null, i + 1))
複製代碼

這裏能夠看到傳遞給中間件的兩個參數:contextnext函數。 前面咱們提到過:在使用app.use 將定的中間件添加到應用程序時,中間件(其實就是一個函數)接收兩個參數:ctxnext。其中next也是一個函數。 看到這裏是否是清楚了在註冊 middwleare的時候爲何要有兩個參數了吧~

回到前面的問題,爲何咱們的demo執行的結果會是上面,咱們看第一個中間件,

app.use(async (ctx, next) => {
    console.log('第一個中間件函數')
    await next();
    console.log('第一個中間件函數next以後!');
})
複製代碼

帶入到代碼中,第一次執行 return dispatch(0), 這時第一個中間件被調用,繼續展開

  • dispatch(0)展開
Promise.resolve((async (ctx, next) => {
   console.log('第一個中間件函數')
   await next();
   console.log('第一個中間件函數next以後');
})(context, dispatch.bind(null, i + 1)));
複製代碼

首先執行 console.log('第一個中間件函數')沒啥毛病, 接下來執行 next()方法,就跑到第二個中間件去了,因此沒有執行第二個 console.log()

app.use(async (ctx, next) => {
    console.log('第二個中間件函數')
    await next();
    console.log('第二個中間件函數next以後!');
})
複製代碼
  • dispatch(1)展開
Promise.resolve(async (ctx, next) => Promise.resolve(async (ctx, next) => {
    console.log('第一個中間件函數')
    Promise.resolve((async (ctx, next) => {
        console.log('第二個中間件函數')
        await next();
        console.log('第二個中間件函數next以後');
    })(context, dispatch.bind(null, i + 1)));
    console.log('第一個中間件函數next以後')
}))
複製代碼

因此執行 onsole.log('第二個中間件函數')是否是就很清楚的看出來了。

在第二個中間件執行到await next()時,一樣會輪轉到第三個中間件,接下若是有第四個中間件,第五個中間件,聰明的大家會發現,以此類推,直到最後一箇中間件。

看到這裏,咱們會不會很好奇 koa 是怎麼調用compose 的呢,等後面的文章再更新~

總結

以上就是我關於 koa compose 的解讀和洋蔥模型的解析。但願對你們有所幫助,從代碼上咱們能夠看出,洋蔥模型也是有所缺陷的,一旦中間件過多,性能仍是會有必定的影響的,因此咱們須要結合本身的項目場景做出合適的選擇。

若是以上有問題,歡迎你們留言,一塊兒探討,謝謝!。

參考連接

相關文章
相關標籤/搜索