Skip to content

Monorepo

Monorepo(Monolithic Repository)是一种将多个项目、多个包放在同一个代码仓库中进行统一管理的代码组织策略。它不是简单地把代码塞到一个仓库里,而是通过工具链支撑 + 工程化约束,实现多包之间的高效协作。Google、Meta、Microsoft、Vercel 等公司都在大规模使用 Monorepo。

本文将从核心概念 → 工具链深度解析(pnpm workspace / Turborepo / Nx / Changesets)→ 工具横向对比 → 最佳实践 → 面试高频题全面拆解 Monorepo 工程化体系。


一、Monorepo 概念

1.1 三种代码组织方式

在软件工程实践中,代码的组织方式主要有三种:

1. Monolith(单体应用)

┌─────────────────────────────────────┐
│              一个仓库                 │
│  ┌───────────────────────────────┐  │
│  │         一个巨大的应用          │  │
│  │                               │  │
│  │  前端 + 后端 + 数据库 + 配置    │  │
│  │  所有代码耦合在一起             │  │
│  │  共用一个 package.json         │  │
│  │  共用一套构建流程               │  │
│  │                               │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

2. Multirepo(多仓库)

┌──────────┐  ┌──────────┐  ┌──────────┐
│  repo-A  │  │  repo-B  │  │  repo-C  │
│          │  │          │  │          │
│ 独立仓库  │  │ 独立仓库  │  │ 独立仓库  │
│ 独立发布  │  │ 独立发布  │  │ 独立发布  │
│ 独立 CI   │  │ 独立 CI   │  │ 独立 CI   │
└──────────┘  └──────────┘  └──────────┘
     ↕               ↕              ↕
  通过 npm 发布后互相引用,版本协调成本高

3. Monorepo(单仓多包)

┌─────────────────────────────────────────┐
│                 一个仓库                  │
│                                         │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐   │
│  │ app-web │ │ app-api │ │app-admin│   │
│  └────┬────┘ └────┬────┘ └────┬────┘   │
│       │           │           │         │
│       └─────┬─────┘───────────┘         │
│             ↓                           │
│  ┌──────────────────────────────────┐   │
│  │       packages/shared-utils      │   │
│  │       packages/ui-components     │   │
│  │       packages/config            │   │
│  └──────────────────────────────────┘   │
│                                         │
│  统一依赖管理 · 原子提交 · 代码复用       │
└─────────────────────────────────────────┘

1.2 三种方式对比

维度MonolithMultirepoMonorepo
代码组织一个项目一份代码多个仓库各自独立一个仓库多个包
代码共享天然共享(但易耦合)通过 npm 包共享通过 workspace 直接引用
依赖管理单一 package.json各仓库独立管理统一管理 + 自动提升
版本协调无需协调手动协调版本统一版本或独立版本
原子提交✅ 天然支持❌ 跨仓库无法原子提交✅ 支持
CI/CD简单各仓库独立配置统一配置 + 增量构建
构建速度全量构建各仓库独立构建增量构建 + 缓存
权限管理简单✅ 仓库级别隔离需要额外工具(CODEOWNERS)
仓库体积中等可能很大
适用场景小型项目独立性强的项目关联性强的多项目

1.3 Monorepo 的优势

代码共享与复用:不同应用可以直接引用内部包,无需发布到 npm,修改即时生效。

修改 packages/ui-components 中的 Button 组件
  → app-web 立即可见最新代码
  → app-admin 立即可见最新代码
  → 无需 npm publish → npm install 的繁琐流程

统一依赖管理:所有项目共享同一份 node_modules(经过优化),避免依赖版本不一致导致的 "在我这能跑" 问题。

原子提交:一次 commit 可以同时修改多个包,保证跨包变更的一致性。比如修改一个共享 API 的接口签名,可以在同一个 PR 中同步修改所有调用方。

统一 CI/CD:一套 CI 配置覆盖所有项目,结合增量构建只测试受影响的包。

统一代码规范:所有项目共享同一份 ESLint、Prettier、TypeScript 配置,保证代码风格一致。

1.4 Monorepo 的劣势

仓库体积膨胀:随着项目增多,仓库体积会越来越大,git clonegit status 等操作变慢。需要 Git 的 sparse checkout、shallow clone 等策略缓解。

构建速度:如果没有增量构建工具的支持,每次 CI 都需要全量构建所有包,耗时巨大。

权限管理复杂:所有代码在同一仓库,无法通过仓库级别的权限来隔离。需要依赖 CODEOWNERS 文件和 CI 检查来实现细粒度权限控制。

工具链门槛:需要掌握 pnpm workspace、Turborepo/Nx、Changesets 等一整套工具链。

1.5 Monorepo 典型项目结构

my-monorepo/
├── apps/                          # 应用层
│   ├── web/                       # Web 前端应用
│   │   ├── src/
│   │   ├── package.json
│   │   └── vite.config.ts
│   ├── admin/                     # 管理后台
│   │   ├── src/
│   │   ├── package.json
│   │   └── vite.config.ts
│   └── api/                       # Node.js 后端
│       ├── src/
│       └── package.json

├── packages/                      # 共享包层
│   ├── ui/                        # 共享 UI 组件库
│   │   ├── src/
│   │   │   ├── Button/
│   │   │   ├── Modal/
│   │   │   └── index.ts
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── utils/                     # 工具函数库
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── types/                     # 共享类型定义
│   │   ├── src/
│   │   └── package.json
│   └── config/                    # 共享配置
│       ├── eslint/
│       ├── tsconfig/
│       └── package.json

