koa-compose源碼閱讀

眾所周知,在函數(shù)式編程中,compose是將多個(gè)函數(shù)合并成一個(gè)函數(shù)(形如: g() + h() => g(h()))腻扇,koa-compose則是將 koa/koa-router 各個(gè)中間件合并執(zhí)行,結(jié)合 next() 就形成了洋蔥式模型植捎。

image

洋蔥模型執(zhí)行順序

我們創(chuàng)建koa應(yīng)用如下:

const koa = require('koa');
const app = new koa();
app.use((ctx, next) => {
 console.log('第一個(gè)中間件函數(shù)')
 await next();
 console.log('第一個(gè)中間件函數(shù)next之后');
})
app.use(async (ctx, next) => {
 console.log('第二個(gè)中間件函數(shù)')
 await next();
 console.log('第二個(gè)中間件函數(shù)next之后');
})
app.use(ctx => {
 console.log('響應(yīng)');
 ctx.body = 'hello'
})
?
app.listen(3000)

以上代碼衙解,可以使用node text-next.js啟動(dòng)阳柔,啟動(dòng)后可以在瀏覽器中訪問(wèn)http://localhost:3000/

訪問(wèn)后焰枢,會(huì)在啟動(dòng)的命令窗口中打印出如下值:

第一個(gè)中間件函數(shù)
第二個(gè)中間件函數(shù)
響應(yīng)
第二個(gè)中間件函數(shù)next之后
第一個(gè)中間件函數(shù)next之后

注意:在使用app.use將給定的中間件添加到應(yīng)用程序時(shí),中間件(其實(shí)就是一個(gè)函數(shù))接收兩個(gè)參數(shù):ctx和next舌剂。其中next也是一個(gè)函數(shù)济锄。

koa-compose源碼

再接著深入koa-compose源碼之前,我們先來(lái)看一下霍转,koa源代碼中是怎么調(diào)用compose的荐绝。詳細(xì)參考上一篇文章

listen(...args) {
   debug('listen');
   const server = http.createServer(this.callback());
   return server.listen(...args);
}
?
callback() {
   // 這里調(diào)用的compose的函數(shù)避消,返回值是fn
   const fn = compose(this.middleware);
?
   if (!this.listenerCount('error')) this.on('error', this.onerror);
?
   const handleRequest = (req, res) => {
   const ctx = this.createContext(req, res); // 創(chuàng)建ctx對(duì)象
   return this.handleRequest(ctx, fn);  // 將fn傳遞給了this.handleRequest
 };
?
 return handleRequest;
}
?
handleRequest(ctx, fnMiddleware) {
   const res = ctx.res;
   res.statusCode = 404;
   onFinished(res, onerror);
   // 在這里低滩,看到以下fnMiddleware().then().catch()寫(xiě)法.
   // 我們大膽猜測(cè)compose函數(shù)的返回值是一個(gè)function。而且該function的返回值是一個(gè)promise對(duì)象岩喷。
   // 待下文源碼驗(yàn)證恕沫。
   return fnMiddleware(ctx)
   .then(() => respond(ctx))
   .catch(err => ctx.onerror(err));
}

callback函數(shù)是在app.listen時(shí)執(zhí)行的,也就是在app.listen時(shí)利用 Node 原生的http 模塊建立http server纱意,并在創(chuàng)建server的時(shí)候婶溯,處理中間件邏輯。

好偷霉,現(xiàn)在我們已經(jīng)知道了koa是怎么調(diào)用compose的迄委,接下來(lái),看koa-compose源代碼类少。koa-compose 的代碼只有不夠50行叙身,細(xì)讀確實(shí)是一段很精妙的代碼,而實(shí)際核心代碼則是這一段:

module.exports = compose
?
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 的引用硫狞。
 // 這里也驗(yàn)證了上文的猜測(cè):compose函數(shù)的返回值是一個(gè)function.
 // 而且看下文可知曲梗,該函數(shù)的返回值是promise對(duì)象。進(jìn)一步驗(yàn)證了上文的猜測(cè)妓忍。
 return function (context, next) {
   let index = -1
   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
     if (!fn) return Promise.resolve()
     try {
       return Promise.resolve(fn(context, function next () {
         return dispatch(i + 1)
       }))
     } catch (err) {
       return Promise.reject(err)
     }
   }
  }
}

雖然短虏两,但是之中使用了4層 return,初看會(huì)比較繞世剖,我們只看第3定罢,4層 return,這是返回實(shí)際的執(zhí)行代碼鏈旁瘫。

return Promise.resolve(fn(context, function next () {
   return dispatch(i + 1)
}))

fn = middleware[i]也就是某一個(gè)中間件祖凫,很顯然上述代碼遍歷中間件數(shù)組middleware琼蚯,依次拿到中間件fn,并執(zhí)行:

fn(context, function next () {
   return dispatch(i + 1)
})

這里可以看到傳遞給中間件的兩個(gè)參數(shù):context和next函數(shù)惠况。

前文提到過(guò):在使用app.use將給定的中間件添加到應(yīng)用程序時(shí)遭庶,中間件(其實(shí)就是一個(gè)函數(shù))接收兩個(gè)參數(shù):ctx和next。其中next也是一個(gè)函數(shù)稠屠。

