跳到主要内容
预计阅读 33 分钟

工程化篇 | Monorepo

前言

随着前端项目越来越复杂,一个团队维护十几个甚至几十个包已经是常态。组件库、工具函数、CLI 工具、文档站……每个包各自一个仓库?还是全部放在一个仓库里?

这就是 Monorepo(单一仓库)和 Polyrepo(多仓库)之争。

很多人听说过 Monorepo,也知道 Google、Meta、微软等大厂在用它,但真到面试被问到:

  • Monorepo 和 Polyrepo 各自的优缺点是什么?
  • pnpm workspace、Turborepo、Nx 这些工具分别解决什么问题?
  • 依赖提升和隔离是怎么回事?
  • 任务编排和缓存是怎么加速构建的?

很多人就只能泛泛而谈了。

本章,我们从 Monorepo 的核心理念出发,拆解主流工具的设计思路,再聊聊大型项目的 Monorepo 实践。


诊断自测

Q1:Monorepo 和把所有代码放在一个仓库里有什么区别?它和”巨型单体应用”是一回事吗?

点击查看答案

Monorepo 不是”巨型单体应用”。Monorepo 是一个仓库里包含多个独立的包/项目,每个包有自己的 package.json、独立的构建和发布流程。它只是共享同一个 Git 仓库和一套基础设施(CI/CD、代码规范等)。

而”巨型单体应用”是所有代码耦合在一起,没有清晰的边界。Monorepo 强调的是代码共享 + 独立管理的平衡。

Q2:Turborepo 和 pnpm workspace 的关系是什么?它们解决的是同一个问题吗?

点击查看答案

不是同一个问题。

  • pnpm workspace 解决的是依赖管理问题:多个包之间如何共享依赖、如何互相引用、如何统一安装
  • Turborepo 解决的是任务编排问题:多个包的构建、测试、lint 任务之间的依赖关系如何、如何并行执行、如何利用缓存避免重复构建

它们是互补的关系。一个典型的 Monorepo 项目会同时使用 pnpm workspace(管理依赖)和 Turborepo(编排任务)。


一、Monorepo vs Polyrepo

1.1 什么是 Polyrepo?

Polyrepo 就是”一个包一个仓库”的传统模式:

github.com/team/
├── component-library/     ← 仓库 1
├── utils/                 ← 仓库 2
├── cli-tool/              ← 仓库 3
└── web-app/               ← 仓库 4

每个仓库有独立的 Git 历史、独立的 CI/CD、独立的发布流程。

1.2 什么是 Monorepo?

Monorepo 是”多个包共存于同一个仓库”:

github.com/team/monorepo/
├── packages/
│   ├── component-library/  ← 包 1
│   ├── utils/              ← 包 2
│   ├── cli-tool/           ← 包 3
│   └── web-app/            ← 包 4
├── package.json            ← 根 package.json
├── pnpm-workspace.yaml     ← workspace 配置
└── turbo.json              ← Turborepo 配置

1.3 对比

维度PolyrepoMonorepo
代码共享通过 npm 发包互相引用,有版本延迟直接引用源码,改了就生效
跨包重构痛苦——改一个接口要改 N 个仓库一个 PR 搞定所有包的修改
CI/CD每个仓库独立配置统一配置,但需要增量构建能力
代码规范每个仓库各自维护统一 ESLint、Prettier、TS 配置
权限控制天然隔离需要额外的工具支持(如 CODEOWNERS)
Git 历史干净(只有自己的提交)混合(所有包的提交混在一起)
上手门槛较高(需要学习 workspace 和任务编排工具)

1.4 什么时候选 Monorepo?

  • 多个包之间有频繁的互相引用
  • 需要经常做跨包重构
  • 希望统一代码规范、CI/CD、工具链
  • 团队规模中等,沟通成本可控

1.5 什么时候不适合 Monorepo?

  • 各个项目完全独立,几乎没有代码共享
  • 团队之间权限需要严格隔离
  • 仓库规模极大(数百 GB 的 Git 历史),需要专门的工具支持

二、依赖管理:pnpm workspace

2.1 基本配置

pnpm workspace 是目前最流行的 Monorepo 依赖管理方案。

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

这告诉 pnpm:“packages/apps/ 下的每个目录都是一个独立的包。“

2.2 包之间的互相引用

假设 packages/utils 要被 apps/web 引用:

// packages/utils/package.json
{
  "name": "@myorg/utils",
  "version": "1.0.0"
}
// apps/web/package.json
{
  "dependencies": {
    "@myorg/utils": "workspace:*"
  }
}

workspace:* 告诉 pnpm:“这个包就在本地 workspace 里,直接链接过去,不要去 npm registry 下载。”

运行 pnpm install 后,pnpm 会自动创建一个符号链接,让 apps/webnode_modules/@myorg/utils 指向 packages/utils。改了 utils 的代码,web 里立刻生效。

