#Next.js#TypeScript

Monorepo 工具链:Turborepo 与 Nx

多个包/项目分散在多仓库难共享代码,Monorepo 解决这个。本文讲清 Monorepo 的价值与代价、pnpm workspace 基础、Turborepo(轻量、任务编排、缓存)与 Nx(功能全、插件生态)的对比,以及什么团队该上 Monorepo。

本文要点

  • 主题聚焦:Next.js、TypeScript
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年5月14日·更新于 2026年7月23日

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:

yaml
# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

然后在 packages/ui/package.json 里把包名声明出来:

json
{
  "name": "@codenodex/ui",
  "version": "0.0.0",
  "private": true,
  "main": "./src/index.ts"
}

apps/web 引用它,用 workspace: 协议,而不是写死版本号:

json
{
  "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:

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 也能秒过。

典型用法:

bash
# 只构建受影响的包(本地开发常用)
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 长这样:

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 库:

bash
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:怎么选

两者不是简单的"谁更好",而是定位不同。一个实战导向的对比:

维度TurborepoNx
上手成本极低,一个 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 团队:

text
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(根):

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:

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

相关推荐