看到這里是不是明白了峦睡,在注冊(cè)中間件的時(shí)候?yàn)槭裁匆袃蓚€(gè)參數(shù)了吶!Hú骸榨了!

接下來(lái),我們繼續(xù)研究洋蔥模型到底是怎么回事兒攘蔽。 比如前文例子中的第一個(gè)中間件:

app.use((ctx, next) => {
 console.log('第一個(gè)中間件函數(shù)')
 await next();
 console.log('第一個(gè)中間件函數(shù)next之后');
})
  • 第一次龙屉,此時(shí)第一個(gè)中間件被調(diào)用,dispatch(0)满俗,展開(kāi):
Promise.resolve(((ctx, next) => {
   console.log('第一個(gè)中間件函數(shù)')
   await next();
   console.log('第一個(gè)中間件函數(shù)next之后');
})(context, function next () {
   return dispatch(i + 1)
})));

首先執(zhí)行console.log('第一個(gè)中間件函數(shù)')转捕,打出來(lái)log沒(méi)毛病。

接下來(lái)注意了老鐵唆垃!注意了老鐵五芝!注意了老鐵!重要的事情說(shuō)三遍降盹。在執(zhí)行到await next();的時(shí)候与柑,return dispatch(i + 1)

瞅一眼上文中的dispatch函數(shù)蓄坏,你就能知道价捧,這是遞歸到了第二個(gè)中間件啊,也就是說(shuō)壓根就沒(méi)執(zhí)行第二個(gè)log即:console.log('第一個(gè)中間件函數(shù)next之后');涡戳,就跑到了第二個(gè)中間件结蟋。

  • 第二次,此時(shí)第二個(gè)中間件被調(diào)用渔彰,dispatch(1)嵌屎,展開(kāi):
Promise.resolve((ctx, next) => Promise.resolve((ctx, next) => s{
 console.log('第一個(gè)中間件函數(shù)')
 await Promise.resolve(((ctx, next) => {
   console.log('第二個(gè)中間件函數(shù)')
   await next();
   console.log('第二個(gè)中間件函數(shù)next之后');
 })(context, function next () {
   return dispatch(i + 1)
 })));
 console.log('第一個(gè)中間件函數(shù)next之后');
});

接下來(lái)的事情,想必你們都猜到了恍涂,在第二個(gè)中間件執(zhí)行到await next();時(shí)宝惰,同樣會(huì)輪轉(zhuǎn)到第三個(gè)中間件,以此類(lèi)推再沧,直到最后一個(gè)中間件尼夺。

總結(jié)

中間件模型非常好用并且簡(jiǎn)潔, 甚至在 koa 框架上大放異彩, 但是也有自身的缺陷, 也就是一旦中間件數(shù)組過(guò)于龐大, 性能會(huì)有所下降, 因此我們需要結(jié)合自身的情況與業(yè)務(wù)場(chǎng)景作出最合適的選擇.

參考文章:

koa 源碼解析
koa-用到的delegates NPM包詳解
redux, koa, express 中間件實(shí)現(xiàn)對(duì)比解析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子淤堵,更是在濱河造成了極大的恐慌寝衫,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,681評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拐邪,死亡現(xiàn)場(chǎng)離奇詭異慰毅,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)扎阶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)汹胃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人乘陪,你說(shuō)我怎么就攤上這事统台〉窭蓿” “怎么了啡邑?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,421評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)井赌。 經(jīng)常有香客問(wèn)我谤逼,道長(zhǎng),這世上最難降的妖魔是什么仇穗? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,114評(píng)論 1 300
  • 正文 為了忘掉前任流部,我火速辦了婚禮,結(jié)果婚禮上纹坐,老公的妹妹穿的比我還像新娘枝冀。我一直安慰自己,他們只是感情好耘子,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布果漾。 她就那樣靜靜地躺著,像睡著了一般谷誓。 火紅的嫁衣襯著肌膚如雪绒障。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,713評(píng)論 1 312
  • 那天捍歪,我揣著相機(jī)與錄音户辱,去河邊找鬼。 笑死糙臼,一個(gè)胖子當(dāng)著我的面吹牛庐镐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播变逃,決...
    沈念sama閱讀 41,170評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼必逆,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起末患,我...
    開(kāi)封第一講書(shū)人閱讀 40,116評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤研叫,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后璧针,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體嚷炉,經(jīng)...
    沈念sama閱讀 46,651評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評(píng)論 3 342
  • 正文 我和宋清朗相戀三年探橱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了申屹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,865評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡隧膏,死狀恐怖哗讥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情胞枕,我是刑警寧澤杆煞,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站腐泻,受9級(jí)特大地震影響决乎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜派桩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評(píng)論 3 336
  • 文/蒙蒙 一构诚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧铆惑,春花似錦范嘱、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,699評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至逆趋,卻和暖如春盏阶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背闻书。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,814評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工名斟, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人魄眉。 一個(gè)月前我還...
    沈念sama閱讀 49,299評(píng)論 3 379
  • 正文 我出身青樓砰盐,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親坑律。 傳聞我的和親對(duì)象是個(gè)殘疾皇子岩梳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容