compose
是一個工具函數,Koa.js
的中間件經過這個工具函數組合後,按 app.use()
的順序同步執行,也就是造成了 洋蔥圈 式的調用。javascript
這個函數的源代碼不長,不到50行,代碼地址 github.com/koajs/compo…java
利用遞歸實現了 Promise 的鏈式執行,無論中間件中是同步仍是異步都經過 Promise 轉成異步鏈式執行。node
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!')
}
...
}複製代碼
函數開頭對參數作了類型的判斷,確保輸入的正確性。middleware 必須是一個數組,數組中的元素必須是 function
。git
function compose (middleware) {
//...
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, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}複製代碼
接下來,是返回了一個函數,接受兩個參數,context
和 next
。context
是 koa 中的 ctx
,next
是全部中間件執行完後,框架使用者來最後處理請求和返回的回調函數。同時函數是一個閉包函數,存儲了全部的中間件,經過遞歸的方式不斷的運行中間件。github
經過代碼能夠看到,做爲中間件一樣必須接受兩個參數, context
和 next
。若是某個中間件沒有調用 next()
, 後面的中間件是不會執行的。這是很是常見的將多個異步函數轉爲同步的處理方式。shell
直接看代碼:數組
const compose = require('./compose')
function mw1 (context, next) {
console.log('===== middleware 1 =====')
console.log(context)
setTimeout(() => {
console.log(`inner: ${context}`)
next()
}, 1000)
}
function mw2 (context, next) {
console.log('===== middleware 2 =====')
console.log(context)
next()
}
function mw3 (context, next) {
console.log('===== middleware 3 =====')
console.log(context)
setTimeout(() => {
console.log(`inner: ${context}`)
}, 1000)
next()
}
const run = compose([mw1, mw2, mw3])
run('context', function () {
console.log('all middleware done!')
})複製代碼
輸出結果是:閉包
===== middleware 1 =====
context
inner: context
===== middleware 2 =====
context
===== middleware 3 =====
context
all middleware done!
inner: context複製代碼
第三個中間件中,故意把 next()
寫在了異步的外面,會致使中間件還完成就直接進入下一個中間件的運行了(這裏是全部中間件運行完後的回調函數)。compose()
生成的函數是 thenable 函數,咱們改一下最後的運行部分。架構
run('context').then(() => {
console.log('all middleware done!')
})複製代碼
結果是:app
===== middleware 1 =====
context
all middleware done!
inner: context
===== middleware 2 =====
context
===== middleware 3 =====
context
inner: context複製代碼
看起來結果不符合咱們的預期,這是由於在 compose 源代碼中,中間件執行完後返回的是一個 Promise
對象,若是咱們在 Promise
中再使用異步函數而且不使用then
來處理異步流程,顯然是不合理的,咱們能夠改一下上面的中間件代碼。
function mw1 (context, next) {
console.log('===== middleware 1 =====')
console.log(context)
return new Promise(resolve => {
setTimeout(() => {
console.log(`inner: ${context}`)
resolve()
}, 1000)
}).then(() => {
return next ()
})
}
function mw2 (context, next) {
console.log('===== middleware 2 =====')
console.log(context)
return next()
}
function mw3 (context, next) {
console.log('===== middleware 3 =====')
console.log(context)
return new Promise(resolve => {
setTimeout(() => {
console.log(`inner: ${context}`)
resolve()
}, 1000)
}).then(() => {
return next ()
})
}複製代碼
輸出:
===== middleware 1 =====
context
inner: context
===== middleware 2 =====
context
===== middleware 3 =====
context
inner: context
all middleware done!複製代碼
這下沒問題了,每個中間件都會返回一個 thenable 的 Promise
對象。
既然是在研究Koa.js 那麼咱們就把上面的代碼再改改,使用 async/await
改寫一下,把異步函數改爲一個 thenable 函數。
async function sleep (context) {
return new Promise(resolve => {
setTimeout(() => {
console.log(`inner: ${context}`)
resolve()
}, 1000)
})
}
async function mw1 (context, next) {
console.log('===== middleware 1 =====')
console.log(context)
await sleep(context)
await next()
}
async function mw2 (context, next) {
console.log('===== middleware 2 =====')
console.log(context)
return next()
}
async function mw3 (context, next) {
console.log('===== middleware 3 =====')
console.log(context)
await sleep(context)
await next ()
}複製代碼
在平常的開發中,Node 後臺通常是做爲微服務架構中的一個面向終端的 API Gateway。
如今有這樣一個場景:咱們從三個其餘微服務中獲取數據再聚合成一個 HTTP API,若是三個服務提供的 service 沒有依賴的話,這種狀況比較簡單,用 Promise.all()
就能夠實現,代碼以下:
function service1 () {
return new Promise((resolve, reject) => {
resolve(1)
})
}
function service2 () {
return new Promise((resolve, reject) => {
resolve(2)
})
}
function service3 () {
return new Promise((resolve, reject) => {
resolve(3)
})
}
Promise.all([service1(), service2(), service3()])
.then(res => {
console.log(res)
})複製代碼
那若是 service2 的請求參數依賴 service1 返回的結果, service3 的請求參數又依賴於 Service2 返回的結果,那就得將一系列的異步請求轉成同步請求,compose 就能夠發揮其做用了,固然用 Promise 的鏈式調用也是能夠實現的,可是代碼耦合度高,不利於後期維護和代碼修改,若是 一、二、3 的順序調換一下,代碼改動就比較大了,另外耦合度過高的代碼不利於單元測試,這裏有一個文章是經過依賴注入的方式解耦模塊,保持模塊的獨立性,便於模塊的單元測試。
Compose 是一種基於 Promise 的流程控制方式,能夠經過這種方式對異步流程同步化,解決以前的嵌套回調和 Promise 鏈式耦合。
Promise 的流程控制有不少種,下篇文章再來寫不一樣應用場景中分別運用的方法。