
前言:版本庫策略決定了一切
流程概覽
flowchart TD Q{版本庫策略<br/>選擇} Q -->|緊密耦合<br/>共用程式碼多<br/>團隊 < 15 人| MONO[Monorepo] Q -->|獨立部署<br/>技術棧多元<br/>團隊 > 50 人| MULTI[Multirepo] Q -->|混合需求| HYBRID[混合策略] MONO --> M1[單一倉庫] MONO --> M2[共享工具鏈] MONO --> M3[原子性變更] MULTI --> U1[獨立倉庫] MULTI --> U2[獨立 CI/CD] MULTI --> U3[團隊自治] HYBRID --> H1[相關服務 Monorepo] HYBRID --> H2[獨立系統 Multirepo] style Q fill:#FF9800,color:#fff style MONO fill:#2196F3,color:#fff style MULTI fill:#4CAF50,color:#fff style HYBRID fill:#9C27B0,color:#fff
版本庫(Repository)的組織方式,看似只是「程式碼放哪裡」的問題,實際上卻深刻影響著團隊的每一個工作面向:
- CI/CD 速度:一個擁有百萬行程式碼的 Monorepo,每次 push 都要跑完整個 pipeline?還是每個小 repo 只跑自己的 30 秒 build?
- 程式碼共享:跨專案共用的 utility、UI 元件、型別定義,要怎麼管理版本與相依性?
- 團隊協作:20 個工程師同時在同一個 repo 裡面改 code,merge conflict 滿天飛?還是各自在獨立的 repo 中自由開發?
- 部署獨立性:改了 shared library 的一行程式碼,是要全部服務重新部署,還是只有受影響的服務需要更新?
這些問題沒有標準答案。但理解 Monorepo 和 Multirepo 各自的優勢與代價,能幫助你在專案初期就做出更好的架構決策,避免日後痛苦的遷移。
架構全景:Monorepo vs Multirepo
以下用一張圖來呈現兩種策略的核心差異:
graph TB subgraph Monorepo["Monorepo(單一版本庫)"] direction TB MR["my-company-repo/"] MR --> PA["packages/ui-library"] MR --> PB["packages/utils"] MR --> PC["packages/api-client"] MR --> AA["apps/web-app"] MR --> AB["apps/mobile-app"] MR --> AC["apps/admin-panel"] MR --> SA["shared/config"] MR --> SB["shared/types"] end subgraph Multirepo["Multirepo(多版本庫)"] direction TB R1["repo: ui-library"] R2["repo: utils"] R3["repo: api-client"] R4["repo: web-app"] R5["repo: mobile-app"] R6["repo: admin-panel"] R4 -.->|"npm install"| R1 R4 -.->|"npm install"| R2 R5 -.->|"npm install"| R3 R6 -.->|"npm install"| R1 end style Monorepo fill:#E3F2FD,stroke:#1565C0 style Multirepo fill:#FFF3E0,stroke:#E65100
Monorepo 把所有專案放在同一個 Git 倉庫中,透過目錄結構來區隔;Multirepo 則讓每個專案擁有自己獨立的 Git 倉庫,透過套件管理器(npm/yarn/pnpm)來管理跨專案的相依關係。
核心概念
Monorepo:一個倉庫,多個專案
Monorepo(Monolithic Repository)是指將多個專案、套件或服務放在同一個版本控制倉庫中的策略。業界最知名的採用者包括:
- Google:整個公司超過 20 億行程式碼放在單一倉庫中,使用自研的 Piper 系統管理
- Meta(Facebook):使用自研的 Buck 建構系統搭配 Mercurial
- Microsoft:Windows 程式碼庫使用 GVFS(Git Virtual File System)來管理超大型 Monorepo
- Uber、Twitter、Airbnb:也都是 Monorepo 的擁護者
Monorepo 的目錄結構通常如下:
my-monorepo/
├── apps/ # 應用程式
│ ├── web/ # 前端 Web 應用
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── api/ # 後端 API 服務
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── admin/ # 管理後台
│ ├── src/
│ ├── package.json
│ └── tsconfig.json
├── packages/ # 共用套件
│ ├── ui/ # UI 元件庫
│ │ ├── src/
│ │ │ ├── Button/
│ │ │ ├── Modal/
│ │ │ └── index.ts
│ │ └── package.json
│ ├── utils/ # 工具函式
│ │ ├── src/
│ │ │ ├── date.ts
│ │ │ ├── format.ts
│ │ │ └── index.ts
│ │ └── package.json
│ └── api-client/ # API 客戶端 SDK
│ ├── src/
│ └── package.json
├── shared/ # 全域共用設定
│ ├── eslint-config/ # 統一 ESLint 規則
│ ├── tsconfig/ # 統一 TypeScript 設定
│ └── jest-config/ # 統一測試設定
├── package.json # Root package.json
├── pnpm-workspace.yaml # Workspace 設定
├── turbo.json # Turborepo 設定
└── nx.json # 或 Nx 設定
Multirepo(Polyrepo):每個專案獨立一個倉庫
Multirepo 策略讓每個專案或服務擁有自己獨立的 Git 倉庫。共用程式碼以 npm package 的形式發佈到 registry(npm、GitHub Packages 或私有 registry),其他專案透過 npm install 來引用。
這也是大多數開源專案和中小型團隊最自然的選擇。
Monorepo 解決了什麼問題?
1. 原子性變更(Atomic Changes)
想像一個情境:你的 api-client 套件新增了一個欄位,前端的 web-app 和 admin 都需要配合修改。
- Multirepo 的做法:先在
api-clientrepo 改 code → 發版 v1.2.3 → 到web-apprepo 更新相依版本 → 改前端 code → 再到adminrepo 做同樣的事。三個 PR、三次 code review、三次部署,中間任何一步漏掉都會造成版本不一致。 - Monorepo 的做法:一個 PR 同時改
api-client、web-app、admin,一次 review、一次合併,保證所有變更是原子性的。
2. 統一的 CI/CD Pipeline
所有專案共用同一套 CI 設定,不需要在每個 repo 都維護一份 .gitlab-ci.yml 或 GitHub Actions。工具鏈的升級(ESLint 規則、TypeScript 版本、測試框架)也只需要改一次。
3. 共用工具鏈與設定
ESLint、Prettier、TypeScript、Jest 的設定只需要維護一份。新加入團隊的成員 clone 一個 repo 就能開始開發所有專案,不需要搞清楚十幾個 repo 各自的開發環境設定。
4. 消除版本不一致(Dependency Hell)
在 Multirepo 中,web-app 用 @company/utils@1.2.0,admin 用 @company/utils@1.3.0,這種版本飄移(version drift)非常常見。在 Monorepo 中,所有專案永遠使用同一版本的共用套件。
Monorepo 創造了什麼問題?
1. CI/CD 速度急劇下降
當倉庫裡有 50 個套件和 10 個應用,每次 push 都要跑完整的 lint、test、build 嗎?即使只改了一行 README?
雖然 Nx 和 Turborepo 透過「affected analysis」和「task caching」大幅改善了這個問題,但這些工具本身也帶來了額外的學習成本與維護成本。
2. 複雜的工具鏈
要讓 Monorepo 順暢運作,你需要:
- Workspace 管理(pnpm workspace / yarn workspace / npm workspace)
- 建構排程(Nx / Turborepo / Lerna)
- 變更偵測(only build what changed)
- 遠端快取(Nx Cloud / Turborepo Remote Cache)
- 可能還需要自訂的 Git hooks 和 CI 腳本
對於一個三人小團隊來說,這些工具鏈的投資可能遠超過它帶來的收益。
3. 爆炸半徑(Blast Radius)
在 Monorepo 中,一個糟糕的 commit 可能影響所有專案。一個 breaking change 被合併進 shared/utils,所有依賴它的 10 個應用都會同時壞掉。
4. 耦合的誘惑
Monorepo 讓跨專案引用變得太容易了。工程師可能會直接 import 另一個應用的內部模組,而不是透過定義清楚的 public API。久而久之,專案之間的邊界模糊,耦合越來越深,最終變成一個巨大的泥球(Big Ball of Mud)。
5. Git 效能問題
當倉庫變得非常大(數十萬個檔案、數十萬個 commit),git clone、git status、git log 都會變慢。雖然 git sparse-checkout 和 git shallow clone 可以緩解,但這又是額外的複雜度。
為什麼有些團隊正在回歸小專案控管?
近年來,一個有趣的趨勢是:一些原本擁抱 Monorepo 的團隊開始將部分服務拆回獨立的 repo。這背後的原因包括:
微服務的獨立性
微服務架構的核心精神是「各自獨立部署、獨立演進」。當一個微服務被鎖在 Monorepo 裡面,它的 CI/CD pipeline、部署節奏、技術選型都被其他服務牽制。拆出去之後,團隊可以自由選擇框架、語言甚至建構工具。
更簡單的 CI/CD
獨立 repo 的 CI 設定就是簡簡單單的「lint → test → build → deploy」,不需要 affected analysis、task graph、remote cache 這些額外層級。CI 壞了很容易排查,因為範圍就在這個 repo 裡面。
團隊自治(Team Autonomy)
在大型組織中,不同團隊負責不同的服務。如果所有服務都在同一個 Monorepo,團隊的 code review 規則、分支策略、release 節奏都被迫統一。拆分成獨立 repo 後,每個團隊可以按照自己的節奏和方式工作。
權限控管更直覺
在 Monorepo 中,要實現「團隊 A 只能改 apps/web 目錄」這種權限控管需要額外的 CODEOWNERS 設定和 CI 檢查。在 Multirepo 中,repo 層級的權限就是最自然的隔離邊界。
決策框架:何時該用哪種策略?
選擇 Monorepo 的時機
| 條件 | 說明 |
|---|---|
| 緊密耦合的服務 | 前端 + BFF + 共用型別,經常需要一起改 |
| 共用基礎設施多 | UI 元件庫、工具函式、設定檔等共用程式碼佔比高 |
| 團隊規模小 | 少於 15-20 人,溝通成本低 |
| 技術棧統一 | 全部都是 TypeScript / Python / Go,不需要多種建構系統 |
| 發版節奏一致 | 所有服務通常一起部署 |
選擇 Multirepo 的時機
| 條件 | 說明 |
|---|---|
| 微服務邊界清晰 | 各服務之間透過 API 溝通,幾乎不共用程式碼 |
| 組織規模大 | 超過 5 個團隊、50 人以上,需要團隊自治 |
| 部署節奏不同 | 有些服務一天部署 10 次,有些一個月才部署一次 |
| 技術棧多元 | 前端用 TypeScript、後端用 Go、ML 用 Python |
| 開源或跨組織協作 | 需要公開的 repo 讓外部貢獻者參與 |
混合策略(Hybrid Approach)
實務上,很多成熟的團隊會採用混合策略:
- Monorepo A:前端應用群(web、admin、mobile)+ 共用 UI 元件 + 共用工具函式
- Monorepo B:後端微服務群(user-service、order-service、payment-service)+ 共用 proto 定義
- 獨立 Repo C:ML Pipeline(完全不同的技術棧和部署流程)
- 獨立 Repo D:Infrastructure as Code(Terraform/Pulumi,由 SRE 團隊獨立管理)
這種策略保留了相關服務之間的 Monorepo 優勢,同時讓本質上獨立的系統擁有完整的自主權。
Monorepo 工具鏈比較
Nx
Nx 是目前功能最完整的 Monorepo 工具,由 Nrwl 團隊開發(多位成員來自 Angular 團隊)。
核心特色:
- 智慧快取(Local & Remote Caching)
- 相依圖分析(Dependency Graph Visualization)
- 程式碼產生器(Code Generators)
- 支援多種框架(React、Angular、Vue、Node.js、Go)
- 受影響分析(Affected Commands)
Nx Workspace 設定範例:
// nx.json
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"],
"production": [
"default",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json",
"!{projectRoot}/jest.config.[jt]s"
]
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"],
"cache": true
},
"test": {
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
"cache": true
},
"lint": {
"inputs": [
"default",
"{workspaceRoot}/.eslintrc.json",
"{workspaceRoot}/.eslintignore"
],
"cache": true
}
},
"nxCloudAccessToken": "your-nx-cloud-token",
"defaultBase": "main"
}使用 Nx 時,只建構受影響的專案:
# 只測試受 main 分支以來變更影響的專案
npx nx affected -t test --base=main
# 視覺化相依圖
npx nx graph
# 只建構特定專案及其相依
npx nx build web-appTurborepo
Turborepo 由 Vercel 收購並維護,主打「簡單、快速」。相比 Nx 的全功能框架,Turborepo 更像是一個輕量級的建構排程器。
Turborepo Pipeline 設定範例:
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"globalEnv": ["NODE_ENV", "API_URL"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json", "package.json"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"env": ["NODE_ENV", "API_URL"]
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**", "test/**", "**/*.test.*"],
"outputs": ["coverage/**"],
"env": ["CI"]
},
"lint": {
"dependsOn": ["^build"],
"inputs": ["src/**", ".eslintrc.*", "tsconfig.json"],
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
},
"type-check": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json"],
"outputs": []
}
}
}Lerna
Lerna 是最早期的 JavaScript Monorepo 工具之一,現在由 Nx 團隊接手維護。它的核心功能是管理多套件的版本發佈。
適用場景:如果你的 Monorepo 主要是一組 npm 套件,需要統一管理版本號和發佈流程,Lerna 仍然是不錯的選擇。
Bazel
Bazel 是 Google 開源的建構系統,是 Google 內部 Blaze 的外部版本。
優點:極致的建構快取、跨語言支援(Java、C++、Go、Python、JS)、遠端執行。 缺點:學習曲線極陡、設定複雜、生態系較小。
適用場景:超大型組織、多語言 Monorepo、需要極致的建構效能。
工具比較總覽
| 特性 | Nx | Turborepo | Lerna | Bazel |
|---|---|---|---|---|
| 學習曲線 | 中等 | 低 | 低 | 極高 |
| 快取 | Local + Remote | Local + Remote | 透過 Nx | Hermetic Cache |
| 相依圖 | 視覺化 | 有 | 基礎 | 強大 |
| 程式碼產生 | 內建 | 無 | 無 | 有 |
| 多語言 | 有限 | JS/TS 為主 | JS/TS | 完整支援 |
| 社群生態 | 豐富 | 成長中 | 成熟 | 較小 |
| 適合規模 | 中大型 | 小中型 | 小型 | 超大型 |
GitLab CI/CD 搭配 Monorepo
在 Monorepo 中,CI 最重要的優化是「只建構受變更影響的套件」。以下是一個 GitLab CI 的範例:
# .gitlab-ci.yml — Monorepo CI with change detection
stages:
- detect
- lint
- test
- build
- deploy
variables:
NX_BASE: $CI_MERGE_REQUEST_DIFF_BASE_SHA
NX_HEAD: $CI_COMMIT_SHA
# 偵測哪些專案受到影響
detect-changes:
stage: detect
image: node:20-alpine
script:
- npx nx show projects --affected --base=$NX_BASE --head=$NX_HEAD > affected.txt
- echo "Affected projects:"
- cat affected.txt
artifacts:
paths:
- affected.txt
# 只 lint 受影響的專案
lint:affected:
stage: lint
image: node:20-alpine
needs: ["detect-changes"]
script:
- pnpm install --frozen-lockfile
- npx nx affected -t lint --base=$NX_BASE --head=$NX_HEAD --parallel=3
rules:
- if: $CI_MERGE_REQUEST_ID
- if: $CI_COMMIT_BRANCH == "main"
# 只測試受影響的專案
test:affected:
stage: test
image: node:20-alpine
needs: ["detect-changes"]
script:
- pnpm install --frozen-lockfile
- npx nx affected -t test --base=$NX_BASE --head=$NX_HEAD --parallel=3 --ci
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
artifacts:
reports:
junit: "**/junit.xml"
coverage_report:
coverage_format: cobertura
path: "**/coverage/cobertura-coverage.xml"
# 只建構受影響的專案
build:affected:
stage: build
image: node:20-alpine
needs: ["lint:affected", "test:affected"]
script:
- pnpm install --frozen-lockfile
- npx nx affected -t build --base=$NX_BASE --head=$NX_HEAD --parallel=3
artifacts:
paths:
- apps/*/dist/
- packages/*/dist/
# 部署特定應用(只在 main 分支且該應用受影響時觸發)
deploy:web-app:
stage: deploy
needs: ["build:affected"]
script:
- |
if grep -q "web-app" affected.txt; then
echo "Deploying web-app..."
# 你的部署指令
else
echo "web-app not affected, skipping deploy."
fi
rules:
- if: $CI_COMMIT_BRANCH == "main"演進敘事:從單一 Repo 到 Monorepo,再回到小 Repo
版本庫策略的演進並不是一條直線,而是一個鐘擺。理解這個鐘擺的擺動規律,能幫助你在不同階段做出正確的判斷。
階段一:什麼都放在一起(2000s - 2010s)
早期的軟體專案通常就是一個 repo 一個應用。隨著專案變大,前端、後端、shared library 自然而然地放在同一個 repo 裡面。這本質上就是一個未經管理的 Monorepo。
問題:沒有模組邊界,everything depends on everything。部署就是整包丟上去。
階段二:拆分成微服務 + Multirepo(2010s - 2015s)
微服務架構興起後,團隊開始將大型應用拆分成獨立的服務,每個服務一個 repo。這帶來了部署獨立性和團隊自治。
問題:跨服務的變更變得痛苦,版本飄移嚴重,每個 repo 的工具鏈和設定各自為政。
階段三:擁抱 Monorepo(2015s - 2020s)
看到 Google、Meta 等巨頭的成功案例,加上 Lerna、Nx、Turborepo 等工具的成熟,許多團隊開始將專案合併到 Monorepo 中。
問題:CI 變慢、工具鏈變複雜、Git 效能下降、團隊自治被限制。
階段四:務實的混合策略(2020s - 現在)
經歷過前幾個階段的洗禮後,越來越多團隊開始採用務實的混合策略:
- 相關性高的專案放在同一個 Monorepo
- 獨立性高的服務放在自己的 repo
- 共用套件視情況決定:如果只有 2-3 個消費者,放在 Monorepo;如果有 10+ 個消費者,獨立發佈到 npm registry
這個鐘擺告訴我們:沒有銀彈,只有 trade-off。
常見問題與風險
1. Monorepo 的 Git Clone 太慢怎麼辦?
使用 git sparse-checkout 只下載你需要的目錄,搭配 --filter=blob:none 實現 partial clone:
git clone --filter=blob:none --sparse https://github.com/my-org/monorepo.git
cd monorepo
git sparse-checkout set apps/web-app packages/ui packages/utils2. Multirepo 的版本一致性怎麼維持?
使用 Renovate 或 Dependabot 自動化相依版本升級,搭配語意化版本(Semantic Versioning)和 Changelog 自動產生。
3. Monorepo 的權限控管怎麼做?
使用 GitHub CODEOWNERS 或 GitLab Code Owners 功能,搭配 CI 中的路徑觸發規則(path-based triggers),確保只有負責的團隊能核准特定目錄的變更。
4. 從 Multirepo 遷移到 Monorepo 的步驟?
- 選擇 Monorepo 工具(推薦 Nx 或 Turborepo)
- 建立 Monorepo 骨架(root package.json、workspace 設定)
- 逐步將 repo 搬入,保留 Git 歷史(使用
git subtree或手動搬移) - 統一工具鏈設定(ESLint、TypeScript、Jest)
- 設定 CI/CD 的受影響偵測
- 將 npm package 的跨 repo 依賴改為 workspace 內部依賴
5. Monorepo 中不同專案要用不同的 Node.js 版本怎麼辦?
使用 Docker 化的 CI pipeline,或透過 volta、fnm 等工具管理每個專案的 Node.js 版本。不過這確實增加了複雜度,如果版本差異很大,可能是該拆分成獨立 repo 的信號。
決策檢查清單
在做出最終決定之前,問自己以下問題:
- 你的團隊有多少人?超過 30 人可能需要考慮 Multirepo 的團隊自治優勢
- 你的服務之間有多少共用程式碼?共用越多,Monorepo 優勢越大
- 你的部署節奏是否一致?不一致的話,Monorepo 可能造成不必要的牽制
- 你有足夠的基礎設施能力維護 Monorepo 工具鏈嗎?
- 你的 CI pipeline 能在 10 分鐘內完成嗎?超過的話需要投資快取與受影響偵測
- 團隊成員是否熟悉 Nx / Turborepo?學習成本是否可接受?
總結
| 面向 | Monorepo | Multirepo |
|---|---|---|
| 原子性變更 | 天然支援 | 需要跨 repo 協調 |
| CI/CD 複雜度 | 高(需要受影響偵測) | 低(各自獨立) |
| 程式碼共享 | 簡單直接 | 需要發佈套件 |
| 團隊自治 | 較受限 | 天然支援 |
| 部署獨立性 | 需要額外設計 | 天然支援 |
| 工具鏈投資 | 高 | 低 |
| 新人上手 | Clone 一次全部搞定 | 需要 clone 多個 repo |
| Git 效能 | 隨規模下降 | 始終穩定 |
最終建議:不要因為 Google 用 Monorepo 就跟著用,也不要因為 Monorepo 聽起來很酷就跟著用。評估你的團隊規模、服務耦合度、技術棧統一程度和 CI 基礎設施,選擇最務實的策略。如果不確定,從 Multirepo 開始通常是更安全的選擇——把東西合在一起永遠比拆開來容易。
Proto 實踐對照
目前 Proto 採用 Multi-repo 策略:Django / Vue / Flutter 各自獨立 repo,Infra 層也是獨立 repo。Proto 透過 Git Submodule 被 Infra 拉入部署流程。這個策略適合目前的規模,但如果未來 Proto 數量增多(多語言 + 多 Track),可以考慮遷移到 Monorepo。詳見 Proto 規劃方法論 的多語言延伸策略。
延伸閱讀
- CD Templates:了解如何設定 Monorepo 的 CI/CD Pipeline
- Proto Planning:跨服務的 API 契約管理,與版本庫策略密切相關
- Nx 官方文件
- Turborepo 官方文件
- Google’s Monorepo Paper:Why Google Stores Billions of Lines of Code in a Single Repository