一、引言
多仓库(Multi-repo)让「跨项目复用代码」要发私有包、让「一次改动涉及多处」要逐个仓库同步,而 Monorepo 把多个项目放进一个仓库,共享代码、统一 CI、一键联动部署。代价是「一个仓库可能很复杂」——构建编排、缓存、增量部署都成了新问题。
本文系统讲 Monorepo 的构建与部署:先建立 Monorepo 的结构与收益/代价认知,再深入 pnpm workspace 依赖管理、Turborepo/Nx 任务编排与缓存、Affected 定向构建,接着讲 Vercel/Cloudflare 上的部署策略与发布流程(Changesets),最后给出边界与选型建议。
关联:https://plumephp.com/tools-deployment-rollback-strategies/(部署策略)、https://plumephp.com/vercel-deploy-vue-nuxt/(多框架部署)、https://plumephp.com/tools-nextjs-app-router-deep/(Next.js 工程)。
二、Monorepo 的收益与代价
2.1 典型结构
apps/
web/ # Next.js 前端
api/ # Node API
worker/ # 边缘函数
packages/
ui/ # 共享组件库
utils/ # 共享工具
types/ # 共享类型
configs/
eslint/ # 共享配置
tsconfig/ # 共享 TS 配置
2.2 收益 vs 代价
| 收益 | 代价 |
|---|---|
| 共享代码(ui/utils/types) | 构建编排复杂 |
| 一次改动联动多包 | 构建时间可能爆炸 |
| 统一 lint/测试/CI | 依赖关系难管理 |
| 原子提交(跨包一致) | 权限粒度粗 |
| 便于 Code Review 联动 | 新人上手门槛高 |
2.3 什么时候该上 Monorepo
适合:多包共享代码、前后端联动频繁、需要原子发布
不适合:团队大且耦合低、各包独立发布节奏、权限隔离要求高
一句话总结:Monorepo 换「共享+联动+原子提交」,代价是「构建编排+复杂度」——多包强耦合时收益大于代价。
三、pnpm workspace 依赖管理
3.1 为什么是 pnpm
pnpm 三大特性:
1. 内容寻址存储(硬链接)→ 省磁盘、省安装
2. 严格依赖隔离(非扁平 node_modules)→ 不会「幽灵依赖」
3. workspace 原生支持 → 跨包引用直接 link
3.2 workspace 配置
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
# 常用命令
pnpm install # 安装全部
pnpm --filter web dev # 只跑 web
pnpm --filter ui build # 只构建 ui
pnpm -r run test # 所有包跑 test
// apps/web/package.json 引用本地包
{
"dependencies": {
"@repo/ui": "workspace:*", // 本地 link
"@repo/utils": "workspace:*"
}
}
3.3 workspace 的坑
1. peer 依赖重复(React 多副本)→ 用 catalog / overrides 统一版本
2. 循环依赖(ui 依赖 utils,utils 依赖 ui)→ 拆包消除
3. 版本漂移(不同包用不同 React 版本)→ 统一 catalog
# pnpm-workspace.yaml 支持 catalog 统一版本
catalog:
react: ^19.0.0
typescript: ^5.5.0
一句话总结:pnpm 用硬链接省空间、严格隔离防幽灵依赖、workspace 原生支持本地 link;统一版本靠 catalog、防循环依赖靠拆包。
四、Turborepo / Nx 任务编排与缓存
4.1 问题:全量构建很慢
多包 → 每个包都 build/lint/test → 全量跑很慢
解法:任务编排(只跑受影响)+ 缓存(重复任务秒回)
4.2 Turborepo 配置
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"], // 先构建依赖
"outputs": [".next/**", "dist/**"],
"cache": true
},
"lint": {},
"test": {}
}
}
pnpm turbo build # 全量(首次)
pnpm turbo build --filter=web # 只构建 web 及依赖
pnpm turbo ls # 查看任务图
4.3 Turborepo 缓存命中
缓存键 = 代码哈希 + 配置 + 依赖 → 未变则命中缓存
命中 = 直接从缓存拿产物(秒级),不重新执行
远程缓存(Turborepo Remote Cache / Vercel)→ CI 与本地共享缓存
# 开启远程缓存(Turborepo + Vercel)
pnpm dlx turbo login
pnpm turbo build --remote-cache
4.4 Nx 对照
Nx 额外能力:
1. Affected 分析(git 变更 → 只跑相关任务)
2. 图可视化(nx graph)
3. 代码生成(nx generate)
Turborepo 更轻量、聚焦缓存;Nx 功能全、学习成本高
一句话总结:Turborepo/Nx 的核心是「只跑受影响 + 缓存秒回」——dependsOn 保证依赖顺序、远程缓存让 CI 与本地共享;Nx 在 Affected 与生成上更强、Turborepo 更轻量。
五、Affected 定向构建
5.1 只构建「受影响的包」
改动 packages/utils → 只需构建依赖 utils 的包,无关包跳过
实现:
Turborepo:缓存自动处理(未变的包命中缓存)
Nx:nx affected:build --base=main
# Nx Affected
npx nx affected:build --base=origin/main # 对比 main 的变更影响
npx nx affected:test --base=origin/main
# Turborepo:缓存天然实现「未变即命中」
pnpm turbo build --filter=[origin/main...HEAD] # 变更范围
5.2 CI 中的增量策略
# 示例:CI 用 affected 缩短构建时间
jobs:
build:
steps:
- checkout
- run: pnpm install
- run: npx nx affected:build --base=origin/main # 只构建受影响
- run: npx nx affected:test --base=origin/main
5.3 影响面分析的价值
1. 小改动不再全量构建 → CI 快 10 倍
2. 只部署受影响的 app → 发布快、风险小
3. 全量构建作为 main 的定期兜底
一句话总结:Affected/缓存让「改小包不触发全量构建」——Turborepo 缓存天然实现、Nx 显式 affected 分析;CI 增量构建是 Monorepo 提速的核心。
六、Monorepo 在 Vercel / Cloudflare 的部署
6.1 Vercel 的 Monorepo 支持
# Vercel 自动识别 Monorepo,按目录部署
# 根目录/目录配置
vercel.json:
{
"buildCommand": "pnpm --filter web build",
"outputDirectory": "apps/web/.next"
}
// 多个应用各自部署
// 每个 app 有独立 vercel.json,或用 workspace 自动探测
{
"projectSettings": {
"framework": "nextjs",
"rootDirectory": "apps/web"
}
}
6.2 Cloudflare 部署 Monorepo 子应用
# 在 apps/worker 内
cd apps/worker
wrangler deploy
# CI 中按 affected 决定部署哪些子应用
if [ "$(git diff --name-only origin/main...HEAD | grep -c '^apps/worker/')" -gt 0 ]; then
pnpm --filter worker deploy
fi
6.3 部署的最佳实践
1. 只部署受影响的 app(affected)
2. 每个 app 独立 preview URL(Vercel 自动)
3. 构建产物缓存共享(远程缓存)
4. 环境变量按 app 维度管理
5. 依赖安装一次(根目录 pnpm install)→ 各 app 复用
一句话总结:Monorepo 部署 = Vercel/Cloudflare 识别子目录 + affected 决定部署哪些 + 远程缓存共享产物——每个 app 独立 preview、一次安装、增量部署。
七、发布与版本管理(Changesets)
7.1 版本策略:单版本 vs 独立版本
单版本(Turborepo 风格):所有包同一版本 → 简单,联动发布
独立版本:各包独立 → 灵活,但协调复杂
7.2 Changesets 工作流
# 1. 提交变更说明
pnpm changeset add # 选择受影响包 + 版本类型(major/minor/patch)
# 2. 合并后 CI 生成 changelog + 升版本
pnpm changeset version # 根据 changesets 更新版本与 CHANGELOG
# 3. 发布
pnpm changeset publish # npm publish + 打 tag
// .changeset/config.json
{
"changelog": "@changesets/cli/changelog",
"commit": false,
"access": "public",
"baseBranch": "main"
}
7.3 发布流水线
PR(带 changeset)→ 合并 main → CI 检查 changeset
→ changeset version(生成 changelog)→ 发布 → 打 tag → 通知
一句话总结:Changesets 把「版本与发布」写进 PR——每个 PR 声明影响与版本类型,合并后自动生成 changelog 并发布。
八、Monorepo 的边界与选型
8.1 什么时候别用 Monorepo
1. 团队独立、发布节奏完全独立
2. 代码共享需求低
3. 仓库规模过大导致工具链卡顿
4. 权限隔离要求严格(多租户/外包)
8.2 工具链选型
| 场景 | 推荐 |
|---|---|
| 依赖管理 | pnpm workspace |
| 任务编排/缓存 | Turborepo(轻)或 Nx(全) |
| 发布 | Changesets |
| 部署 | Vercel/Cloudflare 原生 Monorepo 支持 |
| 后端 Monorepo | pnpm + Nx + 容器多镜像 |
8.3 渐进式采用
不一定要一步到位:
先 2-3 个强耦合包合并 → 建立 workspace
→ 引入 Turborepo 缓存 → 再引入 affected/发布流水线
从「共享代码」起步,逐步演进
一句话总结:Monorepo 不是万能药——团队独立/权限隔离严格时别上;选型用 pnpm+Turborepo/Changesets+Vercel;渐进式从「共享代码」开始演进。
九、Monorepo 实践清单
- pnpm workspace 配置正确,无循环依赖
- 统一版本 catalog,无 React/TS 多副本
- Turborepo/Nx 缓存命中正常(远程缓存共享)
- affected 增量构建在 CI 生效
- 每个 app 独立部署入口与 preview URL
- Changesets 覆盖所有发布包
- 根目录 lint/typecheck 全量兜底
- 构建超时与缓存失效策略明确
十、速查表
| 需求 | 方案 |
|---|---|
| 依赖管理 | pnpm workspace |
| 任务编排 | Turborepo / Nx |
| 增量缓存 | 远程缓存 + affected |
| 只构建受影响 | nx affected / turbo filter |
| 版本管理 | Changesets |
| 部署子应用 | Vercel/Cloudflare 目录配置 |
| 多包共享 | workspace link |
| 统一版本 | catalog / overrides |
| 防幽灵依赖 | pnpm 严格隔离 |
| 发布流水线 | changeset → version → publish |
一句话记忆:Monorepo 用共享代码换联动效率——pnpm workspace 管依赖(硬链接省空间、隔离防幽灵)、Turborepo/Nx 管任务(缓存秒回 + affected 定向)、Changesets 管发布(PR 带版本声明自动发版);部署在 Vercel/Cloudflare 按子目录 + affected 增量;团队独立或权限隔离严格时别硬上——先从「共享代码」渐进演进。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。