Monorepo 构建工具链

Monorepo 构建工具链

写在前面

当公司项目从单个应用变成多个共享代码的应用(组件库、工具库、多个产品线),Monorepo 成了必然选择。但 Monorepo 的工程化挑战是:怎么高效地构建、测试、发布这么多包?

本篇会讲清楚 Monorepo 的本质,对比主流工具(Turborepo、Nx、pnpm workspace、Changesets),以及实战优化 6 个子包项目构建的全过程。

一、什么是 Monorepo

1.1 单仓 vs 多仓

# 多仓 (Polyrepo)
git:org/ui-library    ← 独立仓库
git:org/utils-lib     ← 独立仓库
git:org/web-app       ← 独立仓库
git:org/admin-app     ← 独立仓库

# 单仓 (Monorepo)
git:org/monorepo
├── packages/
│   ├── ui-library/
│   ├── utils-lib/
│   └── config/
├── apps/
│   ├── web-app/
│   └── admin-app/
└── pnpm-workspace.yaml

1.2 Monorepo 的优势

优势 说明
代码共享 直接 import,不需要发包
统一版本 一次升级,所有包升级
原子提交 一次 PR 改多个包
重构容易 跨包重构一气呵成
依赖管理 统一 node_modules,磁盘省一半

1.3 Monorepo 的挑战

挑战 说明
构建慢 改一行代码可能影响 100 个包
依赖管理 跨包依赖关系复杂
权限管理 怎么限制谁能改哪些包
CI 慢 跑一次测试可能 30 分钟
git 体积 大仓库 git clone 慢

构建工具链就是为了解决这些挑战。

二、pnpm workspace

2.1 pnpm 是什么

pnpm 是新一代 Node.js 包管理器,比 npm 快 2x,比 yarn 快 1.5x。

但 pnpm 的核心价值是内容寻址存储(content-addressable storage):

# 同样的 lodash 在多个项目里只存一份
~/.pnpm-store/
├── node_modules/.pnpm/lodash@4.17.21/

磁盘占用降低 50%+。

2.2 pnpm workspace 配置

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

2.3 项目结构

monorepo/
├── pnpm-workspace.yaml
├── package.json
├── pnpm-lock.yaml
├── apps/
│   ├── web-app/
│   │   └── package.json
│   └── admin-app/
│       └── package.json
└── packages/
    ├── ui-library/
    │   └── package.json
    ├── utils-lib/
    │   └── package.json
    └── config/
        └── package.json

2.4 workspace 依赖

// apps/web-app/package.json
{
    "name": "web-app",
    "dependencies": {
        "ui-library": "workspace:*",
        "utils-lib": "workspace:*",
        "react": "^18.2.0"
    }
}

workspace:* 表示引用本地包。

// packages/ui-library/package.json
{
    "name": "ui-library",
    "version": "1.0.0",
    "main": "./dist/index.js",
    "types": "./dist/index.d.ts",
    "exports": {
        ".": {
            "import": "./dist/index.mjs",
            "require": "./dist/index.js"
        }
    }
}

2.5 workspace 命令

# 在所有包里跑命令
pnpm -r run build

# 只在某个包里跑
pnpm --filter web-app run dev

# 在依赖某个包的所有包里跑
pnpm --filter "...ui-library" run build

# 跑所有包的 test
pnpm -r run test

三、Turborepo

3.1 是什么

Turborepo 是 Vercel 出品的 Monorepo 构建系统——核心是任务编排 + 远程缓存。

3.2 配置

// turbo.json
{
    "$schema": "https://turbo.build/schema.json",
    "globalDependencies": ["**/.env.*local"],
    "globalEnv": ["NODE_ENV"],
    "pipeline": {
        "build": {
            "dependsOn": ["^build"],
            "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
        },
        "test": {
            "dependsOn": ["^build"],
            "outputs": ["coverage/**"]
        },
        "lint": {},
        "dev": {
            "cache": false,
            "persistent": true
        },
        "clean": {
            "cache": false
        }
    }
}