2.3 依赖提升与隔离

在 Monorepo 中,依赖提升是一个需要仔细权衡的问题:

提升(Hoisting): 把多个包共同的依赖安装到根目录的 node_modules,减少重复安装。

monorepo/
├── node_modules/
│   └── react/            ← 被提升到根目录,所有包共享
├── packages/
│   ├── app-a/
│   └── app-b/

隔离(Isolation): 每个包只能访问自己声明的依赖,防止幽灵依赖。

PNPM 默认就是隔离模式——每个包只能访问自己在 package.json 中声明的依赖。这在 Monorepo 中尤其重要,因为包之间的依赖关系必须是显式声明的,否则发布后在用户端会出问题。

2.4 常用命令

# 在所有包中执行命令
pnpm -r run build           # -r 表示递归(recursive)

# 只在某个包中执行
pnpm --filter @myorg/utils run test

# 给某个包添加依赖
pnpm --filter @myorg/web add lodash

# 给根目录添加开发依赖(通常是共享的工具)
pnpm add -Dw eslint prettier

三、任务编排:Turborepo

依赖管理解决了”包怎么组织”的问题,但还有一个关键问题:构建怎么编排?

一个有 20 个包的 Monorepo,每个包都要 build、test、lint。如果串行执行,要跑 60 个任务;如果不考虑依赖关系胡乱并行,又可能因为包 A 的构建产物还没生成,依赖 A 的包 B 就先构建了。

这就是 Turborepo 要解决的问题。

3.1 任务拓扑(Task Graph)

Turborepo 会分析所有包之间的依赖关系,构建一个任务拓扑图(Task Graph)

// turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],     // 先构建上游依赖
      "outputs": ["dist/**"]       // 构建产物目录
    },
    "test": {
      "dependsOn": ["build"]       // 先构建,再测试
    },
    "lint": {}                      // 无依赖,可以并行
  }
}

"dependsOn": ["^build"] 中的 ^ 表示拓扑依赖——如果包 B 依赖了包 A,那么 B 的 build 任务要等 A 的 build 任务完成。

Turborepo 会根据这个拓扑图,自动确定哪些任务可以并行、哪些必须串行,并以最大并行度执行。

3.2 缓存

Turborepo 最强大的特性之一是任务缓存

原理:每个任务的”输入”(源代码、依赖版本、环境变量等)会被哈希成一个唯一的 key。如果某次构建的输入没有变化,直接从缓存中恢复输出,跳过实际的构建过程。

$ turbo build

 Tasks:    10 successful, 10 total
 Cached:   8 cached, 10 total 8 个任务命中缓存,直接跳过
 Time:     2.3s 本来要 60s 的构建变成 2.3s

缓存类型:

  • 本地缓存:默认存储在 node_modules/.cache/turbo
  • 远程缓存:可以配置 Vercel Remote Cache 或自建缓存服务器,让 CI 和团队成员共享缓存

远程缓存意味着:你的同事刚跑过一次 build,你 pull 代码后再跑 build,直接命中远程缓存——几秒钟完成。

3.3 增量构建

有了缓存和拓扑分析,Turborepo 实现了真正的增量构建

  1. 你改了 packages/utils 的一行代码
  2. Turborepo 分析出只有 utils 和依赖它的 web-app 需要重新构建
  3. 其他 18 个包的构建结果直接从缓存恢复
  4. 总构建时间从 60s 降到 5s

四、Nx:企业级 Monorepo 方案

4.1 Nx 是什么?

Nx 是另一个流行的 Monorepo 工具,由 Nrwl 团队开发。和 Turborepo 相比,Nx 更”重”——它不仅做任务编排,还提供了项目脚手架、代码生成器、依赖可视化等一整套工具。

4.2 Nx vs Turborepo

维度TurborepoNx
定位轻量级任务编排器全功能 Monorepo 开发平台
上手难度低(一个 turbo.json)较高(有学习曲线)
任务缓存支持支持
远程缓存支持(Vercel)支持(Nx Cloud)
代码生成器有(nx generate)
依赖图可视化有(turbo graph)有(nx graph,更强大)
插件生态轻量丰富(React、Angular、Node 等插件)
适合场景轻量 Monorepo大型企业级 Monorepo

4.3 选型建议

  • 小中型项目(10 个包以内):pnpm workspace + Turborepo 足够
  • 大型项目(几十个包、多团队协作):考虑 Nx 的全套方案
  • 已有 Angular 项目:Nx 原生支持 Angular,迁移成本低

五、大型项目的 Monorepo 实践

5.1 目录结构规范

一个成熟的 Monorepo 通常是这样组织的:

