50行代码串行Promise,koa洋葱模型原来这么有趣?
1. 前言
大家好,我是若川,最近組織了源碼共讀活動(dòng)《1個(gè)月,200+人,一起讀了4周源碼》,感興趣的可以加我微信 ruochuan12 參與,長(zhǎng)期交流學(xué)習(xí)。
之前寫的《學(xué)習(xí)源碼整體架構(gòu)系列》 包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4十余篇源碼文章。其中最新的兩篇是:
Vue 3.2 發(fā)布了,那尤雨溪是怎么發(fā)布 Vue.js 的?
初學(xué)者也能看懂的 Vue3 源碼中那些實(shí)用的基礎(chǔ)工具函數(shù)
寫相對(duì)很難的源碼,耗費(fèi)了自己的時(shí)間和精力,也沒(méi)收獲多少閱讀點(diǎn)贊,其實(shí)是一件挺受打擊的事情。從閱讀量和讀者受益方面來(lái)看,不能促進(jìn)作者持續(xù)輸出文章。
所以轉(zhuǎn)變思路,寫一些相對(duì)通俗易懂的文章。其實(shí)源碼也不是想象的那么難,至少有很多看得懂。
之前寫過(guò) koa 源碼文章學(xué)習(xí) koa 源碼的整體架構(gòu),淺析koa洋蔥模型原理和co原理比較長(zhǎng),讀者朋友大概率看不完,所以本文從koa-compose50行源碼講述。
本文涉及到的 koa-compose 倉(cāng)庫(kù)[1] 文件,整個(gè)index.js文件代碼行數(shù)雖然不到 50 行,而且測(cè)試用例test/test.js文件 300 余行,但非常值得我們學(xué)習(xí)。
歌德曾說(shuō):讀一本好書,就是在和高尚的人談話。同理可得:讀源碼,也算是和作者的一種學(xué)習(xí)交流的方式。
閱讀本文,你將學(xué)到:
1.?熟悉?koa-compose?中間件源碼、可以應(yīng)對(duì)面試官相關(guān)問(wèn)題 2.?學(xué)會(huì)使用測(cè)試用例調(diào)試源碼 3.?學(xué)會(huì)?jest?部分用法2. 環(huán)境準(zhǔn)備
2.1 克隆 koa-compose 項(xiàng)目
本文倉(cāng)庫(kù)地址 koa-compose-analysis[2],求個(gè)star~
#?可以直接克隆我的倉(cāng)庫(kù),我的倉(cāng)庫(kù)保留的?compose?倉(cāng)庫(kù)的?git?記錄 git?clone?https://github.com/lxchuan12/koa-compose-analysis.git cd?koa-compose/compose npm?i順帶說(shuō)下:我是怎么保留 compose 倉(cāng)庫(kù)的 git 記錄的。
#?在?github?上新建一個(gè)倉(cāng)庫(kù)?`koa-compose-analysis`?克隆下來(lái) git?clone?https://github.com/lxchuan12/koa-compose-analysis.git cd?koa-compose-analysis git?subtree?add?--prefix=compose?https://github.com/koajs/compose.git?main #?這樣就把 compose 文件夾克隆到自己的 git 倉(cāng)庫(kù)了。且保留的 git 記錄關(guān)于更多 git subtree,可以看這篇文章用 Git Subtree 在多個(gè) Git 項(xiàng)目間雙向同步子項(xiàng)目,附簡(jiǎn)明使用手冊(cè)[3]
接著我們來(lái)看怎么根據(jù)開(kāi)源項(xiàng)目中提供的測(cè)試用例調(diào)試源碼。
2.2 根據(jù)測(cè)試用例調(diào)試 compose 源碼
用VSCode(我的版本是 1.60 )打開(kāi)項(xiàng)目,找到 compose/package.json,找到 scripts 和 test 命令。
//?compose/package.json {"name":?"koa-compose",//?debug?(調(diào)試)"scripts":?{"eslint":?"standard?--fix?.","test":?"jest"}, }在scripts上方應(yīng)該會(huì)有debug或者調(diào)試字樣。點(diǎn)擊debug(調(diào)試),選擇 test。
VSCode 調(diào)試接著會(huì)執(zhí)行測(cè)試用例test/test.js文件。終端輸出如下圖所示。
koa-compose 測(cè)試用例輸出結(jié)果接著我們調(diào)試 compose/test/test.js 文件。我們可以在 45行 打上斷點(diǎn),重新點(diǎn)擊 package.json => srcipts => test 進(jìn)入調(diào)試模式。如下圖所示。
koa-compose 調(diào)試接著按上方的按鈕,繼續(xù)調(diào)試。在compose/index.js文件中關(guān)鍵的地方打上斷點(diǎn),調(diào)試學(xué)習(xí)源碼事半功倍。
更多 nodejs 調(diào)試相關(guān) 可以查看官方文檔[4]
順便提一下幾個(gè)調(diào)試相關(guān)按鈕。
繼續(xù)(F5)
單步跳過(guò)(F10)
單步調(diào)試(F11)
單步跳出(Shift + F11)
重啟(Ctrl + Shift + F5)
斷開(kāi)鏈接(Shift + F5)
接下來(lái),我們跟著測(cè)試用例學(xué)源碼。
3. 跟著測(cè)試用例學(xué)源碼
分享一個(gè)測(cè)試用例小技巧:我們可以在測(cè)試用例處加上only修飾。
//?例如 it.only('should?work',?async?()?=>?{})這樣我們就可以只執(zhí)行當(dāng)前的測(cè)試用例,不關(guān)心其他的,不會(huì)干擾調(diào)試。
3.1 正常流程
打開(kāi) compose/test/test.js 文件,看第一個(gè)測(cè)試用例。
//?compose/test/test.js 'use?strict'/*?eslint-env?jest?*/const?compose?=?require('..') const?assert?=?require('assert')function?wait?(ms)?{return?new?Promise((resolve)?=>?setTimeout(resolve,?ms?||?1)) } //?分組 describe('Koa?Compose',?function?()?{it.only('should?work',?async?()?=>?{const?arr?=?[]const?stack?=?[]stack.push(async?(context,?next)?=>?{arr.push(1)await?wait(1)await?next()await?wait(1)arr.push(6)})stack.push(async?(context,?next)?=>?{arr.push(2)await?wait(1)await?next()await?wait(1)arr.push(5)})stack.push(async?(context,?next)?=>?{arr.push(3)await?wait(1)await?next()await?wait(1)arr.push(4)})await?compose(stack)({})//?最后輸出數(shù)組是?[1,2,3,4,5,6]expect(arr).toEqual(expect.arrayContaining([1,?2,?3,?4,?5,?6]))}) }大概看完這段測(cè)試用例,context是什么,next又是什么。
在`koa`的文檔[5]上有個(gè)非常代表性的中間件 gif 圖。
中間件 gif 圖而compose函數(shù)作用就是把添加進(jìn)中間件數(shù)組的函數(shù)按照上面 gif 圖的順序執(zhí)行。
3.1.1 compose 函數(shù)
簡(jiǎn)單來(lái)說(shuō),compose 函數(shù)主要做了兩件事情。
接收一個(gè)參數(shù),校驗(yàn)參數(shù)是數(shù)組,且校驗(yàn)數(shù)組中的每一項(xiàng)是函數(shù)。
返回一個(gè)函數(shù),這個(gè)函數(shù)接收兩個(gè)參數(shù),分別是context和next,這個(gè)函數(shù)最后返回Promise。
/***?Compose?`middleware`?returning*?a?fully?valid?middleware?comprised*?of?all?those?which?are?passed.**?@param?{Array}?middleware*?@return?{Function}*?@api?public*/
function?compose?(middleware)?{//?校驗(yàn)傳入的參數(shù)是數(shù)組,校驗(yàn)數(shù)組中每一項(xiàng)是函數(shù)if?(!Array.isArray(middleware))?throw?new?TypeError('Middleware?stack?must?be?an?array!')for?(const?fn?of?middleware)?{if?(typeof?fn?!==?'function')?throw?new?TypeError('Middleware?must?be?composed?of?functions!')}/***?@param?{Object}?context*?@return?{Promise}*?@api?public*/return?function?(context,?next)?{//?last?called?middleware?#let?index?=?-1return?dispatch(0)function?dispatch(i){//?省略,下文講述}}
}
接著我們來(lái)看 dispatch 函數(shù)。
3.1.2 dispatch 函數(shù)
function?dispatch?(i)?{//?一個(gè)函數(shù)中多次調(diào)用報(bào)錯(cuò)//?await?next()//?await?next()if?(i?<=?index)?return?Promise.reject(new?Error('next()?called?multiple?times'))index?=?i//?取出數(shù)組里的?fn1,?fn2,?fn3...let?fn?=?middleware[i]//?最后?相等,next?為?undefinedif?(i?===?middleware.length)?fn?=?next//?直接返回?Promise.resolve()if?(!fn)?return?Promise.resolve()try?{return?Promise.resolve(fn(context,?dispatch.bind(null,?i?+?1)))}?catch?(err)?{return?Promise.reject(err)} }值得一提的是:bind函數(shù)是返回一個(gè)新的函數(shù)。第一個(gè)參數(shù)是函數(shù)里的this指向(如果函數(shù)不需要使用this,一般會(huì)寫成null)。這句fn(context, dispatch.bind(null, i + 1),i + 1 是為了 let fn = middleware[i] 取middleware中的下一個(gè)函數(shù)。也就是 next 是下一個(gè)中間件里的函數(shù)。也就能解釋上文中的 gif圖函數(shù)執(zhí)行順序。測(cè)試用例中數(shù)組的最終順序是[1,2,3,4,5,6]。
3.1.3 簡(jiǎn)化 compose 便于理解
自己動(dòng)手調(diào)試之后,你會(huì)發(fā)現(xiàn) compose 執(zhí)行后就是類似這樣的結(jié)構(gòu)(省略 try catch 判斷)。
//?這樣就可能更好理解了。 //?simpleKoaCompose const?[fn1,?fn2,?fn3]?=?stack; const?fnMiddleware?=?function(context){return?Promise.resolve(fn1(context,?function?next(){return?Promise.resolve(fn2(context,?function?next(){return?Promise.resolve(fn3(context,?function?next(){return?Promise.resolve();}))}))})); };也就是說(shuō)koa-compose返回的是一個(gè)Promise,從中間件(傳入的數(shù)組)中取出第一個(gè)函數(shù),傳入context和第一個(gè)next函數(shù)來(lái)執(zhí)行。
第一個(gè)next函數(shù)里也是返回的是一個(gè)Promise,從中間件(傳入的數(shù)組)中取出第二個(gè)函數(shù),傳入context和第二個(gè)next函數(shù)來(lái)執(zhí)行。
第二個(gè)next函數(shù)里也是返回的是一個(gè)Promise,從中間件(傳入的數(shù)組)中取出第三個(gè)函數(shù),傳入context和第三個(gè)next函數(shù)來(lái)執(zhí)行。
第三個(gè)...
以此類推。最后一個(gè)中間件中有調(diào)用next函數(shù),則返回Promise.resolve。如果沒(méi)有,則不執(zhí)行next函數(shù)。這樣就把所有中間件串聯(lián)起來(lái)了。這也就是我們常說(shuō)的洋蔥模型。
不得不說(shuō)非常驚艷,“玩還是大神會(huì)玩”。
3.2 錯(cuò)誤捕獲
it('should?catch?downstream?errors',?async?()?=>?{const?arr?=?[]const?stack?=?[]stack.push(async?(ctx,?next)?=>?{arr.push(1)try?{arr.push(6)await?next()arr.push(7)}?catch?(err)?{arr.push(2)}arr.push(3)})stack.push(async?(ctx,?next)?=>?{arr.push(4)throw?new?Error()})await?compose(stack)({})//?輸出順序?是?[?1,?6,?4,?2,?3?]expect(arr).toEqual([1,?6,?4,?2,?3]) })相信理解了第一個(gè)測(cè)試用例和 compose 函數(shù),也是比較好理解這個(gè)測(cè)試用例了。這一部分其實(shí)就是對(duì)應(yīng)的代碼在這里。
try?{return?Promise.resolve(fn(context,?dispatch.bind(null,?i?+?1))) }?catch?(err)?{return?Promise.reject(err) }3.3 next 函數(shù)不能調(diào)用多次
it('should?throw?if?next()?is?called?multiple?times',?()?=>?{return?compose([async?(ctx,?next)?=>?{await?next()await?next()}])({}).then(()?=>?{throw?new?Error('boom')},?(err)?=>?{assert(/multiple?times/.test(err.message))}) })這一塊對(duì)應(yīng)的則是:
index?=?-1 dispatch(0) function?dispatch?(i)?{if?(i?<=?index)?return?Promise.reject(new?Error('next()?called?multiple?times'))index?=?i }調(diào)用兩次后 i 和 index 都為 1,所以會(huì)報(bào)錯(cuò)。
compose/test/test.js文件中總共 300余行,還有很多測(cè)試用例可以按照文中方法自行調(diào)試。
4. 總結(jié)
雖然koa-compose源碼 50行 不到,但如果是第一次看源碼調(diào)試源碼,還是會(huì)有難度的。其中混雜著高階函數(shù)、閉包、Promise、bind等基礎(chǔ)知識(shí)。
通過(guò)本文,我們熟悉了 koa-compose 中間件常說(shuō)的洋蔥模型,學(xué)會(huì)了部分 `jest`[6] 用法,同時(shí)也學(xué)會(huì)了如何使用現(xiàn)成的測(cè)試用例去調(diào)試源碼。
相信學(xué)會(huì)了通過(guò)測(cè)試用例調(diào)試源碼后,會(huì)覺(jué)得源碼也沒(méi)有想象中的那么難。
開(kāi)源項(xiàng)目,一般都會(huì)有很全面的測(cè)試用例。除了可以給我們學(xué)習(xí)源碼調(diào)試源碼帶來(lái)方便的同時(shí),也可以給我們帶來(lái)的啟發(fā):自己工作中的項(xiàng)目,也可以逐步引入測(cè)試工具,比如 jest。
此外,讀開(kāi)源項(xiàng)目源碼是我們學(xué)習(xí)業(yè)界大牛設(shè)計(jì)思想和源碼實(shí)現(xiàn)等比較好的方式。
看完本文,非常希望能自己動(dòng)手實(shí)踐調(diào)試源碼去學(xué)習(xí),容易吸收消化。另外,如果你有余力,可以繼續(xù)看我的 koa-compose 源碼文章:學(xué)習(xí) koa 源碼的整體架構(gòu),淺析koa洋蔥模型原理和co原理
參考資料
[1]
koa-compose 倉(cāng)庫(kù): https://github.com/koajs/compose
[2]本文倉(cāng)庫(kù)地址 koa-compose-analysis: https://github.com/lxchuan12/koa-compose-analysis.git
[3]用 Git Subtree 在多個(gè) Git 項(xiàng)目間雙向同步子項(xiàng)目,附簡(jiǎn)明使用手冊(cè): https://segmentfault.com/a/1190000003969060
[4]更多 nodejs 調(diào)試相關(guān) 可以查看官方文檔: https://code.visualstudio.com/docs/nodejs/nodejs-debugging
[5]koa的文檔: https://github.com/koajs/koa/blob/master/docs/guide.md#writing-middleware
[6]jest: https://github.com/facebook/jest
最近組建了一個(gè)江西人的前端交流群,如果你是江西人可以加我微信?ruochuan12?私信 江西?拉你進(jìn)群。
推薦閱讀
1個(gè)月,200+人,一起讀了4周源碼
我讀源碼的經(jīng)歷
老姚淺談:怎么學(xué)JavaScript?
我在阿里招前端,該怎么幫你(可進(jìn)面試群)
·················?若川簡(jiǎn)介?·················
你好,我是若川,畢業(yè)于江西高校。現(xiàn)在是一名前端開(kāi)發(fā)“工程師”。寫有《學(xué)習(xí)源碼整體架構(gòu)系列》多篇,在知乎、掘金收獲超百萬(wàn)閱讀。
從2014年起,每年都會(huì)寫一篇年度總結(jié),已經(jīng)寫了7篇,點(diǎn)擊查看年度總結(jié)。
同時(shí),活躍在知乎@若川,掘金@若川。致力于分享前端開(kāi)發(fā)經(jīng)驗(yàn),愿景:幫助5年內(nèi)前端人走向前列。
識(shí)別上方二維碼加我微信、拉你進(jìn)源碼共讀群
今日話題
略。歡迎分享、收藏、點(diǎn)贊、在看我的公眾號(hào)文章~
總結(jié)
以上是生活随笔為你收集整理的50行代码串行Promise,koa洋葱模型原来这么有趣?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 小程序学习(2):vs code 安装插
- 下一篇: Taro+react开发(32) Ple