文章

Monorepo架构深度解析:从Lerna到Turborepo的工具链进化

Monorepo 把多个相关项目统一在一个仓库管理,核心挑战是依赖管理、任务编排与构建缓存。 从 Lerna 手动编排到 Turborepo 增量缓存,工具链进化是手动控制到智能增量的缩影,面试常考它解决什么痛点与缓存原理。

Monorepo架构深度解析:从Lerna到Turborepo的工具链进化

一句话概括

Monorepo 将多个相关项目的代码统一在一个仓库管理,核心挑战在于依赖管理、任务编排和构建缓存。从 Lerna 的手动编排到 Turborepo 的自动缓存计算,工具链的进化史就是”从手动控制到增量智能”的缩影。

核心知识点

1. 为什么需要 Monorepo

Multi-Repo 的典型痛苦:修改一个 API → 改 A 库 → 提 PR → 合并 → 发布 npm → 更新 B 库的依赖 → 提另一个 PR。两个 PR、一次发布、多次 CI,一个改动要等一整天。

Monorepo 的价值:原子变更。所有相关包的改动在一次 PR 中完成,内部依赖的变更立即可见,不需要等待发布。

2. Turborepo 的 Task Graph

Turborepo 的核心创新是 dependsOn 定义任务依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],  // ← ^ 表示先构建上游依赖包
      "outputs": ["dist/**"],
      "cache": true
    },
    "test": {
      "dependsOn": ["build"],   // 先构建,再测试
      "outputs": ["coverage/**"],
      "cache": true
    },
    "lint": {
      "dependsOn": [],          // lint 不依赖其他任务
      "cache": false
    }
  }
}

^build 的含义:packages/ui 的 build 依赖 packages/utils(ui 的依赖)先完成 build。Turborepo 自动按拓扑排序执行,同一层的并行跑:

1
2
3
4
5
6
# 构建所有包,自动拓扑排序
turbo run build
# 只构建 ui 包及其依赖
turbo run build --filter=@my-app/ui
# 只构建相比 main 分支有变更的包
turbo run build --filter=[main...HEAD]

3. 哈希缓存:没变就不跑

Turborepo 缓存的核心是计算输入哈希:

1
2
3
4
5
6
7
8
// 缓存 key 的组成(伪代码)
taskHash = sha256(
  globHash('src/**/*'),          // 源码文件内容哈希
  depOutputHashes,               // 依赖包构建输出的哈希
  globHash(['tsconfig.json']),   // 全局依赖
  hashEnv(['NODE_ENV']),         // 环境变量
  task.command,                  // 命令本身
);

哈希没变 → 直接从缓存恢复 dist/ 目录(0ms)。一个 50 包 Monorepo,全量构建 10 分钟 → 增量构建 1-2 分钟。

4. 版本发布:Changesets

1
2
3
4
5
6
7
8
9
# 1. 开发完成后,创建 changeset(记录变更)
npx changeset
# 交互式:选择哪些包有变更 → 选择 major/minor/patch → 输入说明

# 2. CI 自动生成版本号 + CHANGELOG
npx changeset version

# 3. 发布到 npm
npx changeset publish

版本模式选项:

  • 独立模式:每个包独立版本号(recommended)
  • 固定模式:所有包统一版本号(如 React 家族)
  • 链接模式:关联包保持版本关系

5. 三工具对比

工具缓存远程缓存适用场景
Lerna❌ 无❌简单工作流(已过时)
Turborepo✅ 哈希缓存✅ Vercel RC大部分项目,上手快
Nx✅ 哈希 + AST 依赖分析✅ Nx Cloud依赖关系复杂,需要更多控制

Turborepo 更简单开箱即用,Nx 更强大(能分析源码 import 语句发现隐式依赖)。

其实你每天都在用

  • turbo run build 第二次跑秒级完成:哈希缓存命中,直接从缓存恢复 dist 目录
  • 改了一个 utils 函数,只构建 ui 和 app:Turborepo 只跑受影响的包,不是全量 50 个
  • pnpm workspace 的 workspace:* 协议:开发时指向本地包,发布时自动替换为真实版本号
  • PR 的 CI 只跑变更包的测试:--filter=[main...HEAD] 只测试你改过的包
  • Changesets 自动生成 CHANGELOG:从变更记录推导语义版本号,不再手动维护

常见误解(FAQ)

❌ 误区 1:「Monorepo 就是把所有代码放在一个文件夹」

只放一起不做任务编排和缓存,Monorepo 是灾难:每次改动全量构建所有包。Turborepo/Nx 的增量构建和缓存才是 Monorepo 真正产生价值的地方。

❌ 误区 2:「Turborepo 和 Nx 功能重复,选一个就行」

两者思路不同:Turborepo 基于哈希缓存,简单开箱即用;Nx 做静态依赖分析(解析 import 语句),能发现”虽然没明确声明依赖但实际上引用了”的隐式依赖。大团队或依赖关系复杂的项目可能更适合 Nx。

❌ 误区 3:「Lerna 已经死透了」

Lerna 在 2022 年被 Nx 团队接手后(Lerna v7+),底层默认使用 Nx 做任务编排,本质上成了 Nx 的上层封装。如果团队习惯 Lerna 的 API,可以继续用但享受 Nx 的缓存能力。

❌ 误区 4:「Monorepo 构建太慢,不如拆仓库」

慢的是没有缓存的 Monorepo。Turborepo 的哈希缓存 + 远程缓存可以让 CI 构建时间从全量的 10 分钟降到增量的 30 秒。问题不在 Monorepo 本身,在于是否用了正确的工具。

一句话总结

Monorepo 工具的核心价值不是”把代码放一起”,而是把”人知道怎么编排”变为”工具自动发现并编排”——配置好 turbo.json 后,每次 turbo run build 都是一次精确的拓扑排序 + 增量计算 + 缓存命中的自动化工程。

本文由作者按照 CC BY 4.0 进行授权