詳解Monorepo:進(jìn)化、優(yōu)劣與使用場(chǎng)景
Hello,大家好,我是 Sunday。
訓(xùn)練營(yíng)同學(xué)在學(xué)習(xí) Vue3 或者 React 源碼的時(shí)候,可以發(fā)現(xiàn) Vue3 或者 React 的源碼是基于 monorepo(單體倉(cāng)庫(kù))架構(gòu)的。其所有相關(guān)的代碼和項(xiàng)目都被組織在同一個(gè)版本控制倉(cāng)庫(kù)中,同時(shí)又必須使用 pnpm 進(jìn)行管理。
那么為什么 Vue3 或者 React 要使用 monorepo 架構(gòu)呢? monorepo 又是什么?為什么 monorepo 要通過(guò) pnpm 進(jìn)行管理呢?這篇文章,咱們來(lái)看一下這些問(wèn)題。
Monorepo的本質(zhì)及其優(yōu)勢(shì)
Monorepo是軟件開(kāi)發(fā)中的一種代碼管理方法,它將多個(gè)項(xiàng)目集中到單個(gè)代碼倉(cāng)庫(kù)中。Monorepo為團(tuán)隊(duì)提供了簡(jiǎn)化的代碼共享、版本控制和部署流程,同時(shí)提高了可重用性和協(xié)作效率。這種方法已被廣泛采用,包括一些知名公司如Google、Facebook和Microsoft等。
Monorepo一詞源自希臘語(yǔ)"μ?νο?"(單一)和"repo"(代碼庫(kù))。雖然一開(kāi)始可能有些反直覺(jué),但將多個(gè)項(xiàng)目放置于同一代碼庫(kù)中確實(shí)帶來(lái)了許多好處。
Monorepo的演變與優(yōu)勢(shì)
Monorepo的發(fā)展經(jīng)歷了從單一倉(cāng)庫(kù)巨石應(yīng)用(Monolith),到多倉(cāng)庫(kù)多模塊應(yīng)用(MultiRepo),再到單倉(cāng)庫(kù)多模塊應(yīng)用(MonoRepo)的階段。每個(gè)階段都有其獨(dú)特的優(yōu)勢(shì)和挑戰(zhàn),具體采用哪種方式取決于項(xiàng)目的需求和團(tuán)隊(duì)的工作流程。
- 單倉(cāng)庫(kù)巨石應(yīng)用(Monolith):在項(xiàng)目初期,Monolith結(jié)構(gòu)比較常見(jiàn),因?yàn)樗写a都集中在一個(gè)倉(cāng)庫(kù)中,便于管理和部署。然而,隨著項(xiàng)目規(guī)模的增長(zhǎng),Monolith結(jié)構(gòu)逐漸顯現(xiàn)出構(gòu)建時(shí)間增加、代碼沖突頻繁以及難以維護(hù)等缺點(diǎn)。
- 多倉(cāng)庫(kù)多模塊應(yīng)用(MultiRepo):為了克服Monolith的缺點(diǎn),團(tuán)隊(duì)可能會(huì)將項(xiàng)目拆分成多個(gè)較小的模塊,每個(gè)模塊使用單獨(dú)的倉(cāng)庫(kù)管理。這種方式提高了模塊的獨(dú)立性,便于團(tuán)隊(duì)并行開(kāi)發(fā)和維護(hù),但也帶來(lái)了跨倉(cāng)庫(kù)依賴(lài)管理、版本同步問(wèn)題以及工作流程復(fù)雜性增加等新挑戰(zhàn)。
- 單倉(cāng)庫(kù)多模塊應(yīng)用(MonoRepo):為了解決多倉(cāng)庫(kù)管理帶來(lái)的問(wèn)題,一些團(tuán)隊(duì)和項(xiàng)目轉(zhuǎn)向使用單一倉(cāng)庫(kù)管理多個(gè)模塊。這種方式簡(jiǎn)化了跨模塊的依賴(lài)管理,提高了代碼共享效率,并統(tǒng)一了構(gòu)建和測(cè)試流程。然而,MonoRepo也面臨著更精細(xì)的權(quán)限控制、大型倉(cāng)庫(kù)性能優(yōu)化等挑戰(zhàn)。
在選擇適合項(xiàng)目的策略時(shí),需要綜合考慮團(tuán)隊(duì)規(guī)模、項(xiàng)目復(fù)雜度以及構(gòu)建測(cè)試流程的需求等因素。
圖片
一個(gè)真正的Monorepo不僅僅是將多個(gè)項(xiàng)目代碼放在同一個(gè)代碼庫(kù)中。它還需要這些項(xiàng)目之間有明確的關(guān)系和定義。如果項(xiàng)目之間缺乏良好的關(guān)系,那么就不能稱(chēng)之為Monorepo。
類(lèi)似地,如果一個(gè)代碼庫(kù)包含龐大的應(yīng)用,但沒(méi)有進(jìn)行合理的分割和封裝,那么這只是一個(gè)大型的代碼庫(kù),而不是真正的Monorepo。即使你給它取一個(gè)新的名字,也無(wú)法改變它的本質(zhì)。
Monorepo中的各個(gè)項(xiàng)目(或模塊、組件)之間應(yīng)該有清晰、明確的依賴(lài)關(guān)系和接口定義。這有助于確保模塊之間能夠高效協(xié)作,同時(shí)保持一定程度的獨(dú)立性和可重用性。
Monorepo 優(yōu)劣
圖片
圖片
Monorepo 使用場(chǎng)景
Monorepo(單一倉(cāng)庫(kù))模式適用于多種場(chǎng)景,特別是在以下情況下,使用 Monorepo 可以帶來(lái)顯著的好處:
- 大型團(tuán)隊(duì)協(xié)作:對(duì)于大型團(tuán)隊(duì)在多個(gè)相關(guān)項(xiàng)目上進(jìn)行協(xié)作時(shí),Monorepo 可以簡(jiǎn)化協(xié)作流程。所有項(xiàng)目位于同一倉(cāng)庫(kù)中,團(tuán)隊(duì)成員可以輕松訪問(wèn)和修改跨項(xiàng)目的代碼,促進(jìn)了團(tuán)隊(duì)間的溝通和合作。
- 微服務(wù)架構(gòu):在微服務(wù)架構(gòu)中,系統(tǒng)由多個(gè)小型、獨(dú)立服務(wù)組成。使用 Monorepo 可以方便地管理這些服務(wù)的代碼,確保服務(wù)之間的兼容性,并簡(jiǎn)化跨服務(wù)的重構(gòu)和共享代碼。
- 多平臺(tái)/多產(chǎn)品開(kāi)發(fā):對(duì)于跨多個(gè)平臺(tái)(如 Web、iOS、Android)或多個(gè)產(chǎn)品線開(kāi)發(fā)的公司,Monorepo 提供了一個(gè)統(tǒng)一的代碼基礎(chǔ)。這使得共享通用庫(kù)、組件和工具變得簡(jiǎn)單,同時(shí)保持構(gòu)建和發(fā)布流程的一致性。
- 共享庫(kù)和組件:在開(kāi)發(fā)涉及多個(gè)共享庫(kù)或可重用組件的項(xiàng)目時(shí),Monorepo 允許開(kāi)發(fā)人員輕松更新和維護(hù)這些共享資源。這有助于提高代碼重用率,降低維護(hù)成本。
- 統(tǒng)一的工具和流程:對(duì)于希望統(tǒng)一代碼風(fēng)格、構(gòu)建工具、測(cè)試框架和部署流程的團(tuán)隊(duì),Monorepo 提供了一個(gè)共同的基礎(chǔ)設(shè)施。這有助于標(biāo)準(zhǔn)化開(kāi)發(fā)實(shí)踐,簡(jiǎn)化新成員的入職過(guò)程。
- 原子性更改和重構(gòu):當(dāng)需要對(duì)跨多個(gè)項(xiàng)目或模塊的代碼進(jìn)行重構(gòu)或更新時(shí),Monorepo 使得這些更改可以作為一個(gè)原子提交進(jìn)行。這降低了部署和回滾的復(fù)雜性。
統(tǒng)一配置:整合 ESLint、TypeScript 和 Babel
在 Monorepo 項(xiàng)目中,統(tǒng)一配置 ESLint、TypeScript 和 Babel 可以有助于保持代碼一致性,簡(jiǎn)化項(xiàng)目維護(hù),并提高開(kāi)發(fā)效率。
TypeScript
我們可以在 packages 目錄中放置 tsconfig.settings.json 文件,并在文件中定義通用的 TypeScript 配置。然后,在每個(gè)子項(xiàng)目中,通過(guò) extends 屬性引入通用配置,并將 compilerOptions.composite 設(shè)置為 true。理想情況下,子項(xiàng)目的 tsconfig.json 文件應(yīng)該只包含以下內(nèi)容:
{
"extends": "../../tsconfig.settings.json", // 繼承通用配置
"compilerOptions": {
"composite": true, // 用于幫助 TypeScript 快速確定引用工程的輸出文件位置
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
ESLint
對(duì)于 ESLint,我們可以使用相同的思路來(lái)配置。在每個(gè)子項(xiàng)目的 .eslintrc.js 文件中,使用 extends 字段繼承頂層配置,并添加或覆蓋規(guī)則。
module.exports = {
extends: "../../.eslintrc.js",
rules: {
// 重寫(xiě)或添加規(guī)則
},
};
Babel
Babel 配置文件的合并方式與 TypeScript 類(lèi)似,甚至更加簡(jiǎn)單。我們只需在子項(xiàng)目的 .babelrc 文件中聲明如下:
{
"extends": "../../.babelrc"
}
當(dāng)所有配置準(zhǔn)備完畢時(shí),我們的項(xiàng)目目錄結(jié)構(gòu)大致如下所示:
├── package.json
├── .babelrc
├── .eslintrc
├── tsconfig.settings.json
└── packages/
│ ├── tsconfig.settings.json
│ ├── .babelrc
├── @mono/project_1/
│ ├── index.js
│ ├── .eslintrc
│ ├── .babelrc
│ ├── tsconfig.json
│ └── package.json
└───@mono/project_2/
├── index.js
├── .eslintrc
├── .babelrc
├── tsconfig.json
└── package.json
以上是統(tǒng)一配置 ESLint、TypeScript 和 Babel 的方法,通過(guò)這種方式,我們可以更輕松地管理和維護(hù) Monorepo 項(xiàng)目中的代碼。
為什么 vue3 || React 要使用 monorepo 架構(gòu)?
根據(jù)以上內(nèi)容所述,Vue3 采用 monorepo 架構(gòu)的決定主要基于以下幾個(gè)考慮:
- 便于代碼管理和共享: Monorepo 架構(gòu)使得不同模塊、組件、工具等相關(guān)的代碼可以統(tǒng)一存放在一個(gè)倉(cāng)庫(kù)中,便于管理和共享。這樣的架構(gòu)有助于更好地組織代碼結(jié)構(gòu),減少重復(fù)代碼,并使得不同模塊之間的依賴(lài)關(guān)系更清晰。
- 更簡(jiǎn)單的依賴(lài)管理: 在 monorepo 中,不同項(xiàng)目之間的依賴(lài)關(guān)系更加清晰,開(kāi)發(fā)人員可以更輕松地管理這些依賴(lài)關(guān)系,確保代碼庫(kù)的穩(wěn)定性和一致性。
- 易于協(xié)作和開(kāi)發(fā): 使用 monorepo 架構(gòu)可以促進(jìn)團(tuán)隊(duì)協(xié)作和開(kāi)發(fā)效率。開(kāi)發(fā)人員可以更方便地在不同項(xiàng)目之間共享代碼、解決問(wèn)題,并且可以更容易地進(jìn)行代碼審查和協(xié)作開(kāi)發(fā)。
- 更好的版本管理: 將相關(guān)的項(xiàng)目放在同一個(gè)倉(cāng)庫(kù)中,使得版本管理更加一致和統(tǒng)一。這樣做有助于確保不同模塊之間的版本兼容性,并使得發(fā)布和部署過(guò)程更加簡(jiǎn)單和可靠。
為什么 pnpm 能實(shí)現(xiàn) Monorepo
pnpm 利用其軟鏈接和硬鏈接功能實(shí)現(xiàn)了內(nèi)容尋址存儲(chǔ)的方法來(lái)保存依賴(lài)項(xiàng)。這種方法基于依賴(lài)項(xiàng)內(nèi)容的哈希值確定存儲(chǔ)位置,帶來(lái)了以下優(yōu)勢(shì):
- 依賴(lài)項(xiàng)共享:多個(gè)項(xiàng)目依賴(lài)相同版本的包時(shí),在全局存儲(chǔ)中只保留一份副本,通過(guò)硬鏈接指向這個(gè)副本,大大減少了磁盤(pán)空間的占用。
- 內(nèi)容完整性:內(nèi)容尋址存儲(chǔ)確保了依賴(lài)項(xiàng)的完整性。任何對(duì)文件內(nèi)容的更改都會(huì)導(dǎo)致哈希值的變化,防止了依賴(lài)污染和意外更改。
其中一個(gè)受大家比較歡迎的就是我們打開(kāi) pnpm 官網(wǎng)就能直接看到的內(nèi)容,那就是安裝快:
圖片
pnpm 在安裝依賴(lài)包時(shí),主要經(jīng)歷了以下三個(gè)步驟:解析依賴(lài)、獲取依賴(lài)以及鏈接依賴(lài)。這個(gè)過(guò)程通過(guò)優(yōu)化來(lái)確保高效的依賴(lài)管理,尤其在處理大型項(xiàng)目或 Monorepo 時(shí)。
- 解析依賴(lài)(Dependency Resolution) 在這個(gè)階段,pnpm 需要確定要安裝的每個(gè)依賴(lài)包的具體版本。它會(huì)查看項(xiàng)目的 package.json 文件以及任何現(xiàn)有的鎖文件(如 pnpm-lock.yaml),來(lái)決定哪些版本的包需要被安裝。解析依賴(lài)時(shí),pnpm 會(huì)遵循以下規(guī)則:
- 版本兼容性:基于 package.json 中指定的版本范圍,選擇與之兼容的最新版本。
- 鎖文件:如果存在鎖文件,pnpm 會(huì)優(yōu)先使用鎖文件中鎖定的版本,以確保依賴(lài)的一致性和項(xiàng)目的可重現(xiàn)性。
- 獲取依賴(lài)(Fetching Dependencies) 一旦確定了需要安裝的依賴(lài)版本,pnpm 將開(kāi)始獲取這些依賴(lài)包。這個(gè)過(guò)程包括以下幾個(gè)步驟:
- 檢查全局存儲(chǔ):pnpm 首先會(huì)檢查其全局存儲(chǔ)中是否已經(jīng)存在所需版本的依賴(lài)包。如果已經(jīng)存在,就不需要從遠(yuǎn)程倉(cāng)庫(kù)下載,直接重用即可。
- 下載缺失的依賴(lài):對(duì)于全局存儲(chǔ)中不存在的依賴(lài),pnpm 會(huì)從 npm 或其他配置的倉(cāng)庫(kù)下載它們。下載的依賴(lài)包會(huì)被存儲(chǔ)在全局存儲(chǔ)中,以便將來(lái)重用。
- 內(nèi)容尋址存儲(chǔ):pnpm 使用內(nèi)容尋址方式來(lái)存儲(chǔ)依賴(lài)包,即根據(jù)包內(nèi)容的哈希值來(lái)確定存儲(chǔ)路徑。這確保了相同內(nèi)容的包在全局存儲(chǔ)中只有一份副本,節(jié)省了磁盤(pán)空間。
- 鏈接依賴(lài)(Linking Dependencies) 獲取依賴(lài)包之后,pnpm 需要將這些依賴(lài)鏈接到項(xiàng)目的 node_modules 目錄中,使得項(xiàng)目能夠使用這些依賴(lài)。這個(gè)步驟涉及:
- 創(chuàng)建硬鏈接和符號(hào)鏈接:對(duì)于每個(gè)依賴(lài)包,pnpm 會(huì)在項(xiàng)目的 node_modules 目錄中創(chuàng)建指向全局存儲(chǔ)中相應(yīng)包的硬鏈接。如果是包內(nèi)部的依賴(lài),還可能創(chuàng)建符號(hào)鏈接來(lái)保持正確的依賴(lài)結(jié)構(gòu)。
- pnpm 通過(guò)構(gòu)建一個(gè)虛擬的 node_modules 目錄來(lái)模擬傳統(tǒng)的嵌套依賴(lài)結(jié)構(gòu),但實(shí)際上依賴(lài)之間是通過(guò)符號(hào)鏈接相連的。這樣做既保持了 npm 生態(tài)的兼容性,又避免了重復(fù)的依賴(lài)副本和深層嵌套的問(wèn)題。
- 通過(guò)這種鏈接方式,pnpm 確保了項(xiàng)目只能訪問(wèn)其直接依賴(lài)的包,防止了對(duì)未聲明依賴(lài)的意外訪問(wèn),提高了項(xiàng)目的穩(wěn)定性和安全性。
通過(guò)上述三個(gè)步驟,pnpm 實(shí)現(xiàn)了對(duì)依賴(lài)的高效管理,優(yōu)化了存儲(chǔ)空間的使用,加快了依賴(lài)安裝的速度,同時(shí)還保證了項(xiàng)目依賴(lài)的一致性和隔離性。
pnpm 在安裝依賴(lài)時(shí)能夠并行執(zhí)行多個(gè)任務(wù),比如解析依賴(lài)、下載和鏈接依賴(lài)。這種并行處理機(jī)制充分利用了現(xiàn)代多核 CPU 的性能,顯著減少了安裝過(guò)程的總時(shí)間。
pnpm 安裝速度快除了上面提到的這些原因之外,它的另一個(gè)優(yōu)點(diǎn)是它支持增量更新。當(dāng)你添加或更新項(xiàng)目依賴(lài)時(shí),pnpm 只會(huì)下載那些實(shí)際改變了的包。如果某個(gè)包的版本已經(jīng)存在于全局存儲(chǔ)中,pnpm 將重用這個(gè)版本,避免了不必要的下載,從而加快了安裝過(guò)程。
在 Monorepo 中,包之間經(jīng)常相互依賴(lài)。pnpm 通過(guò) Workspace 協(xié)議支持這種內(nèi)部依賴(lài),允許包在其 package.json 中直接引用 Monorepo 中的其他包,如:
"dependencies": {
"foo": "workspace:^1.0.0"
}
這種方式使得在本地開(kāi)發(fā)時(shí),包之間可以輕松地相互依賴(lài),而不需要發(fā)布到 npm 上。pnpm 會(huì)自動(dòng)處理這些內(nèi)部依賴(lài),并確保正確的鏈接和版本匹配。
在 workspace 模式下,項(xiàng)目根目錄通常不會(huì)作為一個(gè)子模塊或者 npm 包,而是主要作為一個(gè)管理中樞,執(zhí)行一些全局操作,安裝一些共有的依賴(lài),每個(gè)子模塊都能訪問(wèn)根目錄的依賴(lài),適合把 TypeScript、eslint 等公共開(kāi)發(fā)依賴(lài)裝在這里,下面簡(jiǎn)單介紹一些常用的中樞管理操作。
在項(xiàng)目跟目錄下運(yùn)行 pnpm install,pnpm 會(huì)根據(jù)當(dāng)前目錄 package.json 中的依賴(lài)聲明安裝全部依賴(lài),在 workspace 模式下會(huì)一并處理所有子模塊的依賴(lài)安裝。
安裝項(xiàng)目公共開(kāi)發(fā)依賴(lài),聲明在根目錄的 package.json - devDependencies 中。-w 選項(xiàng)代表在 monorepo 模式下的根目錄進(jìn)行操作。
// 安裝
pnpm install -wD xxx
// 卸載
pnpm uninstall -w xxx
執(zhí)行根目錄的 package.json 中的腳本
pnpm run xxx
在 workspace 模式下,pnpm 主要通過(guò) --filter 選項(xiàng)過(guò)濾子模塊,實(shí)現(xiàn)對(duì)各個(gè)工作空間進(jìn)行精細(xì)化操作的目的。
例如 a 包安裝 lodash 外部依賴(lài),-S 和 -D 選項(xiàng)分別可以將依賴(lài)安裝為正式依賴(lài)(dependencies)或者開(kāi)發(fā)依賴(lài)(devDependencies):
// 為 a 包安裝 lodash
pnpm --filter a add -S lodash // 生產(chǎn)依賴(lài)
pnpm --filter a add -D lodash // 開(kāi)發(fā)依賴(lài)
指定模塊之間的互相依賴(lài)。下面的例子演示了為 a 包安裝內(nèi)部依賴(lài) b。
// 指定 a 模塊依賴(lài)于 b 模塊
pnpm --filter a i -S b
pnpm workspace 對(duì)內(nèi)部依賴(lài)關(guān)系的表示不同于外部,它自己約定了一套 Workspace 協(xié)議。下面給出一個(gè)內(nèi)部模塊 a 依賴(lài)同是內(nèi)部模塊 b 的例子。
{
"name": "a",
// ...
"dependencies": {
"b": "workspace:^"
}
}
在實(shí)際發(fā)布 npm 包時(shí),workspace:^ 會(huì)被替換成內(nèi)部模塊 b 的對(duì)應(yīng)版本號(hào)(對(duì)應(yīng) package.json 中的 version 字段)。替換規(guī)律如下所示:
{
"dependencies": {
"a": "workspace:*", // 固定版本依賴(lài),被轉(zhuǎn)換成 x.x.x
"b": "workspace:~", // minor 版本依賴(lài),將被轉(zhuǎn)換成 ~x.x.x
"c": "workspace:^" // major 版本依賴(lài),將被轉(zhuǎn)換成 ^x.x.x
}
}