前端工程化← 返回文章列表
Monorepo 实践:pnpm Workspace 与 Turborepo
探讨前端 Monorepo 方案中 pnpm workspace 的依赖组织与 Turborepo 任务编排机制,分析依赖拓扑、增量构建缓存与跨包联调实践。
LJ
· 10 分钟
本页目录展开 / 收起
读完你能做什么
- 判断团队的问题是否真的需要 Monorepo,而不是把仓库合并当成目标。
- 用 pnpm workspace 表达包依赖,用 Turborepo 表达任务依赖与缓存边界。
- 让构建缓存依赖明确输入,避免复用过期产物。
Workspace 管依赖安装与包链接,任务编排器管命令顺序和缓存。把两者职责分开,才能定位“依赖没解析”和“任务没重跑”这两类不同问题。
在项目规模扩大后,随着公共 UI 组件、工具函数库与多个业务应用的拆分,多仓库(Multi-repo)往往会带来版本发布繁琐、跨仓库联调链路长等问题。
将关联项目聚合在同一仓库中维护(Monorepo)是一种常见解法。本文结合 pnpm workspace 的依赖拓扑与 Turborepo 任务调度,梳理大仓工程化组织与构建缓存实践。
Multi-repo vs Monorepo 架构权衡
Multi-repo (多仓库模式):
[组件库 Repo] ──发布 npm (v1.0.1)──► [业务应用 A Repo 升级依赖]
└──► [业务应用 B Repo 升级依赖]
痛点: 跨包修改必须先发布才能测试,PR 割裂,CICD 重复运行。
Monorepo (单仓库多包模式):
my-monorepo/
├── apps/
│ ├── web-app/ (Next.js 应用)
│ └── admin-portal/ (管理后台)
└── packages/
├── ui-kit/ (公共组件库,使用 workspace:* 本地实时软链)
├── tsconfig/ (共享 TS 规则)
└── eslint-config/ (共享代码规范)
优势: 单个 PR 覆盖全链路原子变更,本地开发零等待。为什么从 Lerna 转向 Turborepo?
早期的 Lerna 和 npm/yarn v1 在 Monorepo 下存在明显性能瓶颈:
- 依赖幽灵依赖(Phantom Dependencies):npm 扁平化 node_modules 容易导致子包未声明依赖却能意外引用。
- 构建串行与重复计算:当执行
build时,Lerna 往往串行遍历所有包,即便某些包代码完全没变,每次仍要全量重构。
Turborepo 由 Vercel 团队采用 Go 语言重写,带来了两大质的飞跃:
Turborepo 任务执行有向无环图 (DAG Pipeline):
[packages/utils:build]
│
▼
[packages/ui:build]
┌───────────┴───────────┐
▼ ▼
[apps/web:build] [apps/admin:build]
(利用 Go 多核并发调度,相同源码哈希直接命中本地/远程缓存 0ms 产出)生产级工程拓扑搭建全流程
1. pnpm-workspace.yaml 配置
packages:
- 'apps/*'
- 'packages/*'2. 跨包引用使用 workspace:* 协议
在 apps/web/package.json 中声明对公共 UI 包的本地引用:
{
"name": "web-app",
"dependencies": {
"@my-org/ui": "workspace:*",
"@my-org/utils": "workspace:*"
}
}- 原理解析:pnpm 会自动将
@my-org/ui软链接(Symlink)到本地packages/ui,在开发时修改 UI 组件无需发布,Web 应用可立即捕捉变化。
3. turbo.json 任务调度与增量缓存
在根目录配置 turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
// 依赖上游包的 build 产物完成后再执行当前包的 build
"dependsOn": ["^build"],
// 声明产物目录,Turborepo 会自动哈希并缓存该目录
"outputs": ["dist/**", ".next/**", "!dist/.cache/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}性能收益实测对比
| 场景 | 传统 Lerna / 单核串行 | pnpm Workspace + Turborepo |
|---|---|---|
| 全量依赖安装耗时 | 85 秒(重复下载大量相同包) | 14 秒(全局 Hardlink 磁盘去重) |
| 冷构建(Cold Build) | 62 秒 | 18 秒(基于 DAG 拓扑多核并行) |
| 增量热构建(Hot Build) | 62 秒(无法缓存) | 0.3 秒(FULL TURBO 命中缓存直接恢复产物) |
官方资料
工程经验小结
Monorepo 并不会自动带来工程效率的提升,反而对团队的依赖规范和发版流程提出了更高要求。
使用 Turborepo 时,最关键的是合理配置 inputs 与 outputs 缓存边界:如果 outputs 遗漏了关键产物目录,或者 inputs 没有包含影响构建的环境变量,就可能导致 CI 命中错误缓存、构建出带有隐性问题的产物。另外,跨包之间尽量避免循环依赖,保持单向依赖拓扑是保证 Monorepo 可持续维护的前提。