monorepo/
├── apps/                  ← 应用(可部署的最终产物)
│   ├── web/
│   ├── admin/
│   └── mobile/
├── packages/              ← 共享包(被 apps 引用)
│   ├── ui/                ← 组件库
│   ├── utils/             ← 工具函数
│   ├── config/            ← 共享配置(ESLint、TS 等)
│   └── types/             ← 共享类型定义
├── tools/                 ← 内部工具
│   └── scripts/
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
└── tsconfig.base.json     ← 共享 TS 基础配置

5.2 共享配置

ESLint、TypeScript、Prettier 等工具的配置可以抽成一个共享包:

// packages/config/eslint-config/index.js
module.exports = {
  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
  rules: {
    // 团队统一规则
  }
};
// apps/web/.eslintrc.js
module.exports = {
  extends: ['@myorg/eslint-config']
};

5.3 版本管理与发布

Monorepo 中包的版本管理有两种策略:

统一版本(Fixed): 所有包使用相同的版本号。改了任何一个包,所有包一起升版。适合紧密耦合的包集(如 Babel 的各个子包)。

独立版本(Independent): 每个包独立管理版本号,只有实际修改的包才升版。适合松散耦合的包集。

常用的版本管理工具:

  • Changesets:社区最流行的方案,和 pnpm workspace 配合良好
  • Lerna:老牌工具,现已被 Nx 收购和维护
# Changesets 工作流
pnpm changeset         # 1. 记录变更
pnpm changeset version # 2. 根据变更生成版本号
pnpm changeset publish # 3. 发布到 npm

5.4 CI/CD 优化

大型 Monorepo 的 CI/CD 必须做到增量,否则每次 PR 都要全量构建/测试,时间不可接受。

# GitHub Actions 示例
- name: Build affected packages
  run: turbo build --filter=...[HEAD~1]
  # 只构建受本次提交影响的包

关键策略:

  1. 只构建受影响的包:Turborepo/Nx 都支持分析哪些包受当前变更影响
  2. 利用远程缓存:CI 上的构建结果缓存到远程,下次 CI 直接复用
  3. 并行化:充分利用 CI 的多核能力

常见误区

误区一:“Monorepo 就是把所有代码放一起”

Monorepo 的核心不是”放一起”,而是有组织地放一起。每个包应该有清晰的边界(独立的 package.json、独立的构建配置),包之间的依赖关系应该是显式声明的。如果所有代码都耦合在一起没有明确边界,那不叫 Monorepo,叫”大泥球”。

误区二:“Monorepo 一定比 Polyrepo 好”

不是。Monorepo 的优势在于代码共享和统一管理,但它也带来了复杂性:需要额外的工具(workspace、任务编排)、CI/CD 需要增量能力、Git 仓库可能变得很大。如果你的几个项目之间几乎没有代码共享,Polyrepo 反而更简单直接。

误区三:“有了 pnpm workspace 就够了,不需要 Turborepo”

pnpm workspace 只解决依赖管理问题,不解决任务编排问题。没有 Turborepo 或 Nx,你的 pnpm -r run build 会串行执行所有包的 build 任务,没有并行、没有缓存、没有增量构建。项目小的时候没感觉,包一多就会成为瓶颈。

误区四:“Turborepo 和 Nx 只能二选一”

虽然它们有功能重叠,但确实是二选一的——在同一个项目中使用两者没有意义。选择时考虑项目规模和团队偏好:Turborepo 轻量、上手快;Nx 功能全、适合大型项目和 Angular 生态。


小结

本章我们从 Monorepo 的核心理念出发,对比了 Monorepo 和 Polyrepo 的适用场景,深入拆解了 pnpm workspace(依赖管理)和 Turborepo(任务编排)的工作原理,并分享了大型项目的实践经验。

核心要点

  1. Monorepo ≠ 巨型单体:Monorepo 是多个独立包共存于一个仓库,有清晰边界
  2. pnpm workspace 做依赖管理workspace:* 本地链接、严格依赖隔离
  3. Turborepo 做任务编排:拓扑分析 → 最大并行 → 缓存加速
  4. 缓存是关键:本地缓存 + 远程缓存,让增量构建成为可能
  5. 版本管理用 Changesets:记录变更 → 生成版本号 → 发布
  6. CI/CD 必须增量化:只构建/测试受影响的包

本章思维导图

Monorepo
  • 概念
    • Monorepo vs Polyrepo
    • 不是"大泥球",是有组织的多包共存
  • 依赖管理:pnpm workspace
    • pnpm-workspace.yaml 定义包范围
    • workspace:* 本地链接
    • 依赖隔离(防止幽灵依赖)
    • pnpm --filter 精确操作
  • 任务编排:Turborepo
    • 任务拓扑图(Task Graph)
    • ^build:拓扑依赖
    • 缓存(本地 + 远程)
    • 增量构建:只处理受影响的包
  • Nx
    • 全功能 Monorepo 平台
    • vs Turborepo:重 vs 轻
    • 适合大型企业级项目
  • 实践
    • 目录结构:apps / packages / tools
    • 共享配置:ESLint / TS / Prettier
    • 版本管理:Changesets / 统一版本 vs 独立版本
    • CI/CD:增量构建 + 远程缓存