├── tools/                         # 工具脚本层
│   ├── scripts/
│   └── generators/

├── package.json                   # 根 package.json
├── pnpm-workspace.yaml            # pnpm workspace 配置
├── turbo.json                     # Turborepo 配置
├── .changeset/                    # Changesets 配置
│   └── config.json
├── .github/
│   └── workflows/
│       └── ci.yml
└── pnpm-lock.yaml

二、pnpm workspace

2.1 为什么选择 pnpm

pnpm(performant npm)是目前 Monorepo 场景下最主流的包管理器。相比 npm 和 yarn,pnpm 有两个核心优势:

优势一:内容寻址存储(Content-Addressable Storage)

npm 和 yarn 会在每个项目的 node_modules 中保存依赖的完整副本。如果你有 10 个项目都依赖 lodash@4.17.21,磁盘上会存在 10 份完全相同的 lodash 代码。

pnpm 使用全局的内容寻址存储(默认在 ~/.pnpm-store/),所有版本的所有包只存储一份。项目中的 node_modules 通过硬链接指向全局存储:

pnpm 的存储机制:

~/.pnpm-store/v3/                     # 全局存储(内容寻址)
├── files/
│   ├── 00/
│   │   ├── 5a8...hash               # lodash 的某个文件
│   │   └── 3b2...hash               # react 的某个文件
│   ├── 01/
│   └── ...

项目 node_modules(非扁平结构):

my-monorepo/
├── node_modules/
│   ├── .pnpm/                        # 虚拟存储目录
│   │   ├── lodash@4.17.21/
│   │   │   └── node_modules/
│   │   │       └── lodash/           # 硬链接 → ~/.pnpm-store
│   │   ├── react@18.2.0/
│   │   │   └── node_modules/
│   │   │       ├── react/            # 硬链接 → ~/.pnpm-store
│   │   │       └── loose-envify/     # 符号链接 → .pnpm/loose-envify@1.4.0
│   │   └── ...
│   ├── lodash → .pnpm/lodash@4.17.21/node_modules/lodash    # 符号链接
│   └── react → .pnpm/react@18.2.0/node_modules/react        # 符号链接

优势二:严格的依赖隔离

npm 和 yarn 采用扁平化(flat)的 node_modules 结构,这导致了**幽灵依赖(Phantom Dependencies)**问题——你可以引用没有在 package.json 中声明的包。

幽灵依赖问题:

package.json 只声明了 express
  → npm install 后 node_modules 被扁平化
  → body-parser(express 的依赖)也出现在 node_modules 顶层
  → 你的代码 import bodyParser from 'body-parser' 可以运行
  → 但某天 express 不再依赖 body-parser,你的代码突然就挂了

pnpm 的非扁平结构天然杜绝了这个问题:
  → node_modules 顶层只有你声明的直接依赖的符号链接
  → 未声明的包无法被访问

2.2 pnpm-workspace.yaml 配置

在项目根目录创建 pnpm-workspace.yaml,声明哪些目录属于 workspace:

yaml
packages:
  - 'apps/*'
  - 'packages/*'
  - 'tools/*'

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

更灵活的配置:

yaml
packages:
  - 'apps/*'
  - 'packages/*'
  - 'packages/config/*'
  - '!packages/deprecated-*'

使用 ! 前缀可以排除特定的包。

2.3 workspace 协议

在 Monorepo 中,包之间的依赖引用使用 workspace: 协议:

json
{
  "name": "@myorg/web",
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "@myorg/utils": "workspace:^1.0.0",
    "@myorg/types": "workspace:~1.0.0"
  }
}

workspace: 协议的几种写法:

写法含义发布时转换为
workspace:*任意版本,始终引用本地1.2.3(当前本地版本)
workspace:^兼容版本^1.2.3
workspace:~近似版本~1.2.3
workspace:^1.0.0指定兼容范围^1.0.0

关键点:workspace: 协议在本地开发时始终链接到本地包,发布时 pnpm 会自动将其转换为实际版本号

2.4 包之间的依赖引用

假设我们有以下包结构:

packages/
├── ui/
│   └── package.json    # name: @myorg/ui
├── utils/
│   └── package.json    # name: @myorg/utils
└── types/
    └── package.json    # name: @myorg/types

@myorg/ui 中引用 @myorg/utils

json
{
  "name": "@myorg/ui",
  "version": "1.0.0",
  "dependencies": {
    "@myorg/utils": "workspace:*",
    "@myorg/types": "workspace:*"
  }
}

安装后,pnpm 会创建符号链接:

node_modules/
├── @myorg/
│   ├── ui → ../../packages/ui          # 符号链接到本地包
│   ├── utils → ../../packages/utils    # 符号链接到本地包
│   └── types → ../../packages/types    # 符号链接到本地包

在代码中就可以直接 import:

typescript
import { formatDate } from '@myorg/utils'
import type { User } from '@myorg/types'

2.5 常用命令

根目录执行命令

bash
pnpm install

pnpm run build

pnpm add typescript -D -w

-w 表示在根 workspace 安装依赖。

过滤执行(--filter)

bash
pnpm --filter @myorg/web dev

pnpm --filter @myorg/ui build

pnpm --filter @myorg/web add axios

pnpm --filter "@myorg/*" build

pnpm --filter @myorg/web... build

--filter 的高级用法:

命令含义
--filter @myorg/web只在 @myorg/web 包中执行
--filter "@myorg/*"匹配所有 @myorg 作用域的包
--filter @myorg/web...@myorg/web 及其所有依赖
--filter ...@myorg/ui@myorg/ui 及所有依赖它的包
--filter @myorg/web...[origin/main]@myorg/web 及其自 main 分支以来有变更的依赖

递归执行(-r)

bash
pnpm -r build

pnpm -r --parallel build

pnpm -r exec -- rm -rf dist

pnpm -r publish --access public

-r 在所有包中递归执行,--parallel 表示并行执行。


三、Turborepo

Turborepo 是 Vercel 开源的高性能 Monorepo 构建系统。它的核心价值在于:智能任务编排 + 增量构建 + 远程缓存,让 Monorepo 的构建速度从线性增长变为近乎恒定。

3.1 核心架构

Turborepo 核心架构:

┌────────────────────────────────────────────────────┐
│                    turbo run build                  │
│                         │                          │
│                         ▼                          │
│  ┌─────────────────────────────────────────────┐   │
│  │           1. 构建任务依赖图                    │   │
│  │                                             │   │
│  │  读取所有 package.json + turbo.json          │   │
│  │  解析 dependsOn 关系                         │   │
│  │  生成有向无环图(DAG)                        │   │
│  └──────────────────┬──────────────────────────┘   │
│                     ▼                              │
│  ┌─────────────────────────────────────────────┐   │
│  │           2. 计算内容哈希                      │   │
│  │                                             │   │
│  │  对每个任务计算输入文件的哈希值                  │   │
│  │  包含:源代码 + 依赖版本 + 环境变量 + 配置       │   │
│  └──────────────────┬──────────────────────────┘   │
│                     ▼                              │
│  ┌─────────────────────────────────────────────┐   │
│  │           3. 查找缓存                         │   │
│  │                                             │   │
│  │  本地缓存 → 远程缓存 → 未命中则执行任务         │   │
│  │  命中则直接复制缓存产物                        │   │
│  └──────────────────┬──────────────────────────┘   │
│                     ▼                              │
│  ┌─────────────────────────────────────────────┐   │
│  │           4. 并行执行                         │   │
│  │                                             │   │
│  │  基于 DAG 拓扑排序                            │   │
│  │  无依赖关系的任务自动并行                       │   │
│  │  最大化 CPU 利用率                            │   │
│  └─────────────────────────────────────────────┘   │
└────────────────────────────────────────────────────┘

3.2 turbo.json 配置

