问题场景
团队项目越来越多,公共组件库、工具函数库、业务项目分散在 N 个 Git 仓库。每改一个公共包,流程是这样的:
- 改 A 库代码 → commit → push → npm publish
- 切到 B 项目 → 升级依赖 → 发现不兼容
- 回 A 修 → 再发版 → 再切回来 → 循环 N 次
同事直呼:"改一行代码,切五个仓库,发三个版,我的生命在流逝。"
你决定迁移到 Monorepo,但网上教程看着简单,落地上线时全是暗坑。
原因分析 & 方案选型
多仓库的核心痛点就三个:
| 痛点 | 表现 |
|---|---|
| 修改成本高 | 跨仓库改代码需要 N 次发版 |
| 版本碎片 | 不同项目依赖的公共包版本不一致 |
| 复现困难 | issue 复现需要在多个仓库间来回跳 |
Monorepo 工具选型:Turborepo(Vercel 出品)vs Nx vs Lerna。
| 对比项 | Turborepo | Nx | Lerna |
|---|---|---|---|
| 学习曲线 | ⭐ 低 | ⭐⭐⭐ 高 | ⭐ 低但功能弱 |
| 缓存能力 | 内置+远程缓存 | 内置+远程缓存 | 无 |
| 并行执行 | ✅ | ✅ | ❌ |
| 任务编排 | 零配置 | 需配置 | 手动 |
| 社区生态 | 快速增长 | 成熟 | 逐渐边缘化 |
选 Turborepo,理由:Vite + pnpm + Turborepo 三件套,零配置任务编排、增量缓存、并行构建,小团队两周就能上手。
解决方案 & 实操步骤
Step 1: 目录结构设计(最容易被忽视)
perl
my-monorepo/
├── apps/
│ ├── admin/ # 后台管理
│ ├── web/ # 前台 H5
│ └── docs/ # 文档站
├── packages/
│ ├── ui/ # 公共组件库
│ ├── utils/ # 工具函数
│ └── config/ # ESLint/TS 共享配置
├── pnpm-workspace.yaml
├── turbo.json
├── package.json
└── .npmrc
⚠️ 坑 1:packages 里面别放业务代码
有人把业务项目也丢在 packages 里,结果每次改业务代码都触发公共包的重建缓存失效。正确做法:apps 放业务项目,packages 放公共库,两者职责分离。
Step 2: pnpm workspace 配置
yaml
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
根目录 .npmrc:
ini
shamefully-hoist=true
strict-peer-dependencies=false
⚠️ 坑 2:shamefully-hoist 不配,tsconfig paths 全崩
默认 pnpm 严格隔离依赖,Vite + TypeScript 路径别名会找不到 node_modules。加上
shamefully-hoist=true将依赖提升到根 node_modules,否则每个子包都要单独配 tsconfig paths。
Step 3: Turborepo 任务编排
json
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"dev": {
"cache": false,
"persistent": true
},
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["build"]
},
"type-check": {
"dependsOn": ["^type-check"]
}
}
}
jsonc
// package.json (根目录)
{
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
"lint": "turbo lint",
"test": "turbo test"
}
}
关键理解:dependsOn 中的 ^ 前缀
"^build"表示:先构建该包的所有依赖,再构建它自己- 没有
^:等所有前置任务的全部子包执行完 - 不写
dependsOn:所有包并行执行
Turborepo 会自动拓扑排序:先构建 utils → 再构建依赖 utils 的 ui → 最后构建依赖 ui 的 web。
Step 4: 跨包引用
jsonc
// apps/web/package.json
{
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*"
}
}
tsx
// apps/web/src/App.tsx
import { Button } from "@repo/ui"; // 直接引用,无需发版
import { formatDate } from "@repo/utils";
⚠️ 坑 3:workspace: 和 ^1.0.0 的差别*
workspace:*在本地开发时直接链接到本地源码,publish 时会自动替换为实际版本号。 如果写成"@repo/ui": "^1.0.0",pnpm 会去 registry 找包,不走工作空间。务必写workspace:*。
Step 5: 缓存配置提速
Turborepo 默认有本地缓存,第二次构建相同输入直接秒出:
bash
# 第一次:正常构建,耗时 45s
turbo build
# 第二次(代码没变):瞬间完成,耗时 0.2s
turbo build
# 强制跳过缓存:排查问题时用
turbo build --force
远程缓存(配合 Vercel Remote Caching 或自建 S3):
bash
turbo login
turbo link
# 配置后 CI 和本地共享缓存,CI 构建从 10min → 45s
要点总结
| 序号 | 关键要点 |
|---|---|
| 1 | apps 放业务,packages 放公共库,职责分离防止缓存污染 |
| 2 | pnpm workspace 必须配 .npmrc + shamefully-hoist=true |
| 3 | 跨包依赖用 workspace:*,不要写版本号 |
| 4 | Turborepo 的 dependsOn 中的 ^ = 先构建依赖,理解它就能玩转任务编排 |
| 5 | 缓存是核心竞争力,配好远程缓存后 CI 速度飞升 10x |
迁移完成的第一天,改一行 @repo/utils 的代码,所有项目自动生效的那一刻------同事们看着终端输出的 0.1s,露出了满意的微笑。