Monorepo 构建工具链
Monorepo 构建工具链
写在前面
当公司项目从单个应用变成多个共享代码的应用(组件库、工具库、多个产品线),Monorepo 成了必然选择。但 Monorepo 的工程化挑战是:怎么高效地构建、测试、发布这么多包?
本篇会讲清楚 Monorepo 的本质,对比主流工具(Turborepo、Nx、pnpm workspace、Changesets),以及实战优化 6 个子包项目构建的全过程。
一、什么是 Monorepo
1.1 单仓 vs 多仓
# 多仓 (Polyrepo)
git:org/ui-library ← 独立仓库
git:org/utils-lib ← 独立仓库
git:org/web-app ← 独立仓库
git:org/admin-app ← 独立仓库
# 单仓 (Monorepo)
git:org/monorepo
├── packages/
│ ├── ui-library/
│ ├── utils-lib/
│ └── config/
├── apps/
│ ├── web-app/
│ └── admin-app/
└── pnpm-workspace.yaml
1.2 Monorepo 的优势
| 优势 | 说明 |
|---|---|
| 代码共享 | 直接 import,不需要发包 |
| 统一版本 | 一次升级,所有包升级 |
| 原子提交 | 一次 PR 改多个包 |
| 重构容易 | 跨包重构一气呵成 |
| 依赖管理 | 统一 node_modules,磁盘省一半 |
1.3 Monorepo 的挑战
| 挑战 | 说明 |
|---|---|
| 构建慢 | 改一行代码可能影响 100 个包 |
| 依赖管理 | 跨包依赖关系复杂 |
| 权限管理 | 怎么限制谁能改哪些包 |
| CI 慢 | 跑一次测试可能 30 分钟 |
| git 体积 | 大仓库 git clone 慢 |
构建工具链就是为了解决这些挑战。
二、pnpm workspace
2.1 pnpm 是什么
pnpm 是新一代 Node.js 包管理器,比 npm 快 2x,比 yarn 快 1.5x。
但 pnpm 的核心价值是内容寻址存储(content-addressable storage):
# 同样的 lodash 在多个项目里只存一份
~/.pnpm-store/
├── node_modules/.pnpm/lodash@4.17.21/
磁盘占用降低 50%+。
2.2 pnpm workspace 配置
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
- 'libs/*'
- '!**/test/**'
2.3 项目结构
monorepo/
├── pnpm-workspace.yaml
├── package.json
├── pnpm-lock.yaml
├── apps/
│ ├── web-app/
│ │ └── package.json
│ └── admin-app/
│ └── package.json
└── packages/
├── ui-library/
│ └── package.json
├── utils-lib/
│ └── package.json
└── config/
└── package.json
2.4 workspace 依赖
// apps/web-app/package.json
{
"name": "web-app",
"dependencies": {
"ui-library": "workspace:*",
"utils-lib": "workspace:*",
"react": "^18.2.0"
}
}
workspace:* 表示引用本地包。
// packages/ui-library/package.json
{
"name": "ui-library",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
}
}
2.5 workspace 命令
# 在所有包里跑命令
pnpm -r run build
# 只在某个包里跑
pnpm --filter web-app run dev
# 在依赖某个包的所有包里跑
pnpm --filter "...ui-library" run build
# 跑所有包的 test
pnpm -r run test
三、Turborepo
3.1 是什么
Turborepo 是 Vercel 出品的 Monorepo 构建系统——核心是任务编排 + 远程缓存。
3.2 配置
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"globalEnv": ["NODE_ENV"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"lint": {},
"dev": {
"cache": false,
"persistent": true
},
"clean": {
"cache": false
}
}
}
3.3 关键概念
Pipeline(任务管道)
定义每个任务的依赖关系:
"^build"表示依赖上游包的 build"dependsOn": ["^build"]表示跑这个任务前,先跑依赖
Outputs(产物声明)
声明任务的输出文件,这些文件会进缓存。
Inputs(输入声明)
{
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "package.json", "tsconfig.json"],
"outputs": ["dist/**"]
}
}
只考虑声明的文件——其他文件变化不触发重跑。
3.4 使用
# 跑所有 build
turbo run build
# 跑特定包
turbo run build --filter=web-app
# 跑依赖某个包的所有包
turbo run build --filter=...ui-library
# 并行度
turbo run build --concurrency=4
3.5 远程缓存
Turborepo 的杀手锏——远程缓存。
本地构建 → 哈希所有输入 → 检查远程缓存
├─ 命中:从远程下载产物
└─ 未命中:本地构建,上传产物到远程
团队成员共享构建缓存:
- 张三构建过的代码,李四不用重新构建
- CI 上的缓存任何机器都能用
配置远程缓存
# Vercel 远程缓存(免费)
npx turbo login
npx turbo link
# 自建远程缓存(S3 / NAS)
turbo.json
{
"remoteCache": {
"signature": true,
"preflight": false,
"uploadTimeout": 30000
}
}
# 环境变量
TURBO_TOKEN=xxx
TURBO_TEAM=my-team
配合 CI 用 Vercel 远程缓存,免费。
四、Nx
4.1 是什么
Nx 是 Nrwl 出品的 Monorepo 构建系统——比 Turborepo 更强大但更复杂。
4.2 Nx 的优势
- 更智能的依赖图分析
- 代码生成器(generators)
- 插件生态丰富(React、Next、Express、Storybook……)
- 分布式任务执行
- 影响范围分析(affected commands)
4.3 配置
// nx.json
{
"npmScope": "myorg",
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test", "lint"],
"parallel": 4
}
}
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"outputs": ["{projectRoot}/dist"]
}
}
}
4.4 Affected commands
# 只跑受当前改动影响的包
nx affected --target=test
# 看哪些包受影响
nx affected:graph
核心价值:在大型 monorepo 里,改一行代码不需要跑全部包的测试。
4.5 Nx Cloud(付费)
类似 Vercel 的远程缓存,但功能更多:
- 分布式任务执行
- 缓存命中率分析
- 影响图谱可视化
个人项目免费,企业项目付费。
五、Turborepo vs Nx 对比
| 维度 | Turborepo | Nx |
|---|---|---|
| 上手难度 | ⭐(简单) | ⭐⭐⭐(复杂) |
| 配置 | turbo.json | nx.json + project.json |
| 远程缓存 | ✅(免费 + 付费) | ✅(付费) |
| 任务编排 | ✅ | ✅ |
| Affected | ✅(基础) | ✅(智能) |
| 代码生成器 | ❌ | ✅ |
| 插件生态 | ❌ | ✅ |
| 分布式执行 | ❌ | ✅ |
| 性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
5.1 选型
- 小团队 / 简单项目 → Turborepo
- 大型项目 / 需要代码生成 → Nx
- 已经在用 Angular → Nx(无缝)
六、实战:把 6 个子包构建时间砍半
6.1 背景
公司项目结构:
monorepo/
├── apps/
│ ├── web-app (React + Vite)
│ ├── admin-app (React + Rsbuild)
│ └── docs-site (Astro)
├── packages/
│ ├── ui-library (React 组件库)
│ ├── utils-lib (工具函数)
│ └── eslint-config (共享配置)
6.2 优化前
$ turbo run build
• ui-library:build 30s
• utils-lib:build 5s
• eslint-config:build 2s
• web-app:build 90s
• admin-app:build 60s
• docs-site:build 40s
─────────────────────────
Total: 227s
6.3 第一步:依赖图梳理
web-app 依赖 ui-library、utils-lib、eslint-config。admin-app 同理。
Turborepo 自动分析:先构建 ui-library,再构建 web-app。
6.4 第二步:开启远程缓存
# 接入 Vercel 远程缓存(免费)
npx turbo login
npx turbo link
第一次构建:227s(建立缓存) 第二次构建:8s(全部命中缓存)
6.5 第三步:优化 inputs
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"inputs": [
"src/**",
"package.json",
"tsconfig.json",
"vite.config.ts"
],
"outputs": ["dist/**"]
}
}
}
明确 inputs 让缓存命中率更高。
6.6 第四步:拆分 build 和 test
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"inputs": [...],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"], // 测试依赖 build
"outputs": ["coverage/**"]
},
"lint": {
"outputs": []
}
}
}
这样 lint 不需要等 build——可以并行跑。
6.7 第五步:CI 集成
# .github/workflows/ci.yml
- name: Build
run: turbo run build --concurrency=4
- name: Test
run: turbo run test --concurrency=4
- name: Lint
run: turbo run lint
CI 也享受远程缓存。
6.8 优化后
$ turbo run build (本地,首次)
• ui-library:build 30s
• utils-lib:build 5s
• eslint-config:build 2s
• web-app:build 90s
• admin-app:build 60s
• docs-site:build 40s
─────────────────────────
Total: 227s
$ turbo run build (本地,二次,缓存命中)
Total: 8s
$ turbo run build (CI,远程缓存命中)
Total: 15s
本地二次构建 30 倍加速,CI 15 倍加速。
七、依赖管理最佳实践
7.1 集中版本
所有包共享的依赖,在根 package.json 装一次:
// monorepo/package.json
{
"devDependencies": {
"typescript": "^5.3.0",
"vitest": "^1.6.0",
"eslint": "^8.57.0"
}
}
7.2 共享配置
// packages/eslint-config/index.js
module.exports = {
extends: ['eslint:recommended'],
rules: {
// 共享规则
},
};
// apps/web-app/package.json
{
"devDependencies": {
"eslint-config-myorg": "workspace:*"
}
}
7.3 Changesets(版本管理)
Changesets 是 Monorepo 版本管理工具:
# 添加 changeset
npx changeset
# 选哪些包需要升级
# - ui-library: minor
# - utils-lib: patch
# 提交后,CI 自动:
# 1. 升级版本
# 2. 生成 CHANGELOG
# 3. 发布到 npm
# .changeset/happy-dogs-sing.md
---
"ui-library": minor
"utils-lib": patch
---
新增 `<Button loading />` 属性
八、Monorepo 迁移指南
8.1 评估是否需要
适合 Monorepo:
- ≥ 2 个相互依赖的应用
- 共享 UI 组件 / 工具
- 跨应用重构频繁
不适合:
- 单个独立应用
- 完全独立的项目
8.2 迁移步骤
# 1. 初始化
mkdir monorepo && cd monorepo
pnpm init
# 2. 配置 workspace
cat > pnpm-workspace.yaml << EOF
packages:
- 'apps/*'
- 'packages/*'
EOF
# 3. 把现有项目移入
mv ../web-app apps/
mv ../ui-library packages/
# 4. 重新安装依赖
pnpm install
# 5. 安装 Turborepo
pnpm add -D turbo
8.3 跨包重构
# 用 jscodeshift 改代码
pnpm exec jscodeshift -t transforms/rename-component.ts packages/*/src
# 用 Turborepo 跑跨包迁移
turbo run codemod --filter=...
九、本篇小结
| 工具 | 适用 |
|---|---|
| pnpm | 包管理器(必选) |
| Turborepo | 任务编排 + 远程缓存(简单) |
| Nx | 完整 Monorepo 平台(复杂) |
| Changesets | 版本发布 |
| 关键点 | 说明 |
|---|---|
| 远程缓存 | Turborepo 杀手锏 |
| 任务编排 | dependsOn / inputs / outputs |
| 影响范围 | 只跑受影响的包 |
| 版本管理 | Changesets 自动化 |
下一篇讲 CI 环境下的构建优化——Docker 缓存、远程缓存、CI runner 调优。