3.3 关键概念

Pipeline(任务管道)

定义每个任务的依赖关系:

Outputs(产物声明)

声明任务的输出文件,这些文件会进缓存。

Inputs(输入声明)

{
    "build": {
        "dependsOn": ["^build"],
        "inputs": ["src/**", "package.json", "tsconfig.json"],
        "outputs": ["dist/**"]
    }
}

只考虑声明的文件——其他文件变化不触发重跑。

3.4 使用

# 跑所有 build
turbo run build

# 跑特定包
turbo run build --filter=web-app

# 跑依赖某个包的所有包
turbo run build --filter=...ui-library

# 并行度
turbo run build --concurrency=4

3.5 远程缓存

Turborepo 的杀手锏——远程缓存。

本地构建 → 哈希所有输入 → 检查远程缓存
  ├─ 命中:从远程下载产物
  └─ 未命中:本地构建,上传产物到远程

团队成员共享构建缓存:

配置远程缓存

# Vercel 远程缓存(免费)
npx turbo login
npx turbo link

# 自建远程缓存(S3 / NAS)
turbo.json
{
    "remoteCache": {
        "signature": true,
        "preflight": false,
        "uploadTimeout": 30000
    }
}

# 环境变量
TURBO_TOKEN=xxx
TURBO_TEAM=my-team

配合 CI 用 Vercel 远程缓存,免费。

四、Nx

4.1 是什么

Nx 是 Nrwl 出品的 Monorepo 构建系统——比 Turborepo 更强大但更复杂。

4.2 Nx 的优势

4.3 配置

// nx.json
{
    "npmScope": "myorg",
    "tasksRunnerOptions": {
        "default": {
            "runner": "nx/tasks-runners/default",
            "options": {
                "cacheableOperations": ["build", "test", "lint"],
                "parallel": 4
            }
        }
    },
    "targetDefaults": {
        "build": {
            "dependsOn": ["^build"],
            "outputs": ["{projectRoot}/dist"]
        }
    }
}

4.4 Affected commands

# 只跑受当前改动影响的包
nx affected --target=test

# 看哪些包受影响
nx affected:graph

核心价值:在大型 monorepo 里,改一行代码不需要跑全部包的测试。

4.5 Nx Cloud(付费)

类似 Vercel 的远程缓存,但功能更多:

个人项目免费,企业项目付费。

五、Turborepo vs Nx 对比

维度 Turborepo Nx
上手难度 ⭐(简单) ⭐⭐⭐(复杂)
配置 turbo.json nx.json + project.json
远程缓存 ✅(免费 + 付费) ✅(付费)
任务编排 ✅ ✅
Affected ✅(基础) ✅(智能)
代码生成器 ❌ ✅
插件生态 ❌ ✅
分布式执行 ❌ ✅
性能 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐

5.1 选型

六、实战:把 6 个子包构建时间砍半

6.1 背景

公司项目结构:

monorepo/
├── apps/
│   ├── web-app         (React + Vite)
│   ├── admin-app       (React + Rsbuild)
│   └── docs-site       (Astro)
├── packages/
│   ├── ui-library      (React 组件库)
│   ├── utils-lib       (工具函数)
│   └── eslint-config   (共享配置)

6.2 优化前

$ turbo run build
• ui-library:build         30s
• utils-lib:build          5s
• eslint-config:build      2s
• web-app:build            90s
• admin-app:build          60s
• docs-site:build          40s
─────────────────────────
Total:                     227s

6.3 第一步:依赖图梳理

web-app 依赖 ui-library、utils-lib、eslint-config。admin-app 同理。

Turborepo 自动分析:先构建 ui-library,再构建 web-app。

6.4 第二步:开启远程缓存

# 接入 Vercel 远程缓存(免费)
npx turbo login
npx turbo link

第一次构建:227s(建立缓存) 第二次构建:8s(全部命中缓存)

