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