1. 前言
大家好,我是若川,最近組織了源碼共讀活動《1個月,200+人,一起讀了4周源碼》,感興趣的可以加我微信 ruochuan12 參與,長期交流學習。
之前寫的《學習源碼整體架構系列》 包含jQuery
、underscore
、lodash
、vuex
、sentry
、axios
、redux
、koa
、vue-devtools
、vuex4
十余篇源碼文章。其中最新的兩篇是:
Vue 3.2 發布了,那尤雨溪是怎么發布 Vue.js 的?
初學者也能看懂的 Vue3 源碼中那些實用的基礎工具函數
寫相對很難的源碼,耗費了自己的時間和精力,也沒收獲多少閱讀點贊,其實是一件挺受打擊的事情。從閱讀量和讀者受益方面來看,不能促進作者持續輸出文章。
所以轉變思路,寫一些相對通俗易懂的文章。其實源碼也不是想象的那么難,至少有很多看得懂。
之前寫過 koa 源碼文章學習 koa 源碼的整體架構,淺析koa洋蔥模型原理和co原理比較長,讀者朋友大概率看不完,所以本文從koa-compose
50行源碼講述。
本文涉及到的 koa-compose 倉庫[1] 文件,整個index.js
文件代碼行數雖然不到 50
行,而且測試用例test/test.js
文件 300
余行,但非常值得我們學習。
歌德曾說:讀一本好書,就是在和高尚的人談話。同理可得:讀源碼,也算是和作者的一種學習交流的方式。
閱讀本文,你將學到:
1.?熟悉?koa-compose?中間件源碼、可以應對面試官相關問題
2.?學會使用測試用例調試源碼
3.?學會?jest?部分用法
2. 環境準備
2.1 克隆 koa-compose 項目
本文倉庫地址 koa-compose-analysis[2],求個star
~
#?可以直接克隆我的倉庫,我的倉庫保留的?compose?倉庫的?git?記錄
git?clone?https://github.com/lxchuan12/koa-compose-analysis.git
cd?koa-compose/compose
npm?i
順帶說下:我是怎么保留 compose
倉庫的 git
記錄的。
#?在?github?上新建一個倉庫?`koa-compose-analysis`?克隆下來
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 倉庫了。且保留的 git 記錄
關于更多 git subtree
,可以看這篇文章用 Git Subtree 在多個 Git 項目間雙向同步子項目,附簡明使用手冊[3]
接著我們來看怎么根據開源項目中提供的測試用例調試源碼。
2.2 根據測試用例調試 compose 源碼
用VSCode
(我的版本是 1.60
)打開項目,找到 compose/package.json
,找到 scripts
和 test
命令。
//?compose/package.json
{"name":?"koa-compose",//?debug?(調試)"scripts":?{"eslint":?"standard?--fix?.","test":?"jest"},
}
在scripts
上方應該會有debug
或者調試
字樣。點擊debug
(調試),選擇 test
。

接著會執行測試用例test/test.js
文件。終端輸出如下圖所示。

接著我們調試 compose/test/test.js
文件。我們可以在 45行
打上斷點,重新點擊 package.json
=> srcipts
=> test
進入調試模式。如下圖所示。

接著按上方的按鈕,繼續調試。在compose/index.js
文件中關鍵的地方打上斷點,調試學習源碼事半功倍。
更多 nodejs 調試相關 可以查看官方文檔[4]
順便提一下幾個調試相關按鈕。
繼續(F5)
單步跳過(F10)
單步調試(F11)
單步跳出(Shift + F11)
重啟(Ctrl + Shift + F5)
斷開鏈接(Shift + F5)
接下來,我們跟著測試用例學源碼。
3. 跟著測試用例學源碼
分享一個測試用例小技巧:我們可以在測試用例處加上only
修飾。
//?例如
it.only('should?work',?async?()?=>?{})
這樣我們就可以只執行當前的測試用例,不關心其他的,不會干擾調試。
3.1 正常流程
打開 compose/test/test.js
文件,看第一個測試用例。
//?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)({})//?最后輸出數組是?[1,2,3,4,5,6]expect(arr).toEqual(expect.arrayContaining([1,?2,?3,?4,?5,?6]))})
}
大概看完這段測試用例,context
是什么,next
又是什么。
在`koa`的文檔[5]上有個非常代表性的中間件 gif
圖。

