中間件概念在編程中使用廣泛, 不管是前端還是后端, 在實(shí)際編程中或者框架設(shè)計(jì)都有使用到這種實(shí)用的模型, 下面我們就來(lái)談?wù)勊淖饔?
面向切面編程(AOP)
相信很多人都聽(tīng)過(guò)所謂的 AOP
編程或者面向切面編程, 其實(shí)他們都是中間件模型的體現(xiàn), 我舉個(gè)例子, 在前端開(kāi)發(fā)中, 產(chǎn)品會(huì)要求在代碼中進(jìn)行埋點(diǎn), 比如
需要知道這個(gè)按鈕用戶點(diǎn)擊的頻率是多少, 但是這樣的上報(bào)代碼其實(shí)與實(shí)際的業(yè)務(wù)代碼并無(wú)強(qiáng)關(guān)聯(lián), 更不要說(shuō)在實(shí)際上業(yè)務(wù)代碼已經(jīng)封裝成一個(gè)通用的函數(shù)或組件,
所以, 如果想不侵入業(yè)務(wù)代碼而又滿足埋點(diǎn), 中間件模型或許能夠滿足需求, 來(lái)看一看簡(jiǎn)單的代碼:
// 在原函數(shù)執(zhí)行前執(zhí)行 fn 函數(shù)
Function.prorotype.before = function (fn) {
// 保存觸發(fā) before 的函數(shù)
const self = this;
return function (...args) {
let res = fn.call(this);
// 如果上一個(gè)函數(shù)未返回值, 不執(zhí)行下一個(gè)函數(shù)
if(res) {
self.apply(this, args);
}
}
}
// 在原函數(shù)執(zhí)行后執(zhí)行 fn 函數(shù)
Function.prototype.after = function (fn) {
// 保存觸發(fā) after 的函數(shù)
const self = this;
return function (...args) {
let res = self.apply(this, args);
// 如果上一個(gè)函數(shù)未返回值, 不執(zhí)行下一個(gè)函數(shù)
if(res) {
fn.call(this);
}
}
}
上面這兩個(gè)函數(shù)是通過(guò)在 Function.prototype 上添加兩個(gè)函數(shù): before, after. 兩個(gè)函數(shù)的返回值都是一個(gè)函數(shù), 這個(gè)函數(shù)會(huì)按照次序執(zhí)行函數(shù).
這樣函數(shù)各自保持了他們的整潔性.但是這樣的 before 與 after 函數(shù)的簡(jiǎn)單使用缺陷也是很明顯的, 他們并不支持異步的函數(shù), 而日常開(kāi)發(fā)中異步的場(chǎng)景有非常多, 所以這樣的代碼還是只能在 demo 中使用,
不適合生產(chǎn)環(huán)境中使用.所以我們來(lái)看一下 koa 框架是怎么做的.
koa 中的中間件
koa 是 nodejs 中非常精簡(jiǎn)的框架, 其中的精粹思想就是洋蔥模型(中間件模型), 它實(shí)現(xiàn)的核心就是借助 compose 這個(gè)庫(kù)來(lái)實(shí)現(xiàn)的.這里我主要看的是 koa2 所使用的 compose 源碼,
對(duì)于 koa1 的 compose 源碼其實(shí)思想是一致的, 只不過(guò)它針對(duì)的是 generator 函數(shù), koa2 針對(duì)的是 async 函數(shù), 相比之下 async 會(huì)更符合潮流.
對(duì)于 compose 也就是 koa 的核心思想就是像下面這個(gè)圖:
那么 compose 是怎么實(shí)現(xiàn)上面這個(gè)思想的呢?
下面我們來(lái)解讀一下 compose 的源碼, compose 的源碼非常精簡(jiǎn),
middleware in koa1
對(duì)于 koa1 來(lái)說(shuō), 它是基于 generator 函數(shù)與 co 類庫(kù)的:
function compose(middleware){
return function *(next){
// 解釋一下傳入的 next, 這個(gè)傳入的 next 函數(shù)是在所有中間件執(zhí)行后的"最后"一個(gè)函數(shù), 這里的"最后"并不是真正的最后,
// 而是像上面那個(gè)圖中的圓心, 執(zhí)行完圓心之后, 會(huì)返回去執(zhí)行上一個(gè)中間件函數(shù)(middleware[length - 1])剩下的邏輯
// 簡(jiǎn)稱圓心函數(shù)
// 如果沒(méi)有傳入那就就賦值為一個(gè)空函數(shù)
if (!next) next = noop();
var i = middleware.length;
// 從后往前加載中間件
while (i--) {
// 將后面一個(gè)函數(shù)傳給前面的函數(shù)作為 next 函數(shù), 前面函數(shù)中的 next 參數(shù)其實(shí)就是下一個(gè)中間件函數(shù)
next = middleware[i].call(this, next);
// 這里可以知道 next 函數(shù)都是 generator 函數(shù)
console.log('isGenerator:', (typeof next.next === 'function' && typeof next.throw === 'function')); // true
}
// 使用 yield 委托執(zhí)行生成器函數(shù)
return yield *next;
}
}
function *noop(){}
解釋一下 koa1 中的 compose 為什么從后往前遍歷中間件函數(shù)而且還使用了 call 函數(shù)執(zhí)行了一次, 這個(gè)是因?yàn)?koa1 中默認(rèn)函數(shù)都是生成器函數(shù), 我們知道生成器函數(shù)
執(zhí)行一次并不是真正地執(zhí)行了函數(shù)內(nèi)部的邏輯, 而是初始化得到一個(gè)生成器對(duì)象, 而在生成器對(duì)象生成的時(shí)候, 我們需要對(duì)函數(shù)需要的 next 函數(shù)進(jìn)行傳值, 所以會(huì)采用逆序遍歷.
middleware in koa2
對(duì)于 koa2 來(lái)說(shuō)中間件機(jī)制 compose 基于 async 與 Promise: 會(huì)稍微比 koa1 中的復(fù)雜一點(diǎn)
function compose (middleware) {
// 傳入的 middleware 參數(shù)必須是數(shù)組
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
// middleware 數(shù)組的元素必須是函數(shù)
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
// 返回一個(gè)函數(shù)閉包, 保持對(duì) middleware 的引用
return function (context, next) {
// 這里的 context 參數(shù)是作為一個(gè)全局的設(shè)置, 所有中間件的第一個(gè)參數(shù)就是傳入的 context, 這樣可以
// 在 context 中對(duì)某個(gè)值或者某些值做"洋蔥處理"
// 解釋一下傳入的 next, 這個(gè)傳入的 next 函數(shù)是在所有中間件執(zhí)行后的"最后"一個(gè)函數(shù), 這里的"最后"并不是真正的最后,
// 而是像上面那個(gè)圖中的圓心, 執(zhí)行完圓心之后, 會(huì)返回去執(zhí)行上一個(gè)中間件函數(shù)(middleware[length - 1])剩下的邏輯
// index 是用來(lái)記錄中間件函數(shù)運(yùn)行到了哪一個(gè)函數(shù)
let index = -1
// 執(zhí)行第一個(gè)中間件函數(shù)
return dispatch(0)
function dispatch (i) {
// i 是洋蔥模型的記錄已經(jīng)運(yùn)行的函數(shù)中間件的下標(biāo), 如果一個(gè)中間件里面運(yùn)行兩次 next, 那么 i 是會(huì)比 index 小的.
// 如果對(duì)這個(gè)地方不清楚可以查看下面的圖
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) {
// 這里的 next 就是一開(kāi)始 compose 傳入的 next, 意味著當(dāng)中間件函數(shù)數(shù)列執(zhí)行完后, 執(zhí)行這個(gè) next 函數(shù), 即圓心
fn = next
}
// 如果沒(méi)有函數(shù), 直接返回空值的 Promise
if (!fn) return Promise.resolve()
try {
// 為什么這里要包一層 Promise?
// 因?yàn)?async 需要后面是 Promise, 然后 next 函數(shù)返回值就是 dispatch 函數(shù)的返回值, 所以運(yùn)行 async next(); 需要 next 包一層 Promise
// next 函數(shù)是固定的, 可以執(zhí)行下一個(gè)函數(shù)
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
至于在一個(gè)中間件函數(shù)中兩次調(diào)用 next 函數(shù)導(dǎo)致出錯(cuò), 我這里提供一個(gè)簡(jiǎn)單的例子供大家參考:
async function first(ctx, next) {
console.log('1');
// async 與 co + yield 的模型不同, await 是需要后面是 promise 的函數(shù), 并且自己執(zhí)行一次, 而 co 是自己拿到 value 然后幫你自動(dòng)執(zhí)行.
await next();
await next(); // 兩次調(diào)用 next
console.log(ctx);
};
async function second(ctx, next) {
console.log('2');
await next();
};
async function third(ctx, next) {
console.log('3');
await next();
console.log('4');
};
const middleware = [first, second, third];
const com = compose(middleware);
com('ctx', function() {
console.log('hey');
});
如果第一個(gè)中間件中沒(méi)有兩次調(diào)用 next 函數(shù), 那么正確的結(jié)果為 1 2 3 'hey' 4 'ctx'. 對(duì)于出錯(cuò)的真正原因是如下圖:
在第 5 步中, 傳入的 i 值為 1, 因?yàn)檫€是在第一個(gè)中間件函數(shù)內(nèi)部, 但是 compose 內(nèi)部的 index 已經(jīng)是 3 了, 所以 i < 3, 所以報(bào)錯(cuò)了, 可知在一個(gè)中間件函數(shù)內(nèi)部不允許多次調(diào)用 next 函數(shù).
總結(jié)
中間件模型非常好用并且簡(jiǎn)潔, 甚至在 koa 框架上大放異彩, 但是也有自身的缺陷, 也就是一旦中間件數(shù)組過(guò)于龐大, 性能會(huì)有所下降, 因此我們需要結(jié)合自身的情況與業(yè)務(wù)場(chǎng)景作出最合適的選擇.
參考
轉(zhuǎn)自: https://github.com/zhangxiang958/zhangxiang958.github.io/issues/34