Monorepo 的過去、現(xiàn)在、和未來
大廠技術(shù)??高級前端??Node進(jìn)階
點擊上方?程序員成長指北,關(guān)注公眾號
回復(fù)1,加入高級Node交流群
Monorepo 的過去、現(xiàn)在、和未來
最近在準(zhǔn)備 QCon+ Monorepo 專題 的分享,突然發(fā)現(xiàn) Monorepo 方案其實一直在不斷演進(jìn),每一個階段都解決了上一個時代的數(shù)個核心問題。因此,這里簡單地寫一篇文章分享下 Monorepo 方案至今的進(jìn)化路線(真的非常非常之簡單)。
刀耕火種:偽 Monorepo
在這一時代,lerna 等方案還未出現(xiàn),除了各大公司如 Google、Meta 自建的巨型 Monorepo 方案以外,外界開發(fā)者常用的 Monorepo 方案就是將所有的項目直接放進(jìn)一個文件夾/倉庫下,比如這樣:
-?web-app
-?mobile-app
-?web-admin
-?miniprogram
-?node-server
-?node-bff
-?rpc
-?...
嚴(yán)格來說,這一組織方式并不屬于 Monorepo,它只是把所有項目放在一起,每個項目自己安裝一次依賴,如果項目之間存在依賴關(guān)系,需要自己手動 link 。項目的開發(fā)、發(fā)布、CI/CD 等同樣會讓你懷疑人生:在五個 Terminal 之間來回切換一個個啟動項目、手動改版本然后按照依賴項目-主體項目的順序發(fā)布、足夠你開幾把游戲的 CI/CD 時間。
使用這種方式和使用 Polyrepo,即一個項目一個倉庫的管理方式并沒有什么區(qū)別,因此我們這里稱它是偽·Monorepo。
要想成為一個真正意義上的 Monorepo,我理解至少有兩點入門門檻需要滿足:
項目之間存在自治的依賴關(guān)系。這里的自治,指的是不需要人工按照拓?fù)渑判蜻M(jìn)行項目之間的 link,而是有一定的自動化程度。比如在上面的例子里如果我們提供一個 script/link.js文件,那么也可以勉強認(rèn)為滿足了這一點。項目依賴關(guān)系的構(gòu)建主要是在開發(fā)時起到作用,如你可以直接在主項目中 link 子項目,然后對子項目的修改能夠立刻在主項目中產(chǎn)生效果。工程子項目的依賴( node_modules)之間并非是完全獨立的,對于版本一致的三方包,只應(yīng)該被安裝一次。以及,對于對單例有要求的依賴包如 React,版本之間存在沖突又可能并存的包如 Webpack,ESBuild / SWC / NodeSass 這一類非 JavaScript 編寫的依賴包,都需要得到妥善的處理。
總結(jié)一下其實就是這么兩個問題:
項目依賴關(guān)系構(gòu)建 工程依賴處理
實際上,這一刀耕火種階段的 Monorepo 方案其實現(xiàn)在很難見到使用了,畢竟這么做還不如拆分成一個項目一個倉庫的 Polyrepo 架構(gòu)。很快,我們迎來了新的、面向社區(qū)的 Monorepo 方案,它們很好地解決了上面的兩個問題,為我們帶來了真正的 Monorepo 開發(fā)體驗。
面向社區(qū):Google 、Meta、Microsoft 等公司的 Monorepo 方案其實此時還未開源出來,或者說并不適合社區(qū)的普通個人開發(fā)者 / 團隊來使用。
工業(yè)革命:Task Runner 與 Workspace
現(xiàn)在,應(yīng)該很少有沒有聽說過 lerna 的前端同學(xué)了,這里也不準(zhǔn)備介紹它的使用方式。如果還未了解,你可以閱讀官方文檔。
lerna 這一方案要解決的核心問題,其實正對應(yīng)著上面列出的兩個痛點。
對于項目依賴關(guān)系構(gòu)建,lerna 在初始化項目時(lerna bootstrap)會自動地為存在依賴關(guān)系的子項目創(chuàng)建 link,同時也支持了依賴關(guān)系創(chuàng)建(lerna add)、基于依賴關(guān)系的版本升級與發(fā)布(lerna version、lerna publish)等。lerna 其實還支持 --include-dependents 這一類全局選項,來快速從一個項目入口出發(fā),構(gòu)建基于其上游依賴或下游依賴的 Project Graph(更詳細(xì)地說,屬于有向無環(huán)圖) ,但確實用著比較麻煩(include / exclude,dependents / dependencies)。
實際上,這里的依賴關(guān)系,其最簡本質(zhì)也就是將一個有向無環(huán)圖(DAG)進(jìn)行拓?fù)渑判虻倪^程。對于部分 script,必須嚴(yán)格按照這一順序來執(zhí)行(如 start、build、test 等),否則會出現(xiàn)依賴引用的產(chǎn)物版本不一致等問題。
如果你只是需要任務(wù)調(diào)度能力,而不需要 lerna 的其他能力,可以使用一些輕量的替代方案,如 ultra-runner 等。
對于工程依賴,lerna 以及后面的 yarn workspace 都進(jìn)行了更好的處理,如共同依賴地提升(hoist)等。當(dāng)然,依賴提升等特性又帶來了新問題,如幽靈依賴(Phantom Dependencies)使得包可以去訪問未在 package.json 中聲明的依賴(如依賴的依賴),以及 Doppelgangers 問題中被重復(fù)安裝多次的依賴,但這些不是本文的重點,就不做展開講解了。
除了 Lerna + Yarn Workspace 以外,新晉的包管理器 pnpm 也有著自己的 Monorepo 方案,同時由于其獨特的依賴處理機制、強大的 filtering 語法(用 ... ^ 的簡寫形式代替了 dependents / dependencies,支持了基于 git 的 affected 檢測等),許多知名開源項目都在從 lerna 遷移向 pnpm workspace,如 Vue、Vite 等等。我個人的項目也基本上全部完成了到 pnpm workspace 的遷移,如我的起手式項目 starter-collections ,為了獲得更好的 pnpm workspace 下的開發(fā)體驗,我還為它開發(fā)了個 VS Code 擴展 pnpm-vscode-helper。
在過去,Lerna + Yarn Workspace 的形式是社區(qū)中使用率最高的 Monorepo 方案(應(yīng)該目前不需要加上之一)。在這一搭配中, lerna 負(fù)責(zé)任務(wù)調(diào)度,yarn workspace 負(fù)責(zé)依賴處理,看起來好像解決了所有問題,但程序員就是一個永遠(yuǎn)不會知足的團體,有一部分人開始思考,Monorepo 下的工作流是否還可以更絲滑?
當(dāng)下與未來:Monorepo Framework
要想獲得 Monorepo 下更好的工作流體驗,我們得先確認(rèn)目前有哪些不好的部分?
任務(wù)調(diào)度雖然很棒,但實際上開發(fā)中可能下游的依賴項目改動頻率比較低,雖然構(gòu)建器可能有緩存功能(tsc 等),但是我更希望直接跳過未發(fā)生改動的項目,極致地壓縮性能。 我需要在 package.json 中描述依賴關(guān)系,相當(dāng)于我還是要有一點心智負(fù)擔(dān)在,能否由 Task Runner 來自動分析依賴關(guān)系? 上面說了有些 script 是一定要按照拓?fù)渑判騺順?gòu)建的,但有些也是不用的,比如 lint 這種,就可以無視依賴關(guān)系并行執(zhí)行。這樣我的任務(wù)執(zhí)行是否能再快一些? 如果是大型的、多人甚至多團隊協(xié)作的大型 Monorepo 項目,那各個團隊的項目之間其實還是需要有一定的約束關(guān)系存在,比如我不希望別的團隊在沒經(jīng)過允許的情況下去修改我的代碼,也不希望未經(jīng)過允許就直接使用我團隊的基礎(chǔ)庫,然后出了問題又要找我修,我拒絕! 對于構(gòu)建開銷非常大的項目,如果多個團隊成員使用的是同一個版本,那么其實只需要有一個人構(gòu)建完畢,把產(chǎn)物上傳到云端,其他人開始構(gòu)建時直接下載此產(chǎn)物即可,這不比構(gòu)建快多了? ......
而要想解決這些問題,只靠 Task Runner 可就不夠了。我們需要的是一整套的、全生命周期覆蓋的解決方案,我這里將其稱為 Monorepo Framework(目前似乎沒看到別人這么稱呼,那我偷偷聲明一下原創(chuàng)權(quán)利!),目前我認(rèn)為可被稱作 Framework 級別的解決方案主要有這么幾個:
Nx,我個人最推薦的一個解決方案。作者是前 Google 工程師,團隊中也有許多專注于 Monorepo 的大廠成員。按照官方的敘述,Nx 吸收了許多 Google、Meta 內(nèi)部 Monorepo 方案的優(yōu)點。 Turborepo,2021 年的新起之秀,原是個人項目,后被 Vercel 收購。我個人認(rèn)為這是 Vercel 在前端工程中的進(jìn)一步開疆拓土,現(xiàn)在你的框架、倉庫管理、部署都可以只靠 Vercel 完成了。 Rush,微軟開源的 Monorepo 方案,我個人沒有做過比較深入的了解,這里不做評論。 一些企業(yè)級的解決方案,如 Google 的 Bazel、Gradle 這種,這些和前端的關(guān)系不是太大,上手成本也較高,這里同樣不做評論。
我個人比較想詳細(xì)介紹一下 Nx 和 Turborepo,我們來一點點說一下上面解決的問題。
任務(wù)緩存。Nx 和 Turborepo 都支持了任務(wù)的緩存(Local Computation Caching),但實現(xiàn)方式略有差異。Nx 對緩存計算使用了類似 React (VDOM)那樣的 diff 計算,性能方面要更好。 依賴關(guān)系分析。Nx 的依賴分析除了 package.json 以外,還會基于 AST 的分析來確定一個子項目的上下游依賴。而 Turborepo 則和此前的 workspace 方案類似,主要基于 package.json 中的 workspace:協(xié)議。可并行的任務(wù)調(diào)度。實際上這里的任務(wù)調(diào)度要比 lerna 這一類方案強大得多,如你可以更加自由的定義任務(wù)的執(zhí)行方式、前置后置任務(wù)等等。而 Turborepo 比 Nx 強大的地方在于,通過 pipeline 配置的方式,支持了更好的任務(wù)鏈計算,如這張圖片就能很好地說明其優(yōu)勢:
turborepo
實際上,Turborepo 的這一特色來自于微軟的 Lage,一個介于 Task Runner 和 Framework 之間的方案。說它只是 Runner 吧,它又支持了云端產(chǎn)物緩存,說它是 Framework 吧,它又沒有其他拿得出手的功能。
項目間引用約束。Nx 中支持為項目配置中添加 Tag,通過維護一份合法的 Tag 間引用關(guān)系以及 ESLint 插件配置,來實現(xiàn)項目之間的隔離,如 TeamA Tag 的項目不允許引用 TeamB Tag 項目。而 Turborepo 目前暫時不支持。 云端緩存。先說 Turborepo ,由于 Vercel 強大的 PaaS 能力,Turborepo 的構(gòu)建緩存可以非常自然地融入其中,但缺點在于鎖死了平臺,對于部分大廠來說并不能接受將自己的應(yīng)用代碼存儲在別的公司云端中。而 Nx 既提供了 Nx Cloud 這一方案,也提供了基于 Nx Cloud API 將云端緩存能力融入到內(nèi)部 CI/CD 流水線的方案,河南拔智齒。如果這一方案能夠在大廠中進(jìn)行落地,那么其已經(jīng)非常絲滑的構(gòu)建體驗又將再上一個等級。
除了這些能力以外,Monorepo Framework 其實還提供了許多貼心的功能。如 Nx 支持了 Angular 式的 Schematics 與 Builder,在 Nx 中這兩個被稱為 Generator 與 Executor。Generator 即快速生成項目起始模板或基礎(chǔ)代碼片段的能力,而 Executor 提供了項目的各個 script target 自定義執(zhí)行的能力,如你可以基于內(nèi)部的構(gòu)建器提供 Executor。通常來說,Nx 中通過 Plugin 的形式來整合同一框架級別的 Plugin,如 @nrwl/react, @nrwl/nest 等等。
這也是為何我更推崇 Nx 的原因,它允許你通過插件的形式非常自由地接入任何你想用但官方?jīng)]有提供集成的框架,這些集成可以是社區(qū)新秀如 Vite、SvelteKit,可以是非 Web 項目的 Electron、Ionic 等,甚至可以是其他語言如 Go 。
以上我們所講述的就是目前的 Monorepo Framework 方案,不妨放飛你的想象力,想想未來的 Monorepo 方案又會如何演進(jìn)?我個人大概有這么幾點盼望:
和包管理器的深度集成,上面我們說到的這些方案和包管理器仍然是割裂的,比如要想使用 pnpm workspace 和 nx ,就需要啟用 pnpm 的 shamefullyHoist,并且仍然可能會遇到問題。 和 Local Registry(如 Verdaccio)的深度集成,Monorepo 中開發(fā)項目時,如果就在 workspace 內(nèi)部開發(fā)還好,而一旦引用項目在 workspace 外部,就又要回歸到原始的 link 了。 更加開放的插件體系,Nx 做得挺好,但還不夠好。 從 Polyrepo 到 Monorepo 更好、更穩(wěn)定的遷移方案,比如給定幾個倉庫,能夠自動地按照 package.json 進(jìn)行依賴關(guān)系構(gòu)建,自動引入對應(yīng)框架的配置(甚至對于項目內(nèi)定制配置,也可以用過類 nx run-commands 的方式進(jìn)行遷移)。
很明顯,Monorepo 方案還有很長的路可以走,也希望更多的團隊與公司嘗試擁抱 Monorepo 方案。它不僅僅是一種項目管理方式,更是一種擁抱開放的態(tài)度。
Node 社群
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
如果你覺得這篇內(nèi)容對你有幫助,我想請你幫我2個小忙:
1. 點個「在看」,讓更多人也能看到這篇文章 2. 訂閱官方博客?www.inode.club?讓我們一起成長 點贊和在看就是最大的支持??
