中介軟體
首先寫一個簡單的中介軟體demo:
const Koa = require('koa')
const app = new Koa()
const port = 3000
const ctx1 = async (ctx, next) => {
console.log('開始執行中介軟體1')
await next()
ctx.response.type = 'text/html'
ctx.response.body = '<h3>hello world</h3>'
console.log('結束執行中介軟體1')
}
app.use(ctx1)
app.use(async function ctx2 (ctx, next) {
console.log('開始執行中介軟體2')
await next()
console.log('結束執行中介軟體2')
})
app.listen(port, () => {
console.log(`server is running on the port: ${port}`)
})
複製程式碼
很明顯中介軟體執行順序是這樣的:
開始執行中介軟體1
開始執行中介軟體2
結束執行中介軟體2
結束執行中介軟體1
複製程式碼
你可以理解為koa2會先按照中介軟體註冊順序執行next()之前的程式碼, 執行完到底部之後, 返回往前執行next()之後的程式碼。
重點是我們需要koa2原始碼究竟是怎麼樣執行的? 現在開始除錯模式進入koa2原始碼一探究竟。
- 首先在兩個中介軟體註冊的地方打了斷點
- 我們可以看到koa2是先按照你中介軟體的順序去註冊執行
- 然後會進入callback. 這是因為
// 應用程式
app.listen(port, () => {
console.log(`server is running on the port: ${port}`)
})
// 原始碼
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
複製程式碼
這個時候this.middleware已經存了兩個中介軟體。
- 這個時候你請求一個路由比如
http://localhost:3000/a
複製程式碼
koa2的中介軟體處理就是在這個函式裡面
callback() {
// compose()這是處理中介軟體的執行順序所在
}
複製程式碼
於是我們進入這個koa-compose的原始碼看下:
'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
// 返回一個函式, 從第一個中介軟體開始執行, 可以通過next()呼叫後續中介軟體
return dispatch(0)
// dispatch始終返回一個Promise物件
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 {
// next即就是通過dispatch(i+1)來執行下一個中介軟體
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
// 捕獲中介軟體中發生的異常
return Promise.reject(err)
}
}
}
}
複製程式碼
此時i=0取出第一個中介軟體,由於閉包原因i是一直存在的。
這個時候可以看到fn就是ctx1。
注意
// next即就是通過dispatch(i+1)來執行下一個中介軟體
dispatch.bind(null, i + 1)
複製程式碼
這個時候開始進入第一個中介軟體執行第一句console.log('開始執行中介軟體1')
這裡也能看到next指的就是前面提到的dispatch.bind。
然後我們繼續單步除錯進入這句
// ctx1中的
await next()
複製程式碼
此時又重新進入compose(), 繼續執行下一個中介軟體, i=1
取出第二個中介軟體函式ctx2。
此時進入第二個中介軟體ctx2開始執行console.log('開始執行中介軟體2')
繼續單步除錯
此時i=2,fx=undefined
// 這個洋蔥模型的最後做一個兜底的處理
if (!fn) return Promise.resolve()
複製程式碼
執行中介軟體ctx2的第二句console
補充下async的執行機制: async 的執行機制是:只有當所有的 await 非同步都執行完之後才能返回一個 Promise。所以當我們用 async的語法寫中介軟體的時候,執行流程大致如下:
先執行第一個中介軟體(因為compose會預設執行dispatch(0)),該中介軟體返回 Promise,然後被Koa監聽,執行對應的邏輯(成功或失敗)在執行第一個中介軟體的邏輯時,遇到 await next()時,會繼續執行dispatch(i+1),也就是執行 dispatch(1),會手動觸發執行第二個中介軟體。
這時候,第一個中介軟體 await next() 後面的程式碼就會被 pending,等待 await next() 返回 Promise,才會繼續執行第一個中介軟體 await next() 後面的程式碼。
同樣的在執行第二個中介軟體的時候,遇到await next()的時候,會手動執行第三個中介軟體,await next() 後面的程式碼依然被 pending,等待 await 下一個中介軟體的Promise.resolve。
只有在接收到第三個中介軟體的 resolve 後才會執行後面的程式碼,然後第二個中間會返回 Promise,被第一個中介軟體的 await 捕獲,這時候才會執行第一個中介軟體的後續程式碼,然後再返回 Promise 以此類推。
如果有多箇中介軟體的時候,會依照上面的邏輯不斷執行,先執行第一個中介軟體,在 await next() 出 pending,繼續執行第二個中介軟體,繼續在 await next() 出 pending,繼續執行第三個中間,直到最後一箇中介軟體執行完,然後返回 Promise,然後倒數第二個中介軟體才執行後續的程式碼並返回Promise,然後是倒數第三個中介軟體,接著一直以這種方式執行直到第一個中介軟體執行完,並返回 Promise,從而實現文章開頭那張圖的執行順序。