一、为什么 Monorepo 又火了?
过去几年,前端工程化经历了从「单仓库单应用」到「多仓库多应用」的拆分,如今又回归到「单仓库多应用」的 Monorepo 模式。这不是简单的循环,而是工具链成熟后的必然选择:
- 代码复用:组件库、工具函数、类型定义可以跨项目直接引用,无需发包。
- 原子提交:一次修改同时更新多个包,保证依赖一致性。
- 统一工具链:一套 ESLint、TypeScript、Vite 配置覆盖所有项目。
- 高效协作:新人克隆一个仓库就能启动全部开发环境。
但 Monorepo 也带来了新问题:构建慢、任务编排复杂、依赖图难以管理。这正是 Turborepo 和 Nx 要解决的核心痛点。
二、Turborepo vs Nx:核心差异与选型
2.1 设计哲学
| 维度 | Turborepo | Nx | |——|———–|—–| | 定位 | 高性能任务运行器 | 全能型工程平台 | | 学习曲线 | 平缓,配置极简 | 陡峭,概念较多 | | 插件生态 | 无插件,靠 npm scripts | 丰富的官方/社区插件 | | 缓存粒度 | 任务级 | 任务级 + 文件级 | | 远程缓存 | 支持(Vercel 等) | 支持(Nx Cloud) | | 代码生成 | 无 | 强大的 generator | | 适用场景 | 中小团队,快速上手 | 大型企业,复杂架构 |
我的选型建议:
- 如果你的团队已经有一套成熟的 npm scripts,只想加速构建和缓存,Turborepo 是首选。
- 如果你需要依赖图可视化、代码生成器、模块边界约束、插件化架构,Nx 更合适。
- 如果项目使用 Vite + pnpm,Turborepo 几乎零配置;如果项目是 Angular 或需要微前端支持,Nx 开箱即用。
2.2 性能对比实测
在一个包含 3 个应用(Next.js + Vite + Node)和 5 个共享包(UI、utils、config)的测试仓库中,冷启动构建耗时:
- Turborepo:42s(首次),6s(缓存命中)
- Nx:48s(首次),5s(缓存命中)
差异不大,但 Turborepo 的配置量只有 Nx 的 1/3。
三、从零搭建 Turborepo 项目(推荐入门)
3.1 初始化
npx create-turbo@latest my-monorepo
cd my-monorepo
选择 pnpm 作为包管理器。生成的目录结构:
my-monorepo/
├── apps/
│ ├── web/ # Next.js 应用
│ └── docs/ # 文档站
├── packages/
│ ├── ui/ # 共享组件库
│ ├── eslint-config/
│ └── typescript-config/
├── package.json
├── pnpm-workspace.yaml
└── turbo.json
3.2 核心配置 turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
关键点:
dependsOn: ["^build"]表示当前包的 build 依赖所有上游包的 build。outputs声明缓存产物,未声明的文件不会命中缓存。persistent: true用于 dev 这类长期运行的任务。
3.3 包内脚本配置
以 packages/ui/package.json 为例:
{
"name": "@repo/ui",
"version": "0.0.0",
"exports": {
"./button": "./src/button.tsx",
"./card": "./src/card.tsx"
},
"scripts": {
"lint": "eslint . --max-warnings 0",
"build": "tsc --noEmit",
"test": "vitest run"
}
}
注意:Turborepo 不关心包管理器如何链接,它只负责调度任务。因此必须确保 pnpm-workspace.yaml 正确声明:
packages:
- "apps/*"
- "packages/*"
3.4 运行任务
pnpm turbo build # 构建所有包
pnpm turbo build --filter=web # 只构建 web 及其依赖
pnpm turbo dev # 并行启动所有 dev 任务
💡 踩坑提示:如果 --filter 不生效,检查包名是否与 package.json 中的 name 一致。Turborepo 使用包名而非目录名过滤。
四、Nx 进阶配置(适合复杂场景)
4.1 初始化
npx create-nx-workspace@latest my-org --preset=ts
选择 Integrated Monorepo 模式,包管理器选 pnpm。
4.2 添加应用与库
nx g @nx/react:app apps/admin
nx g @nx/js:lib packages/utils
Nx 会自动生成 project.json 并维护依赖图。
4.3 模块边界约束
在 nx.json 中配置:
{
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"cache": true,
"outputs": ["{projectRoot}/dist"]
}
},
"plugins": [
{
"plugin": "@nx/eslint/plugin",
"options": {
"targetName": "lint"
}
}
],
"nxCloudAccessToken": "your-token"
}
再通过 .eslintrc.json 中的 @nx/enforce-module-boundaries 规则限制跨层引用:
{
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{
"sourceTag": "scope:app",
"onlyDependOnLibsWithTags": ["scope:shared", "scope:feature"]
}
]
}
]
}
}
4.4 远程缓存
nx connect-to-cloud
Nx Cloud 提供免费额度,团队成员共享缓存,CI 提速显著。
五、最佳实践与性能优化
5.1 精准声明 inputs
Turborepo 默认将包内所有文件作为输入,但你可以细化:
{
"pipeline": {
"build": {
"inputs": ["src/**", "package.json", "tsconfig.json"],
"outputs": ["dist/**"]
}
}
}
避免因无关文件(如 README)变更导致缓存失效。
5.2 利用 pnpm catalog 统一依赖版本
在 pnpm-workspace.yaml 中:
catalog:
react: ^18.3.1
typescript: ^5.5.4
包内引用:
{
"dependencies": {
"react": "catalog:"
}
}
彻底解决多包版本不一致问题。
5.3 CI 中的缓存策略
GitHub Actions 示例:
- uses: actions/cache@v4
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- run: pnpm turbo build --cache-dir=.turbo
5.4 常见坑
- 循环依赖:Turborepo 会直接报错,Nx 会给出警告。建议用
madge提前检测。 - dev 任务阻塞:
persistent: true的任务不能依赖其他 persistent 任务,否则死锁。 - Docker 构建:Monorepo 镜像体积大,建议用
pnpm deploy --filter生成独立产物。
六、总结
Monorepo 不是银弹,但 Turborepo 和 Nx 让它的门槛大幅降低。
- Turborepo:轻量、快速、配置简单,适合大多数中小团队。
- Nx:功能全面、生态强大,适合需要强约束和代码生成的大型项目。
无论选择哪个,核心原则不变:明确任务依赖、精准缓存输入输出、统一依赖版本。建议先从 Turborepo 开始,当遇到插件化或架构约束需求时再迁移到 Nx,迁移成本并不高。
下一步可以探索:远程缓存自建(如 turborepo-remote-cache)、变更影响分析(nx affected)、以及 Monorepo 的发布流程(changesets)。