json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": [
    "**/.env.*local",
    ".env"
  ],
  "globalEnv": ["NODE_ENV", "CI"],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "tsconfig.json", "vite.config.*"],
      "outputs": ["dist/**", ".next/**"],
      "env": ["VITE_API_URL"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**", "tests/**", "vitest.config.*"],
      "outputs": ["coverage/**"]
    },
    "lint": {
      "dependsOn": [],
      "inputs": ["src/**", ".eslintrc.*", "eslint.config.*"],
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "tsconfig.json"],
      "outputs": []
    }
  }
}

配置详解

  • dependsOn: ["^build"]^ 前缀表示先执行所有上游依赖包build 任务。比如 @myorg/web 依赖 @myorg/ui,那么 @myorg/uibuild 会先执行。
  • dependsOn: ["build"]:没有 ^ 前缀表示先执行当前包自身build 任务。
  • dependsOn: []:没有依赖,可以和其他任务并行执行。
  • inputs:声明任务的输入文件,只有这些文件变化时才会重新执行。
  • outputs:声明任务的输出目录,用于缓存。
  • cache: false:禁用缓存,适用于 dev 这类长期运行的任务。
  • persistent: true:标记为持久运行的任务(如 dev server),Turborepo 不会等待它结束。

3.3 dependsOn 与任务依赖图

dependsOn 是 Turborepo 最核心的概念。它定义了任务之间的依赖关系:

项目依赖关系:
@myorg/web  →  依赖  →  @myorg/ui  →  依赖  →  @myorg/utils

运行 turbo run build 时的任务依赖图(DAG):

                ┌─────────────────┐
                │  @myorg/utils   │
                │     build       │
                └────────┬────────┘


                ┌─────────────────┐
                │   @myorg/ui     │
                │     build       │
                └────────┬────────┘

              ┌──────────┴──────────┐
              ▼                     ▼
   ┌─────────────────┐  ┌─────────────────┐
   │  @myorg/web     │  │ @myorg/admin    │
   │     build       │  │     build       │
   └─────────────────┘  └─────────────────┘

执行顺序:
  第 1 步:@myorg/utils build       (无依赖,最先执行)
  第 2 步:@myorg/ui build          (依赖 utils,utils 完成后执行)
  第 3 步:@myorg/web build  ┐
           @myorg/admin build┘      (依赖 ui,ui 完成后并行执行)

更复杂的场景——同一个包的多个任务依赖:

turbo run test 的完整任务图:

  @myorg/utils     @myorg/ui       @myorg/web
  ┌──────────┐    ┌──────────┐    ┌──────────┐
  │  build   │    │  build   │    │  build   │
  └────┬─────┘    └────┬─────┘    └────┬─────┘
       │    ╲          │    ╲          │
       │     ╲         │     ╲         │
       ▼      ╲        ▼      ╲        ▼
  ┌──────────┐ ╲  ┌──────────┐ ╲  ┌──────────┐
  │   test   │  ╲→│   test   │  ╲→│   test   │
  └──────────┘    └──────────┘    └──────────┘

  test 依赖自身 build(dependsOn: ["build"])
  build 依赖上游 build(dependsOn: ["^build"])

3.4 增量构建:基于内容哈希的缓存

Turborepo 的缓存机制是其性能的关键。对于每个任务,Turborepo 会计算一个内容哈希

哈希计算的输入:
┌─────────────────────────────────────────┐
│  inputs 声明的所有文件内容                  │
│  + 所有依赖包的哈希值                      │
│  + 锁文件(pnpm-lock.yaml)的相关部分      │
│  + turbo.json 中该任务的配置               │
│  + env 声明的环境变量值                    │
│  + globalDependencies 文件内容            │
│  + globalEnv 环境变量值                   │
│                                         │
│  → SHA-256 → 得到唯一哈希值                │
│  → 例如:3a1b9c2d...                     │
└─────────────────────────────────────────┘

缓存命中流程:

  turbo run build


  计算 @myorg/ui#build 的哈希 → 3a1b9c2d


  查找缓存:node_modules/.cache/turbo/3a1b9c2d.tar.zst

       ├── 命中 → 解压缓存产物到 dist/ → 跳过实际构建
       │          终端显示 "cache hit, replaying output"
       │          耗时:< 100ms

       └── 未命中 → 执行实际构建
                   构建完成后将 outputs 打包存入缓存
                   耗时:实际构建时间

3.5 远程缓存

本地缓存只能一个开发者自己享用。Turborepo 支持远程缓存(Remote Caching),让团队中所有成员和 CI 共享构建缓存:

远程缓存协作流程:

开发者 A                      Remote Cache                    开发者 B
┌──────────┐                ┌──────────────┐               ┌──────────┐
│ 修改代码  │                │              │               │          │
│ 运行 build│───上传缓存────→│  Vercel / 自  │               │          │
│ 耗时 30s  │                │  部署 Server  │               │          │
└──────────┘                │              │               │          │
                            │  存储:        │               │ git pull │
                            │  hash→产物    │───下载缓存────→│ 运行 build│
                            │              │               │ 耗时 <1s │
                            └──────────────┘               └──────────┘

CI Pipeline                                    
┌──────────┐                                   
│ 运行 build│───查询远程缓存───→ 命中!→ 跳过构建  
│ 耗时 <1s  │                                   
└──────────┘

启用远程缓存:

bash
npx turbo login

npx turbo link

自托管远程缓存(使用开源实现):

json
{
  "remoteCache": {
    "enabled": true,
    "signature": true
  }
}
bash
TURBO_API=https://cache.mycompany.com TURBO_TOKEN=xxx TURBO_TEAM=myteam turbo run build

3.6 并行执行

Turborepo 基于任务依赖图(DAG)自动最大化并行度。通过 --concurrency 控制并行数:

bash
turbo run lint --concurrency=10

turbo run lint --concurrency=100%

并行执行的可视化:

时间线 ──────────────────────────────────────────────→

          ┌───────────────────┐
CPU 核 1: │ @myorg/utils build│
          └───────────────────┘
                              ┌───────────────────┐
CPU 核 1:                     │  @myorg/ui build  │
                              └───────────────────┘
          ┌──────────────────────────────┐
CPU 核 2: │   @myorg/utils lint          │
          └──────────────────────────────┘
          ┌──────────────────────────────┐
CPU 核 3: │    @myorg/ui lint            │
          └──────────────────────────────┘
          ┌──────────────────────────────┐
CPU 核 4: │   @myorg/web lint            │
          └──────────────────────────────┘
                                         ┌───────────────────┐
CPU 核 2:                                │ @myorg/web build  │
                                         └───────────────────┘
                                         ┌───────────────────┐
CPU 核 3:                                │@myorg/admin build │
                                         └───────────────────┘

lint 任务无依赖(dependsOn: []),所以全部并行
build 任务有依赖关系,按拓扑顺序执行
两者之间也可以并行(不同任务类型之间无冲突)

3.7 常用命令

bash
turbo run build

turbo run build --filter=@myorg/web...

turbo run build --dry

turbo run build --graph

turbo run build --summarize

turbo run build --force

turbo prune --scope=@myorg/web --docker
命令用途
--dry干运行,只展示将要执行的任务
--graph生成任务依赖图的 SVG/DOT
--summarize输出构建摘要(命中率、耗时等)
--force忽略缓存,强制执行所有任务
turbo prune提取指定包及其依赖,生成精简的子集仓库(适用于 Docker 构建)

四、Nx

4.1 Nx 核心特性

Nx 是由 Nrwl 公司开发的 Monorepo 工具,功能比 Turborepo 更丰富,但学习曲线也更陡峭。

项目图(Project Graph)

Nx 在内存中维护一个完整的项目依赖图,通过分析源代码中的 import 语句自动推导项目之间的依赖关系:

Nx 项目图:

┌──────────────────────────────────────────────┐
│               Nx Project Graph               │
│                                              │
│  ┌────────┐         ┌────────┐              │
│  │  web   │────────→│   ui   │              │
│  └────┬───┘         └───┬────┘              │
│       │                 │                    │
│       │    ┌────────┐   │                    │
│       └───→│ utils  │←──┘                    │
│            └────────┘                        │
│                 ↑                            │
│            ┌────────┐                        │
│            │ admin  │                        │
│            └────────┘                        │
│                                              │
│  自动从源代码 import 推导依赖关系               │
│  无需手动维护配置                              │
└──────────────────────────────────────────────┘

受影响命令(Affected)

Nx 的 affected 命令只运行受变更影响的项目的任务:

bash
nx affected -t build

nx affected -t test --base=main --head=HEAD

nx affected:graph
affected 工作原理:

  1. 比较 HEAD 与 base(默认 main)的文件差异
  2. 在项目图中标记变更的项目
  3. 递归标记所有下游依赖项目
  4. 只对标记的项目执行任务

示例:只修改了 @myorg/utils

  @myorg/utils  ← 直接修改  ✅ 受影响

  @myorg/ui     ← 依赖 utils ✅ 受影响

  @myorg/web    ← 依赖 ui   ✅ 受影响
  @myorg/docs   ← 无关      ❌ 不受影响(跳过)

计算缓存

与 Turborepo 类似,Nx 也基于输入哈希进行任务缓存,避免重复计算。

4.2 nx.json 配置

json
{
  "$schema": "https://nx.dev/reference/nx-json",
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"],
    "production": [
      "default",
      "!{projectRoot}/**/*.spec.ts",
      "!{projectRoot}/tsconfig.spec.json"
    ]
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "outputs": ["{projectRoot}/dist"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production"],
      "cache": true
    },
    "lint": {
      "inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
      "cache": true
    }
  },
  "defaultBase": "main"
}

