本文將講解 koa 的洋蔥模型,我們?yōu)槭裁匆褂醚笫[模型,以及它的原理實(shí)現(xiàn)。掌握洋蔥模型對于理解 koa 至關(guān)重要,希望本文對你有所幫助~
什么是洋蔥模型
先來看一個 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`);
});
輸出的結(jié)果是:
1
3
4
2
在 koa 中,中間件被 next() 方法分成了兩部分。next() 方法上面部分會先執(zhí)行,下面部門會在后續(xù)中間件執(zhí)行全部結(jié)束之后再執(zhí)行??梢酝ㄟ^下圖直觀看出:

在洋蔥模型中,每一層相當(dāng)于一個中間件,用來處理特定的功能,比如錯誤處理、Session 處理等等。其處理順序先是 next() 前請求(Request,從外層到內(nèi)層)然后執(zhí)行 next() 函數(shù),最后是 next() 后響應(yīng)(Response,從內(nèi)層到外層),也就是說每一個中間件都有兩次處理時機(jī)。

為什么 Koa 使用洋蔥模型
假如不是洋蔥模型,我們中間件依賴于其他中間件的邏輯的話,我們要怎么處理?
比如,我們需要知道一個請求或者操作 db 的耗時是多少,而且想獲取其他中間件的信息。在 koa 中,我們可以使用 async await 的方式結(jié)合洋蔥模型做到。
app.use(async(ctx, next) => {
const start = new Date();
await next();
const delta = new Date() - start;
console.log (`請求耗時: ${delta} MS`);
console.log('拿到上一次請求的結(jié)果:', ctx.state.baiduHTML);
})
app.use(async(ctx, next) => {
// 處理 db 或者進(jìn)行 HTTP 請求
ctx.state.baiduHTML = await axios.get('http://baidu.com');
})

而假如沒有洋蔥模型,這是做不到的。
深入 Koa 洋蔥模型
我們以文章開始時候的 demo 來分析一下 koa 內(nèi)部的實(shí)現(xiàn)。
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 方法就是做了一件事,維護(hù)得到 middleware 中間件數(shù)組
use(fn) {
// ...
// 維護(hù)中間件數(shù)組——middleware
this.middleware.push(fn);
return this;
}
listen 方法 和 callback 方法
執(zhí)行 app.listen 方法的時候,其實(shí)是 Node.js 原生 http 模塊 createServer 方法創(chuàng)建了一個服務(wù),其回調(diào)為 callback 方法。callback 方法中就有我們今天的重點(diǎn) compose 函數(shù),它的返回是一個 Promise 函數(shù)。
listen(...args) {
debug('listen');
// node http 創(chuàng)建一個服務(wù)
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback() {
// 返回值是一個函數(shù)
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
// 創(chuàng)建 ctx 上下文環(huán)境
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest 中會執(zhí)行 compose 函數(shù)中返回的 Promise 函數(shù)并返回結(jié)果。
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// 執(zhí)行 compose 中返回的函數(shù),將結(jié)果返回
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
koa-compose
compose 函數(shù)引用的是 koa-compose 這個庫。其實(shí)現(xiàn)如下所示:
function compose (middleware) {
// ...
return function (context, next) {
// last called middleware #
let index = -1
// 一開始的時候傳入為 0,后續(xù)會遞增
return dispatch(0)
function dispatch (i) {
// 假如沒有遞增,則說明執(zhí)行了多次
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 拿到當(dāng)前的中間件
let fn = middleware[i]
if (i === middleware.length) fn = next
// 當(dāng) fn 為空的時候,就會開始執(zhí)行 next() 后面部分的代碼
if (!fn) return Promise.resolve()
try {
// 執(zhí)行中間件,留意這兩個參數(shù),都是中間件的傳參,第一個是上下文,第二個是 next 函數(shù)
// 也就是說執(zhí)行 next 的時候也就是調(diào)用 dispatch 函數(shù)的時候
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
代碼很簡單,我們來看看具體的執(zhí)行流程是怎樣的:
當(dāng)我們執(zhí)行第一次的時候,調(diào)用的是 dispatch(0),這個時候 i 為 0,fn 為第一個中間件函數(shù)。并執(zhí)行中間件,留意這兩個參數(shù),都是中間件的傳參,第一個是上下文,第二個是 next 函數(shù)。也就是說中間件執(zhí)行 next 的時候也就是調(diào)用 dispatch 函數(shù)的時候,這就是為什么執(zhí)行 next 邏輯的時候就會執(zhí)行下一個中間件的原因:
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
當(dāng)?shù)诙?、第三次?zhí)行 dispatch 的時候,跟第一次一樣,分別開始執(zhí)行第二、第三個中間件,執(zhí)行 next() 的時候開始執(zhí)行下一個中間件。
當(dāng)執(zhí)行到第三個中間件的時候,執(zhí)行到 next() 的時候,dispatch 函數(shù)傳入的參數(shù)是 3,fn 為 undefined。這個時候就會執(zhí)行
if (!fn) return Promise.resolve()
這個時候就會執(zhí)行第三個中間件 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();
總結(jié)
Koa 的洋蔥模型指的是以 next() 函數(shù)為分割點(diǎn),先由外到內(nèi)執(zhí)行 Request 的邏輯,再由內(nèi)到外執(zhí)行 Response 的邏輯。通過洋蔥模型,將多個中間件之間通信等變得更加可行和簡單。其實(shí)現(xiàn)的原理并不是很復(fù)雜,主要是 compose 方法。