你需要掌握的 Koa 洋蔥模型和中間件
大家好,我是前端西瓜哥。
Koa 是一個 nodejs 框架,經常用于寫 web 后端服務。它是 Express 框架的原班人馬開發的新一代 web 框架,使用了 async / await 來優雅處理無處不在的異步邏輯。
我們常說 Koa 其實是洋蔥模型,今天就來深挖下 Koa 的洋蔥模型到底是什么。
什么是洋蔥模型
在這之前,我們先簡單看看 Koa 是如何使用的。
在 Koa 中,我們通過 app.use 方法注冊中間件。中間件可以注冊多個,它們的執行順序和注冊時機相關,先注冊的先執行。
所謂中間件就是一個函數,這個函數接受 Koa 提供的兩個參數:
- ctx 上下文對象;
- next 函數。
ctx 上有各種參數,比如請求對象 request 和響應對象 response。
調用 next 函數會執行下一個的中間件,如果你不調用 next 函數,那下一個中間件就不會執行。
我們看一個例子:
當請求時,服務器的日志是這樣的:
從中間件 1 睡覺來看來說,代碼邏輯可以分為三部分:
- 先執行next() 。
- 然后執行next() 其后的中間件 2 的所有代碼。
- 最后執行next() 后面的代碼。
這種先執行了當前中間件的前半部分邏輯,然后處理完之后的中間件后,最后繼續執行當前中間件的后半部分的特性,可以讓我們可以像洋蔥一樣,從外到內先處理 request 對象,再從內到外處理 response 對象,于是被稱為 洋蔥模型。
洋蔥模式本質是設計模式中的 職責鏈模式 的變體。
職責鏈模式,指的是將請求和響應解耦,讓多個處理對象有機會依此去處理請求。比如處理對象 A 先處理數據,然后將處理后的數據傳給處理對象 B,依此類推形成了一條鏈。鏈條上的不同處理對象負責各自的職責。
相比經典的職責鏈模式,洋蔥模型可以將一個處理器分成兩個部分,在不同時機觸發但卻擁有相同的上下文,在一些情況下是非常好用的,就比如剛剛提到的打印單個請求花費時長。
Koa 中的源碼實現
Koa 是一個非常輕量的庫,源碼分析起來相對比較容易,所以我們來看看它的洋蔥模型,也就是中間件模型的實現吧。不過因為用到了大量閉包的實現,看起來還是容易眼花的。
首先通過 new Koa() 創建的一個 app 對象,它有一個成員屬性 middleware,初始值為空數組。這個 middleware 就是保存中間件函數的地方。
每當我們調用 app.use(fn) 時,Koa 會將中間件函數加到 middleware。
最后我們調用 app.listen(port),這個 API 會啟動 http 服務器。
this.callback 是一個函數,會返回一個封裝好的函數給 nodejs 原生的 http.createServer 使用。
callback 實現為:
再看看這個 this.compose 方法,它將多個中間件函數進行組合,讓它們可以依此被調用。這個 compose 被抽成一個名為 koa-compose 的 npm 包,里面的代碼很少,我將其中的核心代碼抽出來:
compose 函數的作用是將中間件函數進行組裝,先返回第一個中間件函數的封裝,其類型簽名為 () => Promise<any>。
當這個被封裝的函數被執行時,它會將執行原始的中間件函數,并拿到下一個中間件函數的封裝,也就是 next。
我們回到 callback 方法中,其中 this.handleRequest 的實現為:
this.handleRequest 將 compose 返回的第一個中間件,進行調用。
Express 是洋蔥模型嗎?
Express 發布時,ES6 還沒出來,不能使用 Promise,更不用說 ES7 的 async / await 了。
所以 Express 在當時情況下,其實并沒有能力實現這種支持異步的洋蔥模型,那時候要做異步就只能使用回調的風格。
總的來說,Express 也算是一種 只支持同步的洋蔥模型,因為它在實現上沒有處理 next 是 async 的情況,這是歷史原因導致的。
Express 是在調用 res.send 時,結束數據的處理,返回響應數據給客戶的。在一個請求里不能多次調用 res.rend。
Koa 是給 ctx.response 上加內容,等到中間件走完才返回數據。
結尾
洋蔥模型,就是將數據順序傳入到多個中間件中,讓它們進行處理傳遞,并利用函數遞歸的特性,讓我們可以在一個中間件內先執行前半部分邏輯,再執行之后的所有中間件的完整邏輯后,再掉轉方向繼續執行這個中間件的后半部分。
相比一旦進入下個中間件后再不回來,這種實現可以讓我們的代碼更靈活,在一些場景下很有用。