koa2中間件洋蔥模型理解

中間件概念在編程中使用廣泛, 不管是前端還是后端, 在實(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ò)的真正原因是如下圖:


image.png

在第 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子撕贞,更是在濱河造成了極大的恐慌测垛,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件号涯,死亡現(xiàn)場(chǎng)離奇詭異锯七,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)眉尸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)效五,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人畏妖,你說(shuō)我怎么就攤上這事〗浣伲” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵巫橄,是天一觀的道長(zhǎng)茵典。 經(jīng)常有香客問(wèn)我,道長(zhǎng)彩倚,這世上最難降的妖魔是什么扶平? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮哥谷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘们妥。我一直安慰自己,他們只是感情好王悍,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布压储。 她就那樣靜靜地躺著源譬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪踩娘。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,457評(píng)論 1 311
  • 那天雷绢,我揣著相機(jī)與錄音理卑,去河邊找鬼。 笑死帆疟,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的踪宠。 我是一名探鬼主播妈嘹,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼染厅!你這毒婦竟也來(lái)了津函?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤涩馆,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后魂那,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鲜结,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年活逆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片怒允。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡锈遥,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出丽惶,到底是詐尸還是另有隱情,我是刑警寧澤蚊夫,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布懦尝,位于F島的核電站,受9級(jí)特大地震影響陵霉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜乍桂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一效床、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧憋沿,春花似錦、人聲如沸辐啄。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至抵怎,卻和暖如春岭参,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背冗荸。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工利耍, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人程癌。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓轴猎,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親捻脖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360