首頁 > 軟體

洋蔥模型 koa-compose原始碼解析

2022-12-23 14:01:00

洋蔥模型

koa-compose是一個非常簡單的函數,它接受一箇中介軟體陣列,返回一個函數,這個函數就是一個洋蔥模型的核心。

原始碼地址:github.com/koajs/compo…

網上一搜一大把圖,我就不貼圖了,程式碼也不上,因為等會原始碼就是,這裡只是介紹一下概念。

洋蔥模型是一個非常簡單的概念,它的核心是一個函數,這個函數接受一個函數陣列,返回一個函數,這個函數就是洋蔥模型的核心。

這個返回的函數就是聚合了所有中介軟體的函數,它的執行順序是從外到內,從內到外。

例如:

  • 傳入一箇中介軟體陣列,陣列中有三個中介軟體,分別是abc
  • 返回的函數執行時,會先執行a,然後執行b,最後執行c
  • 執行完c後,會從內到外依次執行ba
  • 執行完a後,返回執行結果。

這樣說的可能還是不太清楚,來看一下流程圖:

這裡是兩步操作,第一步是傳入中介軟體陣列,第二步是執行返回的函數,而我們今天要解析就是第一步。

原始碼

原始碼並不多,只有不到 50 行,我們來看一下:

'use strict'
/**
 * Expose compositor.
 */
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) {
  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
   */
  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)
      }
    }
  }
}

雖然說不到 50 行,其實註釋都快佔了一半,直接看原始碼,先看兩個部分,第一個匯出,第二個是返回的函數。

module.exports = compose
function compose (middleware) {
  // ...
  return function (context, next) {
      return dispatch(0)
      function dispatch (i) {
        // ...
      }
  }
}

這裡確實是第一次見這樣玩變數提升的,所以先給大家講一下變數提升的規則:

  • 變數提升是在函數執行前,函數內部的變數和函數宣告會被提升到函數頂部。
  • 變數提升只會提升變數宣告,不會提升賦值。
  • 函數提升會提升函數宣告和函數表示式。
  • 函數提升會把函數宣告提升到函數頂部,函數表示式會被提升到變數宣告的位置。

這裡的module.exports = compose是變數提升,function compose是函數提升,所以compose函數會被提升到module.exports之前。

下面的return dispatch(0)是函數內部的變數提升,dispatch函數會被提升到return之前。

雖然這樣可行,但是不建議這樣寫,因為這樣寫會讓程式碼變得難以閱讀,不多說了,繼續吧:

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!')
    }
    // ...
}

最開始就是洋蔥模型的要求判斷了,中介軟體必須是陣列,陣列裡面的每一項必須是函數。

繼續看:

return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    // ...
}

這個函數是返回的函數,這個函數接收兩個引數,contextnextcontext是上下文,next是下一個中介軟體,這裡的nextcompose函數的第二個引數,也就是app.callback()的第二個引數。

index註釋寫的很清楚,是最後一個呼叫的中介軟體的索引,這裡初始化為-1,因為陣列的索引是從0開始的。

dispatch函數是用來執行中介軟體的,這裡傳入0,也就是從第一個中介軟體開始執行。

function dispatch (i) {
    if (i <= index) return Promise.reject(new Error('next() called multiple times'))
    index = i
}

可以看到,dispatch函數接收一個引數,這個引數是中介軟體的索引,這裡的i就是dispatch(0)傳入的0

這裡的判斷是為了防止next被呼叫多次,如果i小於等於index,就會丟擲一個錯誤,這裡的index-1,所以這個判斷是不會執行的。

後面就賦值了indexi,這樣就可以防止next被呼叫多次了,繼續看:

let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()

這裡的fn是中介軟體,也是當前要執行的中介軟體,通過索引直接從最開始初始化的middleware陣列裡面取出來。

如果是到了最後一箇中介軟體,這裡的next指的是下一個中介軟體,也就是app.callback()的第二個引數。

如果fn不存在,就返回一個成功的Promise,表示所有的中介軟體都執行完了。

繼續看:

try {
    return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
    return Promise.reject(err)
}

這裡就是執行中介軟體的地方了,fn是剛才取到的中介軟體,直接執行。

然後傳入contextdispatch.bind(null, i + 1),這裡的dispatch.bind(null, i + 1)就是next,也就是下一個中介軟體。

這裡就有點遞迴的感覺了,但是並沒有直接呼叫,而是通過外部手動呼叫next來執行下一個中介軟體。

這裡的try...catch是為了捕獲中介軟體執行過程中的錯誤,如果有錯誤,就返回一個失敗的Promise

動手

老規矩,還是用class來實現一下這個compose函數。

class Compose {
    constructor(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!')
        }
        this.index = -1
        this.middleware = middleware
        return (next) => {
            this.next = next
            return this.dispatch(0)
        }
    }
    dispatch(i) {
        if (i <= this.index) return Promise.reject(new Error('next() called multiple times'))
        this.index = i
        let fn = this.middleware[i]
        if (i === this.middleware.length) fn = this.next
        if (!fn) return Promise.resolve()
        try {
            return Promise.resolve(fn(this.dispatch.bind(this, i + 1)))
        } catch (err) {
            return Promise.reject(err)
        }
    }
}
var middleware = [
    (next) => {
        console.log(1)
        next()
        console.log(2)
    },
    (next) => {
        console.log(3)
        next()
        console.log(4)
    },
    (next) => {
        console.log(5)
        next()
        console.log(6)
    }
]
var compose = new Compose(middleware)
compose()
var middleware = [
    (next) => {
        return next().then((res) => {
            return res + '1'
        })
    },
    (next) => {
        return next().then((res) => {
            return res + '2'
        })
    },
    (next) => {
        return next().then((res) => {
            return res + '3'
        })
    }
]
var compose = new Compose(middleware)
compose(() => {
    return Promise.resolve('0')
}).then((res) => {
    console.log(res)
})

這次不放執行結果的截圖了,可以直接瀏覽器控制檯中自行執行。

總結

koa-compose的實現原理就是通過遞回來實現的,每次執行中介軟體的時候,都會返回一個成功的Promise

其實這裡不使用Promise也是可以的,但是使用Promise可以有效的處理非同步和錯誤。

而且從上面手動實現的程式碼案例中也可以看到,使用Promise可以有更多的靈活性,寫法也是多元化。

以上就是洋蔥模型 koa-compose原始碼解析的詳細內容,更多關於洋蔥模型 koa-compose的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com