配置详解

  • namedInputs:定义可复用的输入集合。production 排除了测试文件,用于 build 任务。
  • targetDefaults:为所有项目的目标(target)设置默认配置。
  • {projectRoot}:当前项目的根目录。
  • {workspaceRoot}:整个 workspace 的根目录。
  • ^production:表示所有上游依赖的 production 输入。

4.3 Nx 特有功能

代码生成器(Generators)

bash
nx generate @nx/react:component Button --project=ui

nx generate @nx/js:library shared-types

模块边界规则(Module Boundary Rules)

通过标签和 ESLint 规则强制模块间的依赖约束:

json
{
  "rules": {
    "@nx/enforce-module-boundaries": [
      "error",
      {
        "depConstraints": [
          {
            "sourceTag": "scope:app",
            "onlyDependOnLibsWithTags": ["scope:lib", "scope:shared"]
          },
          {
            "sourceTag": "scope:lib",
            "onlyDependOnLibsWithTags": ["scope:shared"]
          }
        ]
      }
    ]
  }
}

这保证了架构分层不被破坏:app 层可以依赖 lib 层和 shared 层,但 lib 层不能反向依赖 app 层。

4.4 Turborepo vs Nx 对比

维度TurborepoNx
定位轻量级构建编排工具全功能 Monorepo 框架
学习曲线低,配置简单较高,概念多
配置文件turbo.json(简洁)nx.json + project.json(丰富)
任务编排✅ 基于 turbo.json✅ 基于 nx.json
本地缓存
远程缓存✅ Vercel 原生支持✅ Nx Cloud
项目图基于 package.json基于源代码分析(更精确)
受影响分析--filter 手动过滤nx affected 自动推导
代码生成器❌ 不内置✅ 丰富的生成器
模块边界❌ 不内置✅ 强制架构约束
插件生态少(专注构建)丰富(React、Angular、Node 等)
增量采用✅ 非常容易✅ 支持(但完整功能需更多配置)
适用场景中小型 Monorepo大型企业级 Monorepo

选型建议

  • 如果你追求简单易用、快速上手,选 Turborepo
  • 如果你需要强大的架构约束、代码生成、精确的 affected 分析,选 Nx
  • 两者都可以和 pnpm workspace 配合使用

五、Changesets

Changesets 是专为 Monorepo 设计的版本管理与发布工具。它解决了 Monorepo 中最头疼的问题:多个包如何独立版本管理、如何自动生成 CHANGELOG、如何协调关联包的版本更新

5.1 核心概念

一个 changeset 是一个描述变更的 Markdown 文件,记录了哪些包被修改以及变更的类型(major/minor/patch):

.changeset/
├── config.json
├── brave-dogs-dance.md        # 自动生成的随机文件名
└── happy-cats-fly.md

一个 changeset 文件的内容:

markdown
---
"@myorg/ui": minor
"@myorg/utils": patch
---

新增 DatePicker 组件,修复 formatDate 函数在 Safari 下的兼容问题。

5.2 工作流:add → version → publish

Changesets 完整工作流:

开发阶段:
┌──────────────────────────────────────────────┐
│  1. 开发者修改代码                              │
│  2. 运行 changeset add                        │
│     → 交互式选择受影响的包                       │
│     → 选择版本类型(major/minor/patch)          │
│     → 填写变更描述                              │
│     → 生成 .changeset/xxx.md 文件              │
│  3. 将 changeset 文件随代码一起提交              │
└──────────────────────────────────────────────┘


