Monorepo 工具链:Turborepo 与 Nx
多个包/项目分散在多仓库难共享代码,Monorepo 解决这个。本文讲清 Monorepo 的价值与代价、pnpm workspace 基础、Turborepo(轻量、任务编排、缓存)与 Nx(功能全、插件生态)的对比,以及什么团队该上 Monorepo。
本文要点
- 主题聚焦:Next.js、TypeScript
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
Monorepo 工具链:Turborepo 与 Nx
做过几个仓库之间互相依赖的项目,你大概率经历过这种事:web 仓库要复用 admin 仓库里那段表单逻辑,于是要么复制粘贴埋下后续不同步的雷,要么把它发布成 npm 包,然后每次改动都要走一遍 version bump → publish → 另一边升版本 → 重新装 的漫长流程。一天改三次,一天就这么过去了。
Monorepo(单仓库多项目)就是冲着这种痛去的。但它不是银弹:仓库膨胀、构建变慢、权限粒度变粗,都是绕不开的代价。本文从多仓库的痛讲起,过一遍 pnpm workspace 的基础,再深入对比当前最主流的两套工具——Turborepo 与 Nx,最后给出一个可落地的目录结构与配置示例。
一、多仓库的痛:为什么要上 Monorepo
把多个相关项目拆成多仓库(polyrepo),最初是为了"独立",但代价在协作中逐渐暴露:
- 代码难共享:公共逻辑(组件、hooks、工具函数、类型定义)跨仓库复用,要么复制粘贴(两边漂移),要么发 npm 包(流程极重)。
- 依赖版本不一致:
web仓库用 React 18,admin还在 17,shared-utils装了两个版本。时间一长,升级变成一个跨仓库的大工程,没人愿意碰。 - 跨仓库改动麻烦:一个 API 字段重命名,要同时改
backend、web、admin、mobile四个仓库,开四个 PR、等四次 review、按顺序合并。原子性?不存在的。 - CI/CD 割裂:每个仓库一套流水线、一套配置、一套发布脚本,维护成本随仓库数线性增长。
Monorepo 的核心价值就是一句话:让"一个逻辑改动"能在一个 commit 里完成。共享代码直接以本地包形式引用,版本一致性由 workspace 强制保证,工具链只配置一次。
二、Monorepo 的代价:别只看甜的一面
把项目合到一起不是免费午餐,典型的代价有:
- 仓库体积大:clone 慢,新人上手要看的目录变多。当然现在的 sparse checkout 和 shallow clone 能缓解,但认知负担仍然存在。
- 构建变慢:如果工具不够智能,改一个包会触发全量 build。这正是 Turborepo / Nx 要解决的核心问题——只构建受影响的包。
- 权限粒度粗:Git 的权限模型是仓库级的,没法对子目录单独授权。对外开源、对内部分敏感的项目,权限管理会变得棘手(大厂通常自研一层 CODEOWNERS + CI 策略来缓解)。
- 工具链必须统一:所有项目要用同一套 lint / test / build 工具。对于已经各自跑顺的团队,这是一个迁移成本。
结论很直白:Monorepo 适合协作密集、需要频繁共享代码的团队;如果是几个几乎不相干的项目硬塞在一起,反而会拖累彼此。
三、基础:pnpm workspace
无论你最后选 Turborepo 还是 Nx,底层包管理几乎都绕不开 pnpm workspace(npm 和 yarn 也有 workspace,但 pnpm 在性能、磁盘占用和幽灵依赖防护上明显更好)。
在仓库根目录建一个 pnpm-workspace.yaml:
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
然后在 packages/ui/package.json 里把包名声明出来:
{
"name": "@codenodex/ui",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts"
}
apps/web 引用它,用 workspace: 协议,而不是写死版本号:
{
"dependencies": {
"@codenodex/ui": "workspace:*",
"next": "^15.0.0"
}
}
workspace:* 的含义是:永远链接到当前仓库内的最新版本,不经过 npm registry。这样改 packages/ui 里一个组件,apps/web 立刻能看到——本地包就是本地文件,不存在"发布-升级"的循环。pnpm 还会用硬链接把全局 store 里的依赖共享到每个子包,磁盘占用比 npm/yarn classic 低一个数量级。
到这一步,你已经有一个能用的 Monorepo 了。但还差一层:怎么批量执行任务(lint、test、build),怎么缓存结果避免重复构建。这就是 Turborepo 和 Nx 的主战场。
四、Turborepo:轻量、专注任务编排与缓存
Turborepo 的定位非常克制——它只做两件事:任务编排和构建缓存。不抢包管理器的活,不给你塞生成器、插件体系。配置就是一个 turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {},
"test": {
"dependsOn": ["^build"]
}
}
}
几个关键概念:
^build:先把这个包的所有依赖包的build跑完,再跑自己的。拓扑顺序自动算。- outputs:声明构建产物路径,Turborepo 据此做缓存命中判断——同样输入(源码、依赖锁文件、环境变量)就跳过构建,直接还原产物。
- 本地 + 远程缓存:本地缓存已经能省掉大量重复工作;接上 Vercel 的远程缓存(或自建),CI 和团队成员之间共享缓存,新机器第一次 build 也能秒过。
典型用法:
# 只构建受影响的包(本地开发常用)
turbo run build --filter=...[origin/main]
# 同时跑所有包的 lint / test / build,并行 + 缓存
turbo run lint test build
Turborepo 的优势非常鲜明:
- 配置简单,5 分钟上手,JS/TS 项目几乎是零负担。
- 任务并行 + 拓扑排序,构建速度比裸跑脚本快几倍。
- 缓存机制成熟,远程缓存开箱即用(Vercel 系生态)。
- 侵入性低,不强迫你换包管理器、换目录约定。
它的局限同样明显:没有代码生成器、没有依赖图可视化、没有针对框架的"傻瓜式"插件(比如自动生成一个 Next.js 应用的脚手架)。这些正是 Nx 的强项。
五、Nx:功能全、生成器、插件生态
Nx 走的是另一条路——一套完整的工程化平台。除了任务编排和缓存(这两点它和 Turborepo 能力相当),Nx 还提供:
- 生成器(Generator):一行命令生成符合约定的组件、库、应用,内置最佳实践。
- 依赖图:
nx graph可视化包之间的依赖关系,重构时一眼看清影响范围。 - 插件生态:针对 Angular、React、Next.js、NestJS、Node 等都有官方插件,生成器 + 迁移脚本 + 构建器一整套。
affected命令:精确判断哪些包被某个 commit 影响,只对它们跑 test/build/lint。- 分布式任务执行:把任务分发到多台机器并行执行,适合超大型仓库。
一个典型的 nx.json 长这样:
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "lint", "test"],
"remoteCache": { "enabled": true }
}
}
},
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"]
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["default", "^default"],
"outputs": ["{projectRoot}/dist"]
}
}
}
生成器示例——创建一个新的 React 库:
nx g @nx/react:lib buttons --directory=packages/buttons
这条命令会自动创建目录、生成 package.json、配置构建工具、加好 tsconfig 路径,甚至能在 nx graph 里立刻看到它。
Nx 的优势:
- 大型项目救星:几十上百个包、多框架混用、需要强制约束的团队,Nx 的工程化能力无可替代。
- 插件开箱即用:Angular / NestJS 项目尤其顺滑。
- 可扩展性:从任务编排一路做到 CI 编排、发布自动化。
它的代价:
- 学习曲线陡:
nx.json、project.json、生成器、executor、插件——概念多,新人上手慢。 - 侵入性强:倾向于接管你的目录结构、构建配置、迁移流程。一旦深度依赖,迁出去很痛。
- 配置相对复杂,小项目用 Nx 是杀鸡用牛刀。
六、Turborepo vs Nx:怎么选
两者不是简单的"谁更好",而是定位不同。一个实战导向的对比:
| 维度 | Turborepo | Nx |
|---|---|---|
| 上手成本 | 极低,一个 turbo.json 搞定 | 高,概念多、配置多 |
| 核心能力 | 任务编排 + 缓存 | 任务编排 + 缓存 + 生成器 + 依赖图 + 插件 |
| 适用规模 | 中小型、JS/TS 为主 | 大型、多框架、多语言 |
| 生态 | Vercel 系,与 Next.js 配合好 | 插件丰富,Angular / NestJS 强 |
| 侵入性 | 低,几乎透明 | 高,接管工程结构 |
| 远程缓存 | Vercel 开箱即用(可自建) | 可接自建 |
一个粗暴的判断标准:
- 你的仓库是 JS/TS 为主、规模中等、团队习惯自己管工具链 → Turborepo。
- 你管着几十个包、多框架混用、需要生成器和强约束 → Nx。
- 你已经在用 Angular / NestJS → Nx 几乎是默认选项。
我自己在中型项目里更倾向 Turborepo——它做的事少,但做得足够好,侵入性低意味着随时可以替换。Nx 更像是一个长期投资:你愿意为它的能力付出学习和锁定的成本。
七、什么团队该上 Monorepo
先回答"该不该上",再回答"用哪个工具"。
该上的场景:
- 多个应用 / 包频繁共享代码(组件库、工具函数、类型定义、业务 hooks)。
- 需要统一工具链(lint、test、CI、发布流程),避免每个仓库各搞一套。
- 跨仓库改动高频且需要原子性(改一个接口要同步动好几个项目)。
- 团队对依赖版本一致性敏感(比如必须全场同一个 React 大版本)。
不该上的场景:
- 就一个项目,没什么可共享的代码。
- 几个项目技术栈完全不同、团队各自独立、几乎不协作。
- 团队规模小,引入 Monorepo 工具的学习成本超过收益。
Monorepo 是协作密度的产物,不是"看起来更高级"的工程仪式。如果现在 polyrepo 协作不痛,别为了"工程化"硬上。
八、实战:一个 app + 多 packages 的结构
下面是一个我常用的、Turborepo + pnpm workspace 的落地结构,适合绝大多数中小型 JS/TS 团队:
my-monorepo/
├── apps/
│ ├── web/ # Next.js 应用
│ └── admin/ # 后台管理
├── packages/
│ ├── ui/ # 共享组件库
│ ├── utils/ # 工具函数
│ ├── tsconfig/ # 共享 tsconfig 预设
│ └── eslint-config/ # 共享 eslint 配置
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
└── tsconfig.base.json
package.json(根):
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test"
},
"devDependencies": {
"turbo": "^2.0.0",
"typescript": "^5.5.0"
},
"packageManager": "[email protected]"
}
apps/web/package.json:
{
"name": "@codenodex/web",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build"
},
"dependencies": {
"@codenodex/ui": "workspace:*",
"@codenodex/utils": "workspace:*",
"next": "^15.0.0",
"react": "^19.0.0"
}
}
日常开发就是 pnpm dev(在子包里)或根目录 pnpm run dev(Turborepo 并行启动所有 app);构建时 turbo run build --filter=...[origin/main] 只构建受影响的包。改 packages/ui 里的一个 Button,所有依赖它的 app 在下一次构建里自动拿到最新代码,不需要发布、不需要升版本。
这套结构的好处是:渐进可演。今天用 Turborepo 够用,未来规模真的撑不住想换 Nx,目录约定本身不需要大动。这也是我推荐中小团队从 Turborepo 起步的原因——保留退路。
小结
- Monorepo 解决的是跨仓库协作的痛(代码共享、版本一致、原子改动、统一工具链),代价是仓库变大、构建变慢、权限变粗。
- pnpm workspace 是几乎所有现代 Monorepo 的底座,
workspace:协议让本地包互引零成本。 - Turborepo 专注任务编排与缓存,轻量、配置简单、侵入低,适合 JS/TS 为主的中型团队。
- Nx 功能全面(生成器、依赖图、插件生态、affected),适合大型、多框架、需要强工程约束的团队,代价是学习曲线陡、侵入性强。
- 选型看协作密度与规模:不是所有团队都该上 Monorepo,更不是所有 Monorepo 都该用 Nx。
一句话总结:Monorepo 不是工程化的终点,而是协作密度到了之后的自然选择;工具选型永远先问"现在到底痛不痛",再问"用哪个"。
Related