6.5 第三步:优化 inputs

// turbo.json
{
    "pipeline": {
        "build": {
            "dependsOn": ["^build"],
            "inputs": [
                "src/**",
                "package.json",
                "tsconfig.json",
                "vite.config.ts"
            ],
            "outputs": ["dist/**"]
        }
    }
}

明确 inputs 让缓存命中率更高。

6.6 第四步:拆分 build 和 test

{
    "pipeline": {
        "build": {
            "dependsOn": ["^build"],
            "inputs": [...],
            "outputs": ["dist/**"]
        },
        "test": {
            "dependsOn": ["build"],     // 测试依赖 build
            "outputs": ["coverage/**"]
        },
        "lint": {
            "outputs": []
        }
    }
}

这样 lint 不需要等 build——可以并行跑。

6.7 第五步:CI 集成

# .github/workflows/ci.yml
- name: Build
  run: turbo run build --concurrency=4

- name: Test
  run: turbo run test --concurrency=4

- name: Lint
  run: turbo run lint

CI 也享受远程缓存。

6.8 优化后

$ turbo run build  (本地,首次)
• ui-library:build         30s
• utils-lib:build          5s
• eslint-config:build      2s
• web-app:build            90s
• admin-app:build          60s
• docs-site:build          40s
─────────────────────────
Total:                     227s

$ turbo run build  (本地,二次,缓存命中)
Total:                     8s

$ turbo run build  (CI,远程缓存命中)
Total:                     15s

本地二次构建 30 倍加速,CI 15 倍加速。

七、依赖管理最佳实践

7.1 集中版本

所有包共享的依赖,在根 package.json 装一次:

// monorepo/package.json
{
    "devDependencies": {
        "typescript": "^5.3.0",
        "vitest": "^1.6.0",
        "eslint": "^8.57.0"
    }
}

7.2 共享配置

// packages/eslint-config/index.js
module.exports = {
    extends: ['eslint:recommended'],
    rules: {
        // 共享规则
    },
};
// apps/web-app/package.json
{
    "devDependencies": {
        "eslint-config-myorg": "workspace:*"
    }
}

7.3 Changesets(版本管理)

Changesets 是 Monorepo 版本管理工具:

# 添加 changeset
npx changeset

# 选哪些包需要升级
# - ui-library: minor
# - utils-lib: patch

# 提交后,CI 自动:
# 1. 升级版本
# 2. 生成 CHANGELOG
# 3. 发布到 npm
# .changeset/happy-dogs-sing.md
---
"ui-library": minor
"utils-lib": patch
---

新增 `<Button loading />` 属性

八、Monorepo 迁移指南

8.1 评估是否需要

适合 Monorepo:

不适合:

8.2 迁移步骤

# 1. 初始化
mkdir monorepo && cd monorepo
pnpm init

# 2. 配置 workspace
cat > pnpm-workspace.yaml << EOF
packages:
  - 'apps/*'
  - 'packages/*'
EOF

# 3. 把现有项目移入
mv ../web-app apps/
mv ../ui-library packages/

# 4. 重新安装依赖
pnpm install

# 5. 安装 Turborepo
pnpm add -D turbo

8.3 跨包重构

# 用 jscodeshift 改代码
pnpm exec jscodeshift -t transforms/rename-component.ts packages/*/src

# 用 Turborepo 跑跨包迁移
turbo run codemod --filter=...

九、本篇小结

工具 适用
pnpm 包管理器(必选)
Turborepo 任务编排 + 远程缓存(简单)
Nx 完整 Monorepo 平台(复杂)
Changesets 版本发布
关键点 说明
远程缓存 Turborepo 杀手锏
任务编排 dependsOn / inputs / outputs
影响范围 只跑受影响的包
版本管理 Changesets 自动化

下一篇讲 CI 环境下的构建优化——Docker 缓存、远程缓存、CI runner 调优。

参考资料