而compose
函數作用就是把添加進中間件數組的函數按照上面 gif
圖的順序執行。
3.1.1 compose 函數
簡單來說,compose
函數主要做了兩件事情。
接收一個參數,校驗參數是數組,且校驗數組中的每一項是函數。
返回一個函數,這個函數接收兩個參數,分別是
context
和next
,這個函數最后返回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)?{//?校驗傳入的參數是數組,校驗數組中每一項是函數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){//?省略,下文講述}} }
接著我們來看
dispatch
函數。3.1.2 dispatch 函數
function?dispatch?(i)?{//?一個函數中多次調用報錯//?await?next()//?await?next()if?(i?<=?index)?return?Promise.reject(new?Error('next()?called?multiple?times'))index?=?i//?取出數組里的?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
函數是返回一個新的函數。第一個參數是函數里的this指向(如果函數不需要使用this
,一般會寫成null
)。這句fn(context, dispatch.bind(null, i + 1)
,i + 1
是為了let fn = middleware[i]
取middleware
中的下一個函數。也就是next
是下一個中間件里的函數。也就能解釋上文中的gif
圖函數執行順序。測試用例中數組的最終順序是[1,2,3,4,5,6]
。3.1.3 簡化 compose 便于理解
自己動手調試之后,你會發現
compose
執行后就是類似這樣的結構(省略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();}))}))})); };
也就是說
koa-compose
返回的是一個Promise
,從中間件(傳入的數組)
中取出第一個函數,傳入context
和第一個next
函數來執行。
第一個next
函數里也是返回的是一個Promise
,從中間件(傳入的數組)
中取出第二個函數,傳入context
和第二個next
函數來執行。
第二個next
函數里也是返回的是一個Promise
,從中間件(傳入的數組)
中取出第三個函數,傳入context
和第三個next
函數來執行。
第三個...
以此類推。最后一個中間件中有調用next
函數,則返回Promise.resolve
。如果沒有,則不執行next
函數。這樣就把所有中間件串聯起來了。這也就是我們常說的洋蔥模型。洋蔥模型圖如下圖所示: 不得不說非常驚艷,“玩還是大神會玩”。
3.2 錯誤捕獲
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]) })
相信理解了第一個測試用例和
compose
函數,也是比較好理解這個測試用例了。這一部分其實就是對應的代碼在這里。try?{return?Promise.resolve(fn(context,?dispatch.bind(null,?i?+?1))) }?catch?(err)?{return?Promise.reject(err) }
3.3 next 函數不能調用多次
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))}) })
這一塊對應的則是:
index?=?-1 dispatch(0) function?dispatch?(i)?{if?(i?<=?index)?return?Promise.reject(new?Error('next()?called?multiple?times'))index?=?i }
調用兩次后
i
和index
都為1
,所以會報錯。compose/test/test.js
文件中總共 300余行,還有很多測試用例可以按照文中方法自行調試。4. 總結
雖然
koa-compose
源碼 50行 不到,但如果是第一次看源碼調試源碼,還是會有難度的。其中混雜著高階函數、閉包、Promise
、bind
等基礎知識。通過本文,我們熟悉了
koa-compose
中間件常說的洋蔥模型,學會了部分 `jest`[6] 用法,同時也學會了如何使用現成的測試用例去調試源碼。相信學會了通過測試用例調試源碼后,會覺得源碼也沒有想象中的那么難。
開源項目,一般都會有很全面的測試用例。除了可以給我們學習源碼調試源碼帶來方便的同時,也可以給我們帶來的啟發:自己工作中的項目,也可以逐步引入測試工具,比如
jest
。此外,讀開源項目源碼是我們學習業界大牛設計思想和源碼實現等比較好的方式。
看完本文,非常希望能自己動手實踐調試源碼去學習,容易吸收消化。另外,如果你有余力,可以繼續看我的
koa-compose
源碼文章:學習 koa 源碼的整體架構,淺析koa洋蔥模型原理和co原理參考資料
[1]
koa-compose 倉庫: https://github.com/koajs/compose
[2]本文倉庫地址 koa-compose-analysis: https://github.com/lxchuan12/koa-compose-analysis.git
[3]用 Git Subtree 在多個 Git 項目間雙向同步子項目,附簡明使用手冊: https://segmentfault.com/a/1190000003969060
[4]更多 nodejs 調試相關 可以查看官方文檔: https://code.visualstudio.com/docs/nodejs/nodejs-debugging
[5]
[6]koa
的文檔: https://github.com/koajs/koa/blob/master/docs/guide.md#writing-middlewarejest
: https://github.com/facebook/jest最近組建了一個江西人的前端交流群,如果你是江西人可以加我微信?ruochuan12?私信 江西?拉你進群。
推薦閱讀
1個月,200+人,一起讀了4周源碼
我讀源碼的經歷老姚淺談:怎么學JavaScript?
我在阿里招前端,該怎么幫你(可進面試群)
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》多篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,活躍在知乎@若川,掘金@若川。致力于分享前端開發經驗,愿景:幫助5年內前端人走向前列。識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。歡迎分享、收藏、點贊、在看我的公眾號文章~