閱讀本文您將了解到:什么是 monorepo、為什么要 monorepo、如何實(shí)踐 monorepo。
項(xiàng)目管理模式
Monorepo 這個(gè)詞您可能不是首次聽(tīng)說(shuō),在當(dāng)下大型前端項(xiàng)目中基于 monorepo 的解決方案已經(jīng)深入人心,無(wú)論是比如 Google、Facebook,社區(qū)內(nèi)部知名的開(kāi)源項(xiàng)目 Babel、Vue-next ,還是集團(tuán)中 rax-components 等等,都使用了 monorepo 方案來(lái)管理他們的代碼。
?發(fā)展歷程
倉(cāng)庫(kù)(repository,簡(jiǎn)稱 repo),是我們用來(lái)管理項(xiàng)目代碼的一個(gè)基本單元。通常每個(gè)倉(cāng)庫(kù)負(fù)責(zé)一個(gè)模塊或包的編碼、構(gòu)建、測(cè)試和發(fā)布,代碼規(guī)模相對(duì)較小,邏輯聚合,業(yè)務(wù)場(chǎng)景也比較收攏。
當(dāng)我們?cè)谝徽麎K業(yè)務(wù)域下進(jìn)行研發(fā)時(shí),代碼的解耦和復(fù)用是一個(gè)非常重要的問(wèn)題。
初期業(yè)務(wù)系統(tǒng)不復(fù)雜時(shí),通常只用一個(gè)倉(cāng)庫(kù)來(lái)管理項(xiàng)目,項(xiàng)目為單體應(yīng)用架構(gòu) Monolithic。這時(shí)我們會(huì)以合理劃分目錄,提取公共組件的方式來(lái)解決問(wèn)題。由文件的層級(jí)劃分和引入,來(lái)進(jìn)行頁(yè)面、組件和工具方法等的管理。此時(shí)其整個(gè)依賴和工作流都是統(tǒng)一的、單向的。
當(dāng)業(yè)務(wù)復(fù)雜度的提升,項(xiàng)目的復(fù)雜性增長(zhǎng),由此就會(huì)導(dǎo)致一系列的問(wèn)題:比如項(xiàng)目編譯速度變慢(調(diào)試成本變大)、部署效率/頻率低(非業(yè)務(wù)開(kāi)發(fā)耗時(shí)增加)、單場(chǎng)景下加載內(nèi)容冗余等等,技術(shù)債務(wù)會(huì)越積越多。同時(shí)又有了代碼共享的需求,此時(shí)就需要按照業(yè)務(wù)和模塊來(lái)拆分。那么組件化開(kāi)發(fā)是一個(gè)不錯(cuò)的選擇。這樣每個(gè)倉(cāng)庫(kù)都能獨(dú)立進(jìn)行各模塊的編碼、測(cè)試和發(fā)版,又能實(shí)現(xiàn)多項(xiàng)目共享代碼,研發(fā)效率提升也很明顯(特別是調(diào) UI 樣式的時(shí)候)。同時(shí)在團(tuán)隊(duì)規(guī)模變大,人員分工開(kāi)始明確,拆分的好處還帶來(lái)不同開(kāi)發(fā)人員關(guān)注點(diǎn)可按照域來(lái)分散研發(fā),隊(duì)員只需關(guān)心自己模塊所在的倉(cāng)庫(kù),對(duì)各自核心的業(yè)務(wù)場(chǎng)景關(guān)注思考更加集中和收攏。這種管理模式我們稱之為多倉(cāng)多模塊管理 Multirepo(Polyrepo也是一個(gè)意思)。
再隨著時(shí)間的沉淀,模塊數(shù)量也在飛速增長(zhǎng)。Multirepo 這種方式雖然從業(yè)務(wù)邏輯上解耦了,但也同時(shí)增加了項(xiàng)目的工程管理難度。組件化的前期可以忽略不計(jì),當(dāng)模塊量到達(dá)一定體量程度下,這個(gè)問(wèn)題會(huì)逐漸明顯。比如:
- 代碼和配置很難共享:每個(gè)倉(cāng)庫(kù)都需要做一些重復(fù)的工程化能力配置(如 eslint/test/ci 等)且無(wú)法統(tǒng)一維護(hù),當(dāng)有工程上的升級(jí)時(shí),沒(méi)能同步更新到所有涉及模塊,就會(huì)一直存在一個(gè)過(guò)渡態(tài)的情況,對(duì)工程的不斷優(yōu)化非常不利。
- 依賴的治理復(fù)雜:模塊越來(lái)越多,涉及多模塊同時(shí)改動(dòng)的可能性急劇增加。如何保障底層組件升級(jí)后,其引用到的組件也能同步更新到位。這點(diǎn)很難做到,如果沒(méi)及時(shí)升級(jí),各工程的依賴版本不一致,往往會(huì)引發(fā)一些意想不到的問(wèn)題。
- 存儲(chǔ)和構(gòu)建消耗增加:假如多個(gè)工程依賴 pkg-a,那么每個(gè)工程下 node_modules 都會(huì)重復(fù)安裝 pkg-a,對(duì)本地磁盤(pán)內(nèi)存和本地啟動(dòng)都是個(gè)很大的挑戰(zhàn),增加了開(kāi)發(fā)時(shí)調(diào)試的困難。而且每個(gè)模塊的發(fā)布都是相對(duì)獨(dú)立的,當(dāng)一次迭代修改較多模塊時(shí),總體發(fā)布時(shí)效就是每個(gè)發(fā)布流程的串聯(lián)。對(duì)發(fā)布者來(lái)說(shuō)是一個(gè)非常大的負(fù)擔(dān)。
有沒(méi)有一種更好的管理模式,既能享受到 組件化多包管理 的收益,又能降輕工程復(fù)雜度引起的影響呢?這時(shí)就提出了單倉(cāng)多模塊管理 Monorepo 的概念。Monorepo 其實(shí)不是一個(gè)新的概念,在軟件工程領(lǐng)域,它已經(jīng)有著十多年的歷史了。它是相對(duì)于 Multirepo 而言的一種模式,概念上非常好理解,就是把多個(gè)項(xiàng)目放在一個(gè)倉(cāng)庫(kù)里面。用統(tǒng)一的本地關(guān)聯(lián)、構(gòu)建、發(fā)布流程,來(lái)消費(fèi)業(yè)務(wù)域下所有管理的組件模塊。
?單體應(yīng)用架構(gòu) Monolithic
項(xiàng)目初期起步階段,團(tuán)隊(duì)規(guī)模很小,此時(shí)適合「單體應(yīng)用」,一個(gè)代碼倉(cāng)庫(kù)承接一個(gè)應(yīng)用,管理成本低,最簡(jiǎn)力度支撐業(yè)務(wù)快速落地。
此時(shí)目錄架構(gòu)大概長(zhǎng)這樣:
project├── node_modules/│ ├── lib@1.0.0├── src/│ ├── compA│ ├── compB│ └── compC└── package.json
優(yōu)點(diǎn):
- 代碼管理成本低
- 代碼能見(jiàn)度高(無(wú)需額外的學(xué)習(xí)成本)
- 發(fā)布簡(jiǎn)單,鏈路輕便
缺點(diǎn):
- 代碼量大了后,調(diào)試、構(gòu)建效率顯著下降
- 無(wú)法跨項(xiàng)目復(fù)用
?多倉(cāng)多模塊管理 Multirepo
團(tuán)隊(duì)規(guī)模變大,人員分工明確,單體應(yīng)用的缺點(diǎn)會(huì)愈發(fā)突出,此時(shí) 「Multirepo」就更適合。模塊分工更明確,可拓展可復(fù)用性更強(qiáng),調(diào)試構(gòu)建發(fā)布能力也有一定提升。
此時(shí)目錄架構(gòu)大概長(zhǎng)這樣:
project├── node_modules/│ ├── lib@1.0.0│ ├── lib@2.0.0│ ├── pkgA│ ├── pkgB│ └── ..├── src/└── package.jsonpackageA├── node_modules/│ └── lib@1.0.0├── src/└── package.jsonpackageB├── node_modules/│ └── lib@2.0.0├── src/└── package.json
優(yōu)點(diǎn):
- 便于代碼復(fù)用
- 模塊組件獨(dú)立開(kāi)發(fā)調(diào)試,業(yè)務(wù)理解清晰度高
- 人員編排分工更加明確
- 提高研發(fā)人員的公共抽取思維能力
- 源代碼訪問(wèn)權(quán)限設(shè)置靈活
缺點(diǎn):
- 模塊劃分力度不容易把握
- 共同引用的版本問(wèn)題,容易導(dǎo)致重復(fù)安裝相同依賴的多個(gè)版本
- 構(gòu)建配置不復(fù)用,不好管理
- 串行構(gòu)建,修改模塊體量大時(shí),發(fā)布成本急劇上升
- Code Review、Merge Request 從各自模塊倉(cāng)庫(kù)執(zhí)行,比較分散
?單倉(cāng)多模塊管理 Monorepo
隨著組件/模塊越來(lái)越多, multirepo 維護(hù)成本越來(lái)越大,于是我們意識(shí)到我們的方案是時(shí)候改進(jìn)了。
此時(shí)目錄架構(gòu)大概長(zhǎng)這樣:
project├── node_modules/│ ├── lib@2.0.0│ ├── pkgA│ ├── pkgB│ └── ..├── src/└── package.jsonmono-project├── node_modules/│ └── lib@2.0.0├── packages/│ ├── packageA│ │ └── package.json│ └── packageB│ └── package.json└── package.json
優(yōu)點(diǎn):
- 所有源碼在一個(gè)倉(cāng)庫(kù)內(nèi),分支管理與單體應(yīng)用一樣簡(jiǎn)單
- 公共依賴顯示更清晰,更方便統(tǒng)一公共模塊版本
- 統(tǒng)一的配置方案,統(tǒng)一的構(gòu)建策略
- 并行構(gòu)建,執(zhí)行效率提升
- 保留 multirepo 的主要優(yōu)勢(shì)
- 代碼復(fù)用
- 模塊獨(dú)立管理
- 分工明確,業(yè)務(wù)場(chǎng)景獨(dú)立
- 代碼耦合度降低
- 項(xiàng)目引入時(shí),除去非必要組件代碼
- CR、MR 由一個(gè)倉(cāng)庫(kù)發(fā)布,閱讀和處理十分方便
缺點(diǎn):
- git 服務(wù)根據(jù)目錄進(jìn)行訪問(wèn)權(quán)限劃分,倉(cāng)庫(kù)內(nèi)全部代碼開(kāi)發(fā)給所有開(kāi)發(fā)成員(這種非特殊限制場(chǎng)景不用考慮)
- 當(dāng)代碼規(guī)模大到一定程度時(shí),git 的操作速度達(dá)到瓶頸,影響 git 操作體驗(yàn)(中小型規(guī)模不用考慮,而且就算是 def 平臺(tái)可并行量也為 500)
?優(yōu)缺點(diǎn)對(duì)比梳理
場(chǎng)景 | multirepo | monorepo |
項(xiàng)目代碼維護(hù) | ? 多個(gè)倉(cāng)庫(kù)需要分別download各自的node_modules,像這種上百個(gè)包的,多少內(nèi)存都不夠用 | ? 代碼都只一個(gè)倉(cāng)庫(kù)中,相同依賴無(wú)需多分磁盤(pán)內(nèi)存。 |
代碼可見(jiàn)性 | ? 包管理按照各自owner劃分,當(dāng)出現(xiàn)問(wèn)題時(shí),需要到依賴包中進(jìn)行判斷并解決 ? 對(duì)需要代碼隔離的情況友好,研發(fā)者只關(guān)注自己核心管理模塊本身 | ? 每個(gè)人可以方便地閱讀到其他人的代碼,這個(gè)橫向可以為團(tuán)隊(duì)帶來(lái)更好的協(xié)作和跨團(tuán)隊(duì)貢獻(xiàn),不同開(kāi)發(fā)者容易關(guān)注到代碼問(wèn)題本身 ? 但同時(shí)也會(huì)容易產(chǎn)生非owner管理者的改動(dòng)風(fēng)險(xiǎn) ? 不好進(jìn)行代碼可視隔離 |
代碼一致性 | ? 需要收口eslint等配置包到統(tǒng)一的npm包,再到各自項(xiàng)目引用,這就允許每個(gè)包還能手動(dòng)調(diào)整配置文件 | ? 當(dāng)您將所有代碼庫(kù)放在一個(gè)地方時(shí),執(zhí)行代碼質(zhì)量標(biāo)準(zhǔn)和統(tǒng)一風(fēng)格會(huì)更容易。 |
代碼提交 | ? 底層組件升級(jí),需要通知到所有項(xiàng)目依賴的相關(guān)方,并進(jìn)行回歸 ? 每個(gè)包的修改需要分別提交 | ? API 或共享庫(kù)中的重大更改能夠立即公開(kāi),迫使不同的開(kāi)發(fā)者需要提前溝通并聯(lián)合起來(lái)。每個(gè)人都必須跟上變化。 ? 提交使大規(guī)模重構(gòu)更容易。開(kāi)發(fā)人員可以在一次提交中更新多個(gè)包或項(xiàng)目。 |
唯一來(lái)源 | ? 子包引用的相同依賴的不同版本的包 | ? 每個(gè)依賴項(xiàng)的一個(gè)版本意味著沒(méi)有版本沖突,也沒(méi)有依賴地獄。 |
開(kāi)發(fā) | ? 倉(cāng)庫(kù)體積小,模塊劃分清晰。 ? 多倉(cāng)庫(kù)來(lái)回切換(編輯器及命令行),項(xiàng)目一多真的得暈。如果倉(cāng)庫(kù)之間存在依賴,還得各種 npm link。 | ? 只需在一個(gè)倉(cāng)庫(kù)中開(kāi)發(fā),編碼會(huì)相當(dāng)方便。 ? 代碼復(fù)用高,方便進(jìn)行代碼重構(gòu)。 ? 項(xiàng)目如果變的很龐大,那么 git clone、安裝依賴、構(gòu)建都會(huì)是一件耗時(shí)的事情。 |
工程配置 | ? 各個(gè)團(tuán)隊(duì)可能各自有一套標(biāo)準(zhǔn),新建一個(gè)倉(cāng)庫(kù)又得重新配置一遍工程及 CI / CD 等內(nèi)容。 | ? 工程統(tǒng)一標(biāo)準(zhǔn)化 |
依賴管理 | ? 依賴重復(fù)安裝,多個(gè)依賴可能在多個(gè)倉(cāng)庫(kù)中存在不同的版本,npm link 時(shí)不同項(xiàng)目的依賴可能會(huì)存在沖突問(wèn)題。 | ? 共同依賴可以提取至 root,版本控制更加容易,依賴管理會(huì)變的方便。 |
代碼管理 | ? 各個(gè)團(tuán)隊(duì)可以控制代碼權(quán)限,也幾乎不會(huì)有項(xiàng)目太大的問(wèn)題。 | ? 代碼全在一個(gè)倉(cāng)庫(kù),如果項(xiàng)目一大,幾個(gè) G 的話,用 Git 管理可能會(huì)存在問(wèn)題。 ? 代碼權(quán)限如果需要設(shè)置,暫時(shí)不支持 |
部署(這部分兩者其實(shí)都存在問(wèn)題) | ? multi repo 的話,如果各個(gè)包之間不存在依賴關(guān)系倒沒(méi)事,一旦存在依賴關(guān)系的話,開(kāi)發(fā)者就需要在不同的倉(cāng)庫(kù)按照依賴先后順序去修改版本及進(jìn)行部署。 | ? 而對(duì)于 mono repo 來(lái)說(shuō),有工具鏈支持的話,部署會(huì)很方便,但是沒(méi)有工具鏈的話,存在的問(wèn)題一樣蛋疼。(社區(qū)推薦pnpm、lerna) |
持續(xù)集成 | ? 每個(gè)repo需要定制統(tǒng)一的構(gòu)建部署過(guò)程,然后再各自執(zhí)行 | ? 可以為 repo 中的每個(gè)項(xiàng)目使用相同的CI/CD部署過(guò)程。 ? 同時(shí)未來(lái)可以實(shí)現(xiàn)更自動(dòng)化的部署方式,一次命令完成所有的部署 |
總體來(lái)說(shuō),當(dāng)業(yè)務(wù)發(fā)展到一定規(guī)模時(shí),monorepo 的升級(jí)相比 multirepo 來(lái)說(shuō),是利遠(yuǎn)大于弊的。
Monorepo 使用 or not
?業(yè)務(wù)現(xiàn)狀
天貓校園如意pos業(yè)務(wù)域場(chǎng)景豐富,整體的代碼邏輯比較復(fù)雜,因此采取按照 app(項(xiàng)目入口)-bundle(業(yè)務(wù)域板塊:可以理解為頁(yè)面)-component/util(通用組件:base組件、biz組件、utils和sdk平鋪,都屬于這個(gè)) 的形式進(jìn)行整個(gè)項(xiàng)目的管理。目前項(xiàng)目所涉及的 npm 業(yè)務(wù)模塊數(shù)量已經(jīng)超過(guò)了 100 個(gè)。
?存在的制約
- 應(yīng)用規(guī)模增長(zhǎng),構(gòu)建依賴本地環(huán)境,構(gòu)建效率低下,非業(yè)務(wù)投入成本不斷上升
- 主應(yīng)用需要頻繁構(gòu)建
- 構(gòu)建前依賴的模塊需要單獨(dú)構(gòu)建,構(gòu)建速度串行
- 組件還在不斷增長(zhǎng),愈加不利于工程的維護(hù)
- 組件類發(fā)布沒(méi)有對(duì)接集團(tuán)規(guī)范,無(wú)CR卡點(diǎn),二級(jí)依賴凌亂
- 代碼 review 全靠 人工 diff 進(jìn)行 cr
- 每次的版本信息都是通過(guò)手動(dòng)維護(hù)
- 組件依賴的二級(jí)依賴不統(tǒng)一,package-conflict 非常多
?優(yōu)化目標(biāo)
構(gòu)建部署發(fā)布提效,全鏈路CR及需求管控,全代碼卡點(diǎn)管控,后續(xù)代碼質(zhì)量,單測(cè)節(jié)點(diǎn)補(bǔ)充等等
- 降低構(gòu)建部署成本,對(duì)于一次合理的多包改動(dòng),只需要進(jìn)行1~2次的構(gòu)建即可完成部署任務(wù)
- 降低每次迭代的應(yīng)用發(fā)布的維護(hù)成本,對(duì)于一個(gè)應(yīng)用及其包含的子應(yīng)用(包括集成包和微應(yīng)用模式),一次完整的研發(fā)流程只需要維護(hù)一個(gè)發(fā)布迭代。發(fā)布依賴關(guān)系通過(guò)自動(dòng)化流程進(jìn)行優(yōu)化。
- 對(duì)于主子應(yīng)用/組件可以進(jìn)行合理的CR管控
- 每個(gè)有變更的子應(yīng)用都可以關(guān)聯(lián)到對(duì)應(yīng)的aone需求(可多個(gè))。
- 能將整個(gè)研發(fā)和發(fā)布流程統(tǒng)一到一個(gè)平臺(tái)上進(jìn)行操作,降低理解和操作成本。(更進(jìn)一步的優(yōu)化,將原來(lái)割裂的一些流程節(jié)點(diǎn)進(jìn)行整合,以及版本迭代修改日志的統(tǒng)一維護(hù)。)
- 在流程節(jié)點(diǎn)上可以提供擴(kuò)展方式,預(yù)留后續(xù)類似代碼掃碼,質(zhì)量評(píng)估,灰度管控等體系。
?選用結(jié)論
綜上所述,不管是當(dāng)應(yīng)用規(guī)模發(fā)展到一定規(guī)模下普遍遇到的情況,還是歷史包袱,如意pos現(xiàn)在已經(jīng)是一個(gè)超級(jí)復(fù)雜的應(yīng)用。以上的問(wèn)題所帶來(lái)的制約,只會(huì)愈加凸顯。在這個(gè)大背景下,這個(gè)階段,為了解決上面的問(wèn)題,使用 monorepo 進(jìn)行項(xiàng)目管理升級(jí),是非常有價(jià)值的。
?落地結(jié)果
落地過(guò)程參考后面的「最佳實(shí)踐」
打包 | 開(kāi)發(fā) | 發(fā)布 | |
架構(gòu)升級(jí)前 | 單組件打包時(shí)間70~90s,迭代 n 個(gè)包需要 *n 的時(shí)間 | 單個(gè)包啟動(dòng)開(kāi)發(fā)需要~60s,同時(shí)開(kāi)發(fā)多個(gè)包會(huì)拖慢速度,進(jìn)入打包發(fā)布流程會(huì)打斷開(kāi)發(fā) | 脫軌線上發(fā)布平臺(tái)CR/測(cè)試無(wú)卡點(diǎn),脫離管控 |
架構(gòu)升級(jí)后 | 并行構(gòu)建打包 n 個(gè)包只需要~90s | 本地開(kāi)發(fā)不會(huì)被打斷,節(jié)省重啟時(shí)間 | 云平臺(tái)構(gòu)建模式,接入 CR 卡點(diǎn),進(jìn)一步提高質(zhì)量穩(wěn)定性 |
提效總結(jié) | 組件打包成本降低90% | 啟動(dòng)成本降低100% | 發(fā)版提效80%,本地構(gòu)建轉(zhuǎn)云構(gòu)建 |
Monorepo 生態(tài)
Monorepo 只是一個(gè)管理概念,實(shí)際上它并不代表某項(xiàng)具體的技術(shù),更不是所謂的框架。開(kāi)發(fā)人員需要根據(jù)不同場(chǎng)景、不同的研發(fā)習(xí)慣,使用相應(yīng)的技術(shù)手段或者工具,來(lái)達(dá)到或者完善它的整個(gè)流程,從而達(dá)到更好的開(kāi)發(fā)和管理體驗(yàn)。
目前前端領(lǐng)域的 Monorepo 生態(tài)有一個(gè)很顯著的特點(diǎn)就是只有庫(kù),而沒(méi)有大一統(tǒng)的框架或者完整的構(gòu)建系統(tǒng)來(lái)支持。目前的工具形態(tài)上像是傳統(tǒng)的 CMake 那樣的輔助工具,而不是像 Gradle(構(gòu)建語(yǔ)言 生態(tài)鏈)或 Cargo(包管理器自身集成) 那樣統(tǒng)一的方式??赡芪磥?lái)的趨勢(shì)是像 nx 或者 turborepo 這樣的庫(kù)要往完整的框架發(fā)展,或者包管理器自身就逐步支持相應(yīng)的功能,不需要過(guò)多的三方依賴。以下介紹一下生態(tài)中的一些核心技術(shù):
?包管理方案
- Npm
npm 在 v7 才支持了 workspaces,屬于終于能用上了但是并不好用的情況,重點(diǎn)是比較慢,通常無(wú)法兼容存量的 monorepo 應(yīng)用,出來(lái)的時(shí)間太晚了,不能像 yarn 支持自定義 nohoist 以應(yīng)對(duì)某些依賴被 hoist 到 monorepo root 導(dǎo)致的問(wèn)題,也沒(méi)有做到像 pnpm 以 link 的方式共享依賴,能顯著的減少磁盤(pán)占用,除了 npm 自帶之外沒(méi)有其他優(yōu)點(diǎn)。
- Yarn
yarn 1.x
最早支持 workspaces 模式的包管理器,配合 lerna 占據(jù)了大部分 monorepo,在比較長(zhǎng)的一段時(shí)間里是 monorepo 的事實(shí)標(biāo)準(zhǔn),缺點(diǎn)是 yarn 的共享包才會(huì)提升到 root node_modules 下,其他非共享庫(kù)都會(huì)每個(gè)地方留一份,占用空間比較多,還有提升到 root 這一行為也會(huì)帶來(lái)兼容性問(wèn)題(有些包的 require 方式比較 hack)
yarn berry(2 ~ 3)
比較新的點(diǎn)就是 pnp 模式,pnp 模式是為了解決 node_modules 臃腫、復(fù)雜度過(guò)高的問(wèn)題而來(lái)的,但是比較激進(jìn),所以很難支持現(xiàn)有的項(xiàng)目。不過(guò) yarn 3 基本上把各個(gè)包管理的功能都支持了(nodeLinker 配置),從功能上可以算是最多,比較復(fù)雜,概念好多。吸收了部分競(jìng)爭(zhēng)對(duì)手的優(yōu)點(diǎn),并開(kāi)辟了許多有趣的功能特性。
- Pnpm
全稱是 “Performant NPM”,即高性能的 npm。
如它官方文檔介紹的所說(shuō):“Saving disk space and boosting installation speed”,Pnpm 是一個(gè)能夠提高安裝速度、節(jié)省磁盤(pán)空間的包管理工具,并天然支持 Monorepo 的解決方案。除此之外,它也解決了很多令人詬病的問(wèn)題,其中,比較經(jīng)典的就是 Phantom dependencies(幻影依賴)。
pnpm 的優(yōu)勢(shì):
- 安裝依賴速度快,軟/硬鏈接結(jié)合
- 安裝過(guò)的依賴緩存全局復(fù)用,緩存邏輯基于文件塊,不同版本的依賴可以只緩存 diff
- 自身支持 workspaces 相關(guān)
推薦導(dǎo)讀:
pnpm官網(wǎng):https://pnpm.io/zh/
?包版本方案
- Lerna
Lerna 是一個(gè)管理工具,用于管理包含多個(gè)軟件包(package)的 Javascript 項(xiàng)目。它可以優(yōu)化使用 git 和 npm 管理多包存儲(chǔ)庫(kù)的工作流程。Lerna 主流應(yīng)用在處理版本、構(gòu)建工作流以及發(fā)布包等方面都比較優(yōu)秀,既兼顧版本管理,還支持全量發(fā)布和單獨(dú)發(fā)布等功能。在前端領(lǐng)域,它是最早出現(xiàn)也是相當(dāng)長(zhǎng)一段時(shí)間 monorepo 方案的事實(shí)標(biāo)準(zhǔn),具有統(tǒng)治地位,很多后來(lái)的工具的概念或者 workspaces 結(jié)構(gòu)都借鑒了 lerna,是 lerna 的延續(xù)。在業(yè)界實(shí)踐中,比較多的時(shí)間上,都是采用 Yarn 配合 lerna 組合完整的實(shí)現(xiàn)了 Monorepo 中項(xiàng)目的包管理、更新到發(fā)布的全流程。
后來(lái)停更了相當(dāng)長(zhǎng)一段時(shí)間,至今還是不支持 pnpm 的 workspaces(pnpm 下有 workspace:protocol,lerna 并沒(méi)有支持),與 yarn 強(qiáng)綁定。
最近由 nx 的開(kāi)發(fā)公司 nrwl 接手維護(hù),不過(guò)新增的 features 都是圍繞 nx 而加,nrwl 目前似乎還并沒(méi)有其他方向的 bug fix 或者新增 features 的計(jì)劃。不過(guò)社區(qū)也出現(xiàn)了 lerna-lite ,可以作為 lerna 長(zhǎng)久停滯的補(bǔ)充和替代,主要的新 features 就是支持在 pnpm workspaces。
推薦導(dǎo)讀:
- lerna:https://www.lernajs.cn/
- lerna-lite:https://Github.com/ghiscoding/lerna-lite
- Changesets
Changesets 是一個(gè)用于 Monorepo 項(xiàng)目下版本以及 Changelog 文件管理的工具。在 Changesets 的工作流會(huì)將開(kāi)發(fā)者分為兩類人,一類是項(xiàng)目的維護(hù)者,還有一類為項(xiàng)目的開(kāi)發(fā)者,開(kāi)發(fā)者在Monorepo項(xiàng)目下進(jìn)行開(kāi)發(fā),開(kāi)發(fā)完成后,給對(duì)應(yīng)的子項(xiàng)目添加一個(gè)changeset文件。項(xiàng)目的維護(hù)者后面會(huì)通過(guò)changeset來(lái)消耗掉這些文件并自動(dòng)修改掉對(duì)應(yīng)包的版本以及生成CHANGELOG文件,最后將對(duì)應(yīng)的包發(fā)布出去。
?包構(gòu)建方案
- Turborepo
上述提到傳統(tǒng)的 Monorepo 解決方案中,項(xiàng)目構(gòu)建時(shí)如果基于多個(gè)應(yīng)用程序存在依賴構(gòu)建,耗時(shí)是非??膳碌?。Turborepo 的出現(xiàn),正是解決 Monorepo 慢的問(wèn)題。
Turborepo 是一個(gè)用于 JavaScript 和 TypeScript 代碼庫(kù)的高性能構(gòu)建系統(tǒng)。通過(guò)增量構(gòu)建、智能遠(yuǎn)程緩存和優(yōu)化的任務(wù)調(diào)度,Turborepo 可以將構(gòu)建速度提高 85% 或更多,使各種規(guī)模的團(tuán)隊(duì)都能夠維護(hù)一個(gè)快速有效的構(gòu)建系統(tǒng),該系統(tǒng)可以隨著代碼庫(kù)和團(tuán)隊(duì)的成長(zhǎng)而擴(kuò)展。
推薦導(dǎo)讀:https://vercel.com/blog/vercel-acquires-turborepo
- Nx
定位上是 Smart, Fast and Extensible build system,出現(xiàn)得比較早,發(fā)展了挺久,功能特別多,基本上 cover 了各種應(yīng)用場(chǎng)景,文檔也比較詳細(xì),是現(xiàn)在幾個(gè) Monorepo 工具里比較接近完整的解決方案和框架的。
最近的話他們也接手了 lerna 的維護(hù),不過(guò)給 lerna 加的東西都是圍繞 nx 而來(lái)。
推薦導(dǎo)讀:https://nx.dev/
?其它生態(tài)工具
- Bolt
和 lerna 類似,更像是一個(gè) Task Runner,用于執(zhí)行 workspaces 下的各種 script,用法上和 npm 的 workspaces 類似,已經(jīng)停更一段時(shí)間。
- Preconstruct
Monorepo 下統(tǒng)一的 dev/Build 工具。亮點(diǎn)是 dev 模式使用了執(zhí)行時(shí)的 require hook,直接引用源文件在運(yùn)行時(shí)執(zhí)行轉(zhuǎn)譯(babel),不需要在開(kāi)發(fā)時(shí) watch 產(chǎn)物實(shí)時(shí)構(gòu)建,調(diào)試很方便。用法上比較像 parcel、microbundle 那樣 zero-config bundler,使用統(tǒng)一的 package.json 字段來(lái)指定輸出產(chǎn)物,缺點(diǎn)是比較死板,整個(gè)項(xiàng)目的配置都得按照這種配置方式,支持的選項(xiàng)目前還不多,不夠靈活。
- Rushstack
體感上比較像 Lerna Turborepo 各種東西的工具鏈,比較老牌,但是沒(méi)用過(guò),也很少見(jiàn)到有用這個(gè)的。
- Lage
Microsoft 出的,定位上是一個(gè) Task Runner in JS Monorepos ,亮點(diǎn)是 pipeline 的任務(wù)模式,構(gòu)建產(chǎn)物緩存,遠(yuǎn)程緩存等。
Monorepo 工具鏈選用
最終我們選用了 pnpm lerna-lite turborepo
?Pnpm
pnpm 的依賴全局緩存(global store and hard link)與安裝方式即是天然的依賴共享,相同版本的依賴只會(huì)安裝一次,有效地節(jié)約空間以及節(jié)省安裝時(shí)間,在 monorepo 場(chǎng)景下十分切合。
?Lerna-lite
pnpm 推薦的方案其實(shí)是 changesets,但是 changesets 的發(fā)版流程更貼近 Github Action Workflow,以及 打 changeset 然后 version 的概念和流程相對(duì) lerna 會(huì)復(fù)雜一些。
不直接使用 lerna 的原因是 lerna 并不支持 pnpm 的 workspace protocol 。
同時(shí) lerna 比較久沒(méi)有更新,雖然最近被 nx 的組織 nrwl 接管了,但是 nrwl 只擴(kuò)展了針對(duì) nx lerna 場(chǎng)景的功能,并沒(méi)有對(duì) lerna 的其他功能進(jìn)行維護(hù),所以社區(qū)里出現(xiàn)了 lerna-lite,真正意義上的延續(xù)了 lerna 的發(fā)展,目前比較有用的新功能是其 publish 命令支持了 pnpm 的 workspace protocol (workspace:),支持了 pnpm workspace 下 lerna 的發(fā)布流程。
?Turborepo
如果有高速構(gòu)建緩存需求則使用 turborepo。Turborepo 的基本原則是從不重新計(jì)算以前完成的工作,Turborepo 會(huì)記住你構(gòu)建的內(nèi)容并跳過(guò)已經(jīng)計(jì)算過(guò)的內(nèi)容,把每次構(gòu)建的產(chǎn)物與日志緩存起來(lái),下次構(gòu)建時(shí)只有文件發(fā)生變動(dòng)的部分才會(huì)重新構(gòu)建,沒(méi)有變動(dòng)的直接命中緩存并重現(xiàn)日志。在多次構(gòu)建開(kāi)發(fā)時(shí),這也就意味更少的構(gòu)建耗時(shí)。
天貓校園 Monorepo 最佳實(shí)踐
?前置準(zhǔn)備
使用 pnpm 作為包管理器,全局安裝 pnpm,命令如下:
$ tnpm i -g pnpm
?創(chuàng)建 mono 倉(cāng)庫(kù)
初始化該倉(cāng)庫(kù)為 mono 倉(cāng)庫(kù)
# 初始化成功$ tree ./your-mono-projectyour-mono-project├── packages│ ├── bundles│ │ └── bundle-a│ │ └── package.json│ └── components│ └── util-a│ └── package.json├── .gitignore├── .npmrc├── abc.json├── lerna.json├── package.json├── pnpm-workspace.yaml├── README.md└── turbo.json
項(xiàng)目結(jié)構(gòu)介紹:
packages/..
monorepo 各個(gè) workspaces 的目錄
abc.json
云構(gòu)建 builder 配置,與 def 平臺(tái)相關(guān)聯(lián)
lerna.json
lerna 配置,管理多個(gè)包的發(fā)布版本,變更日志生成的工具
package.json
monorepo 主目錄 package 文件,
pnpm-lock.yaml,
pnpm-workspace.yaml
pnpm lockfile(執(zhí)行 pnpm i 后生成),pnpm workspace 聲明文件
turbo.json
Turborepo 配置,主要用于產(chǎn)物緩存,構(gòu)建加速,構(gòu)建流配置。
Turborepo 地址:https://turborepo.org/
?基礎(chǔ) mono 配置設(shè)置
這里是使用 def 云構(gòu)建 builder 配置,默認(rèn)是 @ali/builder-xpos
{ "builder": "@ali/builder-xpos"}
lerna 的配置,包括 packages 的范圍,publish,version 的命令配置,還有指定 npmClient 為 pnpm,這里需要使用 @lerna-lite/cli
{ "packages": [ "packages/*/*" ], "command": { "publish": { "conventionalCommits": true, }, "version": { "conventionalCommits": true, "syncWorkspaceLock": true } }, "version": "independent", "npmClient": "pnpm"}
配置簡(jiǎn)要說(shuō)明:
packages:指定組件包所在文件夾,限定了packages的管理范圍。我們這里調(diào)整為「packages/*/*」。
需要配置為二級(jí)目錄。因?yàn)槲覀儼凑疹愋蛥^(qū)分各種包,然后相同類型的收納到該類型的目錄下,方便研發(fā)人員閱讀和理解,可以看到初始創(chuàng)建后,packages目錄下有二級(jí)目錄 bundles 和 components
version:配置組件包版本號(hào)管理方式,默認(rèn)是版本號(hào)。我們這里調(diào)整為「independent」。
注意 lerna 默認(rèn)使用的是集中版本,所有的package共用一個(gè)version,如果需要packages下不同的模塊 使用不同的版本號(hào),需要配置Independent模式
command:command主要是配置各種lerna指令的參數(shù),這些命令可以通過(guò)命令行配置,也可以在配置文件中配
更多配置參考,https://github.com/lerna/lerna#lernajson
pnpm-workspace.yaml 的配置
packages: - packages/*/*
同lerna配置說(shuō)明
turborepo 主要用于產(chǎn)物緩存,構(gòu)建加速
{ "$schema": "https://turborepo.org/schema.json", "pipeline": { "build": { "dependsOn": ["^build"], "inputs": [ "src/**/*" ], "outputs": [ "es/**", "lib/**", "build/**" ] } }}
?入駐組件
- 選擇需要入駐的組件
- 判斷該組件屬于哪個(gè)mono分類(如 bundles、components 等)
- 切換到相應(yīng)的分類目錄,如 /packages/components
- 入駐組件代碼到該目錄下
- 通過(guò)手動(dòng)復(fù)制代碼,或者 git clone 方式都可以
- 清除入駐組件的 .git
- 在其目錄 /packages/components/your-component 下執(zhí)行,rm -rf .git
- 只保留 mono 的 git 管理能力即可(重點(diǎn)注意!如果沒(méi)有清除,則無(wú)法被mono的gitdiff檢測(cè)到)
- 增加/替換 入駐組件中 package.json 中 build script 為 gulp build
- "build": "gulp build"
- 注意同時(shí)請(qǐng)保證 組件的構(gòu)建使用的是相同版本的 gulp,如如意pos統(tǒng)一使用的是 gulp@4
- 在 mono目錄下(或者 /packages/components/your-component 目錄下,或者mono中任意位置都可),執(zhí)行 pnpm i
- 執(zhí)行成功后,pnpm-lock.yaml 文件有對(duì)應(yīng)的更新,即入駐成功
- 不需要特別關(guān)注相互依賴問(wèn)題
- 入駐之后 pnpm 將會(huì)自動(dòng)識(shí)別本地的 package 變動(dòng)
- 仔細(xì)查看 pnpm-lock.yaml 中,本倉(cāng)庫(kù)依賴的組件的版本號(hào),會(huì)變成 link: ../xxx
?本地開(kāi)發(fā)關(guān)聯(lián)
入駐好組件后,就可以盡情地開(kāi)發(fā)編碼了。
正常情況下,組件通過(guò) npm link(tnpm link、pnpm link 相似)方式進(jìn)行本地開(kāi)發(fā)關(guān)聯(lián)。組件體量大時(shí),這樣就非常的麻煩,因此我們升級(jí)了本地關(guān)聯(lián)的方式,通過(guò)webpack alias 方式,將應(yīng)用的依賴路徑與本地mono倉(cāng)庫(kù)中的組件進(jìn)行替換,然后通過(guò)選擇的方式實(shí)現(xiàn)關(guān)聯(lián)。
操作步驟:
- 應(yīng)用中配置依賴的mono組件庫(kù)(構(gòu)建器中實(shí)現(xiàn))
- module.exports = {
'monorepo': 'xpos-ruyi-mono'
} - 啟動(dòng)本地構(gòu)建
- $ def dev –mono
- 選擇需要關(guān)聯(lián)的本地 mono 組件(構(gòu)建器CLI自行實(shí)現(xiàn)即可)
- 啟動(dòng)完成,就可以開(kāi)心編碼了
- 不再需要進(jìn)行手動(dòng) link 的操作
核心代碼簡(jiǎn)要如下:
// relateLocalMonoLinks.jsmodule.exports = async function relateLocalMonoLinks(def) { try { const localLinks = await getAppDepsLocalMainJsPath() // 獲取app中的依賴項(xiàng),獲取本地mono中的組件,匹配存在的組件,并將包名映射為本地組件入口文件的路徑 const cacheLinks = await getCacheLinks() // 獲取緩存過(guò)的關(guān)聯(lián)中的本地依賴 const chosenLinks = await getChosenLinks(localLinks, cacheLinks) // 用戶自行選擇需要關(guān)聯(lián)的本地依賴 await updateChosenLinks(chosenLinks) // 編寫(xiě) local-links.js 依賴文件,供 webpack 構(gòu)建時(shí) alias 使用 } catch (e) {}}
// webpack.config.jsconst baseConfig = { resolve: { alias: getAliasMap() }}// lib/util.jsfunction getAliasMap() { let aliasMap = { '@': path.resolve(cwd, './src') } const pjson = require(path.resolve(cwd, 'package.json')) Object.keys(pjson.dependencies || {}) .map(packageName => { return { key: packageName, value: path.resolve(cwd, 'node_modules', packageName) } }) .forEach(({ key, value }) => (aliasMap[key] = value)) const isLocalDev = process.env.IS_LOCAL_DEV if (isLocalDev) { try { let links = require(path.resolve(cwd, 'local-links')) || {} aliasMap = Object.assign({}, aliasMap, links) } catch (e) { console.log('invalid local links') } } return aliasMap}
?構(gòu)建 && 發(fā)布組件
- 如果有引入新的依賴,請(qǐng)先執(zhí)行 pnpm i
- 開(kāi)發(fā)完成后,正常在 mono 倉(cāng)庫(kù)下,進(jìn)行 git 提交
- 通過(guò)上述工具鏈實(shí)現(xiàn)的構(gòu)建器進(jìn)行發(fā)布
- 發(fā)布成功后,會(huì)根據(jù)代碼提交,進(jìn)行增量改動(dòng)判斷,產(chǎn)出對(duì)應(yīng)改動(dòng)的組件升級(jí)包
- 將相應(yīng)的包的版本號(hào),配置到應(yīng)用的項(xiàng)目中使用即可
原文鏈接:https://mp.weixin.qq.com/s/N0CZABDD0TKTmdljH3y74A
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻(xiàn),該文觀點(diǎn)僅代表作者本人。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請(qǐng)發(fā)送郵件至 舉報(bào),一經(jīng)查實(shí),本站將立刻刪除。