发布阶段:
┌──────────────────────────────────────────────┐
│  4. 运行 changeset version                    │
│     → 消费所有 .changeset/*.md 文件            │
│     → 计算每个包的新版本号                       │
│     → 更新 package.json 中的 version 字段      │
│     → 更新关联包的依赖版本                       │
│     → 生成/更新 CHANGELOG.md                   │
│     → 删除已消费的 changeset 文件               │
│                                              │
│  5. 审查并提交版本变更                           │
│                                              │
│  6. 运行 changeset publish                    │
│     → 检测哪些包版本号有变化                      │
│     → 执行 npm publish                        │
│     → 创建 git tag                            │
└──────────────────────────────────────────────┘

具体命令

bash
npx changeset add

交互式流程:

🦋  Which packages would you like to include?
◯ @myorg/web
◉ @myorg/ui
◉ @myorg/utils
◯ @myorg/admin

🦋  Which packages should have a major bump?
◯ @myorg/ui
◯ @myorg/utils

🦋  Which packages should have a minor bump?
◉ @myorg/ui
◯ @myorg/utils

🦋  The following packages will be patch bumped:
  @myorg/utils

🦋  Please enter a summary for this change:
  新增 DatePicker 组件,修复 formatDate 时区问题

🦋  Summary: 新增 DatePicker 组件,修复 formatDate 时区问题

🦋  === Changeset added! ===
bash
npx changeset version

执行后的变化:

Before:
  @myorg/ui:    1.2.0
  @myorg/utils: 2.0.5
  @myorg/web:   依赖 @myorg/ui: "workspace:^1.2.0"

After:
  @myorg/ui:    1.3.0  (minor bump)
  @myorg/utils: 2.0.6  (patch bump)
  @myorg/web:   依赖 @myorg/ui: "workspace:^1.3.0"  (自动更新)

同时生成:
  packages/ui/CHANGELOG.md        ← 追加变更记录
  packages/utils/CHANGELOG.md     ← 追加变更记录
bash
npx changeset publish

5.3 语义化版本(SemVer)

Changesets 严格遵循语义化版本规范:

版本号格式:MAJOR.MINOR.PATCH

MAJOR(主版本号):不兼容的 API 变更
  例:删除一个公开函数、修改函数签名
  1.2.3 → 2.0.0

MINOR(次版本号):向后兼容的功能新增
  例:新增一个组件、新增一个工具函数
  1.2.3 → 1.3.0

PATCH(补丁版本号):向后兼容的问题修复
  例:修复一个 bug、优化性能
  1.2.3 → 1.2.4

预发布版本:
  1.0.0-alpha.1
  1.0.0-beta.2
  1.0.0-rc.1

5.4 自动生成 CHANGELOG

changeset version 执行后,会自动在每个受影响的包目录下生成或更新 CHANGELOG.md

markdown
# @myorg/ui

## 1.3.0

### Minor Changes

- 新增 DatePicker 组件

### Patch Changes

- Updated dependencies
  - @myorg/utils@2.0.6

## 1.2.0

### Minor Changes

- 新增 Select 组件

5.5 Changesets 配置

.changeset/config.json

json
{
  "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [],
  "linked": [["@myorg/ui", "@myorg/utils"]],
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": ["@myorg/web", "@myorg/admin"]
}
字段含义
fixed绑定版本的包组,组内所有包始终保持同一版本号
linked关联版本的包组,组内包的版本号会协同升级
accessnpm 发布的访问级别(public/restricted)
updateInternalDependencies当依赖的内部包有版本变化时,如何更新本包版本
ignore忽略不需要发布的包(如应用层的包)

5.6 CI 中的自动化发布

结合 GitHub Actions 实现自动化版本发布:

yaml
name: Release

on:
  push:
    branches:
      - main

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile
      - run: pnpm run build

      - name: Create Release Pull Request or Publish
        uses: changesets/action@v1
        with:
          publish: pnpm changeset publish
          version: pnpm changeset version
          commit: 'chore: release packages'
          title: 'chore: release packages'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

这个 Action 会自动创建一个 "Version Packages" PR,合并后自动发布到 npm。


六、Monorepo 工具对比

6.1 包管理器对比:pnpm vs Yarn vs npm

维度pnpm workspaceYarn workspacenpm workspace
出现时间20172017(v1)2020(v7)
node_modules 结构非扁平(符号链接)扁平扁平
幽灵依赖✅ 杜绝❌ 存在❌ 存在
磁盘占用极低(硬链接 + CAS)
安装速度最快中等较慢
workspace 协议workspace:*workspace:**(隐式)
--filter 过滤✅ 功能丰富yarn workspace <name>--workspace
严格模式默认严格需手动配置
锁文件格式pnpm-lock.yamlyarn.lockpackage-lock.json
社区活跃度🔥 高速增长稳定稳定

6.2 构建工具对比:Turborepo vs Nx vs Lerna

维度TurborepoNxLerna(v6+)
维护方VercelNrwlNx(收购后)
定位构建编排工具全功能 Monorepo 框架版本管理 + 构建
配置复杂度
任务编排✅(使用 Nx 内核)
本地缓存✅(通过 Nx)
远程缓存✅ Vercel✅ Nx Cloud✅(通过 Nx)
增量构建✅ 内容哈希✅ 内容哈希✅(通过 Nx)
受影响分析手动 filter✅ 自动 affected✅(通过 Nx)
项目图可视化--graph 导出✅ 交互式 UI
代码生成✅ 丰富
模块边界
版本管理❌(配合 Changesets)✅ 内置✅ 内置
安装体积~10MB~40MB+~10MB
上手难度⭐⭐⭐⭐⭐⭐⭐⭐⭐
社区生态快速增长成熟丰富经典但逐渐被替代
适用项目中小型 Monorepo大型企业级项目已有 Lerna 项目迁移

6.3 选型决策树

开始选型

  ├── Q: 你的项目有多少个包?
  │   ├── < 10 个包
  │   │   └── pnpm workspace + Turborepo + Changesets
  │   │       简单高效,5 分钟上手
  │   │
  │   ├── 10-50 个包
  │   │   ├── 需要架构约束?
  │   │   │   ├── 是 → Nx
  │   │   │   └── 否 → Turborepo
  │   │   │
  │   │   └── 都搭配 pnpm workspace + Changesets
  │   │
  │   └── > 50 个包
  │       └── Nx(项目图 + affected 分析 + 模块边界)
  │           + pnpm workspace + Changesets

  └── Q: 你的团队对工具链的接受度?
      ├── 希望尽量简单 → Turborepo
      └── 愿意投入学习 → Nx

七、最佳实践

7.1 项目结构组织

推荐的三层目录结构:

monorepo/
├── apps/           # 应用层:可独立部署的应用
│   ├── web/        # 面向用户的 Web 应用
│   ├── admin/      # 管理后台
│   ├── api/        # 后端服务
│   └── docs/       # 文档站点

├── packages/       # 共享包层:被应用层引用的共享库
│   ├── ui/         # 共享 UI 组件
│   ├── utils/      # 工具函数
│   ├── types/      # 共享 TypeScript 类型
│   ├── hooks/      # 共享 React Hooks
│   └── config/     # 共享配置(见 7.2)

└── tools/          # 工具层:内部开发工具
    ├── scripts/    # 自动化脚本
    ├── generators/ # 代码生成器
    └── cli/        # 内部 CLI 工具

命名规范

统一使用 npm scope(如 @myorg/),所有包以 @myorg/ 开头:

json
{
  "name": "@myorg/ui",
  "name": "@myorg/utils",
  "name": "@myorg/web"
}

7.2 共享配置

共享 TypeScript 配置

packages/config/
└── tsconfig/
    ├── package.json
    ├── base.json
    ├── react-library.json
    └── node.json

base.json

json
{
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "moduleResolution": "bundler",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  }
}

react-library.json

json
{
  "extends": "./base.json",
  "compilerOptions": {
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "module": "ESNext",
    "target": "ES2022",
    "outDir": "./dist"
  }
}

node.json

json
{
  "extends": "./base.json",
  "compilerOptions": {
    "lib": ["ES2022"],
    "module": "CommonJS",
    "target": "ES2022",
    "outDir": "./dist"
  }
}

在子包中引用:

json
{
  "extends": "@myorg/tsconfig/react-library.json",
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist"
  },
  "include": ["src"]
}

共享 ESLint 配置

packages/config/
└── eslint/
    ├── package.json
    ├── base.js
    ├── react.js
    └── node.js

base.js

js
module.exports = {
  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint'],
  rules: {
    'no-console': 'warn',
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
  },
}

react.js

js
module.exports = {
  extends: [
    './base.js',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'plugin:jsx-a11y/recommended',
  ],
  settings: {
    react: { version: 'detect' },
  },
  rules: {
    'react/react-in-jsx-scope': 'off',
    'react/prop-types': 'off',
  },
}

在子包中引用:

js
module.exports = {
  extends: [require.resolve('@myorg/eslint-config/react')],
}

共享 Prettier 配置

根目录 prettier.config.js

js
module.exports = {
  semi: false,
  singleQuote: true,
  trailingComma: 'all',
  printWidth: 100,
  tabWidth: 2,
}

7.3 内部包开发策略

内部包有两种主要开发模式:

模式一:仅源码引用(推荐用于内部不发布的包)

不构建内部包,直接通过 TypeScript 引用源码:

json
{
  "name": "@myorg/utils",
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "default": "./src/index.ts"
    }
  }
}

优势:修改后即时生效,无需构建步骤。应用层的构建工具(Vite、Next.js)会直接编译这些源码。

模式二:预构建(推荐用于需要发布到 npm 的包)

使用 tsupunbuild 等工具将包预构建为 ESM/CJS:

json
{
  "name": "@myorg/ui",
  "version": "1.0.0",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts"
  }
}

7.4 Docker 构建优化

Monorepo 中的 Docker 构建需要特殊处理。Turborepo 提供了 turbo prune 命令来提取精简子集:

dockerfile
FROM node:20-alpine AS builder

RUN corepack enable && corepack prepare pnpm@9 --activate

WORKDIR /app
COPY . .
RUN pnpm turbo prune --scope=@myorg/api --docker

FROM node:20-alpine AS installer

RUN corepack enable && corepack prepare pnpm@9 --activate

WORKDIR /app
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN pnpm install --frozen-lockfile

COPY --from=builder /app/out/full/ .
RUN pnpm turbo run build --filter=@myorg/api

FROM node:20-alpine AS runner

WORKDIR /app
COPY --from=installer /app/apps/api/dist ./dist
COPY --from=installer /app/apps/api/package.json .

CMD ["node", "dist/index.js"]

turbo prune 生成的 out/ 目录结构:

out/
├── json/           # 只包含 package.json 文件(用于 pnpm install 缓存层)
│   ├── package.json
│   ├── apps/api/package.json
│   └── packages/utils/package.json
├── full/           # 完整的源代码(只包含相关的包)
│   ├── apps/api/
│   └── packages/utils/
└── pnpm-lock.yaml  # 精简后的锁文件

7.5 CI/CD 优化

yaml
name: CI

on:
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build
        run: pnpm turbo run build lint test typecheck
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

关键优化点:

  1. fetch-depth: 2:只拉取最近 2 次提交,减少 clone 时间
  2. cache: 'pnpm':缓存 pnpm store
  3. TURBO_TOKEN + TURBO_TEAM:启用远程缓存
  4. 一条 turbo run 命令执行多个任务,Turborepo 自动处理依赖和并行

八、面试高频问题

Q1:Monorepo 和 Multirepo 各适用于什么场景?如何选型?

回答思路

Multirepo 适合独立性强、团队分散、技术栈差异大的项目。每个仓库独立开发、独立发布、独立部署。典型场景:开源社区中的独立项目。

Monorepo 适合关联性强的多个项目:共享大量代码(组件库、工具函数、类型定义)、需要频繁跨包变更、需要统一代码规范和 CI/CD。典型场景:一个产品的前端应用 + 管理后台 + 组件库 + 工具包。

关键决策因素:代码复用程度、跨包变更频率、团队组织结构。如果每周都要跨多个包做变更,Monorepo 的原子提交和即时引用就是巨大优势。

Q2:pnpm 的 node_modules 结构和 npm/yarn 有什么区别?为什么能解决幽灵依赖?

回答思路

npm/yarn 使用扁平化(hoisting)的 node_modules,把所有依赖都提升到顶层。这导致你可以 import 没有在 package.json 中声明的包(幽灵依赖),一旦这个包不再是某个直接依赖的子依赖,代码就会突然报错。

pnpm 使用嵌套 + 符号链接的结构:node_modules 顶层只有直接依赖的符号链接,指向 .pnpm/ 目录下的实际位置。.pnpm/ 中每个包通过硬链接指向全局 content-addressable store,实现去重。

核心是三层结构:node_modules/<pkg> → 符号链接 → .pnpm/<pkg>@version/node_modules/<pkg> → 硬链接 → ~/.pnpm-store

Q3:Turborepo 的缓存机制是怎么工作的?

回答思路

Turborepo 对每个任务计算一个基于内容的哈希值(content hash),输入包括:声明的 inputs 文件内容、依赖包的哈希值、环境变量、turbo.json 配置、lockfile 相关部分。

如果哈希值与上次执行时相同,说明输入没变,直接从缓存(node_modules/.cache/turbo/)中恢复之前的 outputs 目录和终端输出,耗时从几十秒降到毫秒级。

支持远程缓存:将缓存推送到远端(Vercel 或自部署),团队成员和 CI 可以共享。开发者 A 构建过的产物,开发者 B 直接复用,无需重新构建。

Q4:什么是 dependsOn 中的 ^ 前缀?

回答思路

^ 表示拓扑依赖——先执行上游依赖包的同名任务。

dependsOn: ["^build"] 意味着:在执行当前包的 build 之前,先执行它在 package.json 中声明的所有 workspace 依赖包的 build 任务。

没有 ^dependsOn: ["build"] 意味着:先执行当前包自身的 build 任务(同包内的任务间依赖)。

dependsOn: [] 意味着无依赖,可以立即执行,与其他任务并行。

这套机制让 Turborepo 构建出一个有向无环图(DAG),据此进行拓扑排序和最大化并行执行。

Q5:Changesets 的 fixedlinked 有什么区别?

回答思路

fixed:绑定版本。组内所有包始终保持完全相同的版本号。只要组内任一包有变更,所有包都一起升版到同一版本。适用于紧密耦合的包组。

linked:关联版本。组内包可以有不同的版本号,但当组内某个包进行了 major/minor 升版时,组内其他有变更的包也会进行至少同级别的升版。适用于希望版本号有关联但不完全一致的包组。

举例:@myorg/react-core@myorg/react-dom 使用 fixed,保证版本号一致(类似 React 自身)。@myorg/plugin-a@myorg/plugin-b 使用 linked,版本可以不同但升版协调。

Q6:Monorepo 中如何处理不同包需要不同版本的同一个依赖?

回答思路

pnpm 天然支持同一个依赖的多个版本共存。不同于 npm 的扁平化会把某一版本提升到顶层,pnpm 的 .pnpm/ 目录下可以同时存在 react@17.0.2react@18.2.0,各个包的符号链接指向各自需要的版本。

但在实践中,应尽量统一版本。可以利用 pnpm 的 pnpm.overrides 强制统一:

json
{
  "pnpm": {
    "overrides": {
      "react": "^18.2.0",
      "react-dom": "^18.2.0"
    }
  }
}

或者使用 syncpack 等工具检查跨包的版本一致性。

Q7:大型 Monorepo 中 Git 操作变慢怎么办?

回答思路

几个常用的 Git 优化策略:

  1. Shallow Clonegit clone --depth=1,只拉取最近一次提交
  2. Sparse Checkout:只检出需要的目录,而非整个仓库
  3. Git LFS:大型二进制文件使用 Git LFS 存储
  4. git maintenance:定期运行 Git 维护命令优化仓库性能
  5. Partial Clonegit clone --filter=blob:none,按需下载文件内容

在 CI 中尤其重要——结合 fetch-depth: 2 和远程缓存,可以显著减少 CI 时间。

Q8:如何从 Multirepo 迁移到 Monorepo?

回答思路

渐进式迁移策略:

  1. 先搭建 Monorepo 骨架:pnpm workspace + Turborepo + 共享配置
  2. 从最核心的共享包开始迁入(如组件库、工具函数)
  3. 使用 git subtree 或工具保留 git 历史
  4. 逐步迁入应用层项目
  5. 统一 CI/CD 配置
  6. 建立 Changesets 发布流程

关键原则:每一步都保证可回滚,不要一次性迁移所有项目。优先迁移依赖关系紧密、跨仓库变更频繁的项目。


九、延伸阅读

用心学习,用代码说话 💻