练习挑战

第一题 ⭐(基础):概念辨析

请判断以下说法是否正确,并说明原因:

  1. “Monorepo 意味着所有代码必须用同一种语言”
  2. “pnpm workspace 可以让包 A 直接 import 包 B 的源码”
  3. “Turborepo 的缓存只能在本地使用”
点击查看答案与解析
  1. 错误。 Monorepo 是仓库组织方式,和编程语言无关。一个 Monorepo 里可以同时有 TypeScript 前端项目、Go 后端服务和 Python 脚本。Google 的 Monorepo 就包含了几乎所有语言的代码。

  2. 正确。 使用 workspace:* 声明依赖后,pnpm 会创建符号链接,让包 A 的 node_modules 中的包 B 直接指向包 B 的源码目录。修改包 B 的代码后,包 A 中立即生效,不需要重新发布。

  3. 错误。 Turborepo 支持远程缓存(Remote Cache)。可以通过 Vercel Remote Cache 或自建缓存服务器,让团队成员和 CI 共享构建缓存。

第二题 ⭐⭐(进阶):配置 turbo.json

一个 Monorepo 有以下包依赖关系:web 依赖 uiutilsui 依赖 utils。每个包都有 buildtestlint 三个任务。请配置 turbo.json,要求:

  1. build 必须先构建上游依赖
  2. test 必须在本包的 build 完成后才能执行
  3. lint 无依赖,可以和任何任务并行
点击查看答案与解析
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    }
  }
}

执行 turbo build test lint 时,Turborepo 会:

  1. 先构建 utils(没有上游依赖)
  2. utils 构建完成后,并行构建 uiweb(取决于 web 是否等 ui——因为 web 依赖 ui,所以 web 的 build 要等 ui 的 build)
  3. 实际顺序:utils.buildui.buildweb.build
  4. 每个包的 test 在其 build 完成后执行
  5. 所有包的 lint 可以随时并行执行

^build 中的 ^ 是关键——它表示”我的上游依赖的 build 任务”,而不是”当前包自己的某个任务”。

第三题 ⭐⭐⭐(综合):Monorepo 选型方案

你们团队有以下项目:一个 React Web 应用、一个 React Native 移动端应用、一个共享组件库、一个共享工具函数库、一个 Node.js BFF 服务。目前是 5 个独立仓库,频繁出现”改了组件库要等发布、其他项目才能用”的问题。

请设计一个 Monorepo 迁移方案,包括:目录结构、工具选型、依赖管理策略、CI/CD 方案。

点击查看答案与解析

一、目录结构

monorepo/
├── apps/
│   ├── web/                    ← React Web 应用
│   ├── mobile/                 ← React Native 应用
│   └── bff/                    ← Node.js BFF
├── packages/
│   ├── ui/                     ← 共享组件库
│   ├── utils/                  ← 共享工具函数
│   ├── config/                 ← 共享配置
│   │   ├── eslint-config/
│   │   └── tsconfig/
│   └── types/                  ← 共享类型定义
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
└── .changeset/                 ← Changesets 配置

二、工具选型

用途工具原因
包管理器PNPM严格依赖、省磁盘、内置 workspace
任务编排Turborepo轻量、上手快、缓存能力强
版本管理Changesets社区主流、和 PNPM 配合好
CIGitHub Actions配合 Turborepo 远程缓存

三、依赖管理策略

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
  • 包之间用 workspace:* 互相引用
  • 共享的开发工具(ESLint、Prettier、TypeScript)安装在根目录
  • 每个包声明自己的运行时依赖

四、CI/CD 方案

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v4
        with:
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm turbo build test lint --cache-dir=.turbo

关键点:

  1. 使用 --frozen-lockfile 确保 CI 和本地安装一致
  2. Turborepo 自动只构建受变更影响的包
  3. 配置远程缓存进一步加速 CI

自我检测

  • 能说清楚 Monorepo 和 Polyrepo 的各自优缺点和适用场景
  • 能解释 pnpm workspace 中 workspace:* 的作用和工作原理
  • 能描述 Turborepo 的任务拓扑图和缓存机制
  • 能区分 pnpm workspace 和 Turborepo 各自解决的问题
  • 能说出 Turborepo 和 Nx 的核心差异和各自适用场景
  • 能设计一个合理的 Monorepo 目录结构
  • 能解释版本管理的两种策略(统一版本 vs 独立版本)
  • 能说出大型 Monorepo CI/CD 的增量构建策略

购买课程解锁全部内容

大厂前端面试通关:71 篇构建完整知识体系

¥89.90