【Node】深入淺出 Koa 的洋蔥模型
本文將講解 koa
的洋蔥模型,我們爲什麼要使用洋蔥模型,以及它的原理實現。掌握洋蔥模型對於理解 koa
至關重要,希望本文對你有所幫助~
什麼是洋蔥模型
先來看一個 demo
const Koa = require('koa');
const app = new Koa();
// 中間件1
app.use((ctx, next) => {
console.log(1);
next();
console.log(2);
});
// 中間件 2
app.use((ctx, next) => {
console.log(3);
next();
console.log(4);
});
app.listen(8000, '0.0.0.0', () => {
console.log(`Server is starting`);
});
輸出的結果是:
1
3
4
2
在 koa
中,中間件被 next()
方法分成了兩部分。next()
方法上面部分會先執行,下面部門會在後續中間件執行全部結束之後再執行。可以通過下圖直觀看出:
在洋蔥模型中,每一層相當於一箇中間件,用來處理特定的功能,比如錯誤處理、Session
處理等等。其處理順序先是 next()
前請求(Request
,從外層到內層)然後執行 next()
函數,最後是 next()
後響應(Response
,從內層到外層),也就是說每一箇中間件都有兩次處理時機。
爲什麼 Koa 使用洋蔥模型
假如不是洋蔥模型,我們中間件依賴於其他中間件的邏輯的話,我們要怎麼處理?
比如,我們需要知道一個請求或者操作 db
的耗時是多少,而且想獲取其他中間件的信息。在 koa
中,我們可以使用 async await
的方式結合洋蔥模型做到。
app.use(async(ctx, next) => {
const start = new Date();
await next();
const delta = new Date() - start;
console.log (`請求耗時: ${delta} MS`);
console.log('拿到上一次請求的結果:', ctx.state.baiduHTML);
})
app.use(async(ctx, next) => {
// 處理 db 或者進行 HTTP 請求
ctx.state.baiduHTML = await axios.get('http://baidu.com');
})
而假如沒有洋蔥模型,這是做不到的。
深入 Koa 洋蔥模型
我們以文章開始時候的 demo
來分析一下 koa
內部的實現。
const Koa = require('koa');
//Applications
const app = new Koa();
// 中間件1
app.use((ctx, next) => {
console.log(1);
next();
console.log(2);
});
// 中間件 2
app.use((ctx, next) => {
console.log(3);
next();
console.log(4);
});
app.listen(9000, '0.0.0.0', () => {
console.log(`Server is starting`);
});
use 方法
use
方法就是做了一件事,維護得到 middleware
中間件數組
use(fn) {
// ...
// 維護中間件數組——middleware
this.middleware.push(fn);
return this;
}
listen 方法 和 callback 方法
執行 app.listen
方法的時候,其實是 Node.js
原生 http
模塊 createServer
方法創建了一個服務,其回調爲 callback
方法。callback
方法中就有我們今天的重點 compose
函數,它的返回是一個 Promise
函數。
listen(...args) {
debug('listen');
// node http 創建一個服務
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback() {
// 返回值是一個函數
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
// 創建 ctx 上下文環境
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest
中會執行 compose
函數中返回的 Promise
函數並返回結果。
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// 執行 compose 中返回的函數,將結果返回
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
koa-compose
compose
函數引用的是 koa-compose
這個庫。其實現如下所示:
function compose (middleware) {
// ...
return function (context, next) {
// last called middleware #
let index = -1
// 一開始的時候傳入爲 0,後續會遞增
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
// 當 fn 爲空的時候,就會開始執行 next() 後面部分的代碼
if (!fn) return Promise.resolve()
try {
// 執行中間件,留意這兩個參數,都是中間件的傳參,第一個是上下文,第二個是 next 函數
// 也就是說執行 next 的時候也就是調用 dispatch 函數的時候
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
代碼很簡單,我們來看看具體的執行流程是怎樣的:
當我們執行第一次的時候,調用的是 dispatch(0)
,這個時候 i 爲 0,fn
爲第一個中間件函數。並執行中間件,留意這兩個參數,都是中間件的傳參,第一個是上下文,第二個是 next
函數。也就是說中間件執行 next 的時候也就是調用 dispatch 函數的時候,這就是爲什麼執行 next
邏輯的時候就會執行下一個中間件的原因:
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
當第二、第三次執行 dispatch
的時候,跟第一次一樣,分別開始執行第二、第三個中間件,執行 next()
的時候開始執行下一個中間件。
當執行到第三個中間件的時候,執行到 next()
的時候,dispatch
函數傳入的參數是 3,fn
爲 undefined
。這個時候就會執行
if (!fn) return Promise.resolve()
這個時候就會執行第三個中間件 next()
之後的代碼,然後是第二個、第一個,從而形成了洋蔥模型。
其過程如下所示:
簡易版 compose
模範 koa
的邏輯,我們可以寫一個簡易版的 compose
。方便大家的理解:
const middleware = []
let mw1 = async function (ctx, next) {
console.log("next前,第一個中間件")
await next()
console.log("next後,第一個中間件")
}
let mw2 = async function (ctx, next) {
console.log("next前,第二個中間件")
await next()
console.log("next後,第二個中間件")
}
let mw3 = async function (ctx, next) {
console.log("第三個中間件,沒有next了")
}
function use(mw) {
middleware.push(mw);
}
function compose(middleware) {
return (ctx, next) => {
return dispatch(0);
function dispatch(i) {
const fn = middleware[i];
if (!fn) return;
return fn(ctx, dispatch.bind(null, i+1));
}
}
}
use(mw1);
use(mw2);
use(mw3);
const fn = compose(middleware);
fn();
總結
Koa
的洋蔥模型指的是以 next()
函數爲分割點,先由外到內執行 Request
的邏輯,再由內到外執行 Response
的邏輯。通過洋蔥模型,將多箇中間件之間通信等變得更加可行和簡單。其實現的原理並不是很複雜,主要是 compose
方法。
參考
-
Talk about koa’s onion model[1]
-
如何更好地理解中間件和洋蔥模型 [2]
參考資料
[1]
Talk about koa’s onion model: https://developpaper.com/talk-about-koas-onion-model/
[2]
如何更好地理解中間件和洋蔥模型: https://juejin.cn/post/6890259747866411022
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/HNX1cOBIRSrDEhItUe4swA