Monorepo 迁移血泪史:从 Multi-Repo 到 Turborepo,这 3 个坑我帮你踩完了

15 阅读3分钟

问题场景

团队项目越来越多,公共组件库、工具函数库、业务项目分散在 N 个 Git 仓库。每改一个公共包,流程是这样的:

  1. 改 A 库代码 → commit → push → npm publish
  2. 切到 B 项目 → 升级依赖 → 发现不兼容
  3. 回 A 修 → 再发版 → 再切回来 → 循环 N 次

同事直呼:"改一行代码,切五个仓库,发三个版,我的生命在流逝。"

你决定迁移到 Monorepo,但网上教程看着简单,落地上线时全是暗坑。


原因分析 & 方案选型

多仓库的核心痛点就三个:

痛点表现
修改成本高跨仓库改代码需要 N 次发版
版本碎片不同项目依赖的公共包版本不一致
复现困难issue 复现需要在多个仓库间来回跳

Monorepo 工具选型:Turborepo(Vercel 出品)vs Nx vs Lerna。

对比项TurborepoNxLerna
学习曲线⭐ 低⭐⭐⭐ 高⭐ 低但功能弱
缓存能力内置+远程缓存内置+远程缓存
并行执行
任务编排零配置需配置手动
社区生态快速增长成熟逐渐边缘化

选 Turborepo,理由:Vite + pnpm + Turborepo 三件套,零配置任务编排、增量缓存、并行构建,小团队两周就能上手。


解决方案 & 实操步骤

Step 1: 目录结构设计(最容易被忽视)

my-monorepo/
├── apps/
│   ├── admin/          # 后台管理
│   ├── web/            # 前台 H5
│   └── docs/           # 文档站
├── packages/
│   ├── ui/             # 公共组件库
│   ├── utils/          # 工具函数
│   └── config/         # ESLint/TS 共享配置
├── pnpm-workspace.yaml
├── turbo.json
├── package.json
└── .npmrc

⚠️ 坑 1:packages 里面别放业务代码

有人把业务项目也丢在 packages 里,结果每次改业务代码都触发公共包的重建缓存失效。正确做法:apps 放业务项目,packages 放公共库,两者职责分离。

Step 2: pnpm workspace 配置

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

根目录 .npmrc

shamefully-hoist=true
strict-peer-dependencies=false

⚠️ 坑 2:shamefully-hoist 不配,tsconfig paths 全崩

默认 pnpm 严格隔离依赖,Vite + TypeScript 路径别名会找不到 node_modules。加上 shamefully-hoist=true 将依赖提升到根 node_modules,否则每个子包都要单独配 tsconfig paths。

Step 3: Turborepo 任务编排

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "dev": {
      "cache": false,
      "persistent": true
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "type-check": {
      "dependsOn": ["^type-check"]
    }
  }
}
// package.json (根目录)
{
  "scripts": {
    "dev": "turbo dev",
    "build": "turbo build",
    "lint": "turbo lint",
    "test": "turbo test"
  }
}

关键理解:dependsOn 中的 ^ 前缀

  • "^build" 表示:先构建该包的所有依赖,再构建它自己
  • 没有 ^:等所有前置任务的全部子包执行完
  • 不写 dependsOn:所有包并行执行

Turborepo 会自动拓扑排序:先构建 utils → 再构建依赖 utilsui → 最后构建依赖 uiweb

Step 4: 跨包引用

// apps/web/package.json
{
  "dependencies": {
    "@repo/ui": "workspace:*",
    "@repo/utils": "workspace:*"
  }
}
// apps/web/src/App.tsx
import { Button } from "@repo/ui";  // 直接引用,无需发版
import { formatDate } from "@repo/utils";

⚠️ 坑 3:workspace: 和 ^1.0.0 的差别*

workspace:* 在本地开发时直接链接到本地源码,publish 时会自动替换为实际版本号。 如果写成 "@repo/ui": "^1.0.0",pnpm 会去 registry 找包,不走工作空间。务必写 workspace:*

Step 5: 缓存配置提速

Turborepo 默认有本地缓存,第二次构建相同输入直接秒出:

# 第一次:正常构建,耗时 45s
turbo build

# 第二次(代码没变):瞬间完成,耗时 0.2s
turbo build

# 强制跳过缓存:排查问题时用
turbo build --force

远程缓存(配合 Vercel Remote Caching 或自建 S3):

turbo login
turbo link
# 配置后 CI 和本地共享缓存,CI 构建从 10min → 45s

要点总结

序号关键要点
1apps 放业务,packages 放公共库,职责分离防止缓存污染
2pnpm workspace 必须配 .npmrc + shamefully-hoist=true
3跨包依赖用 workspace:*,不要写版本号
4Turborepo 的 dependsOn 中的 ^ = 先构建依赖,理解它就能玩转任务编排
5缓存是核心竞争力,配好远程缓存后 CI 速度飞升 10x

迁移完成的第一天,改一行 @repo/utils 的代码,所有项目自动生效的那一刻——同事们看着终端输出的 0.1s,露出了满意的微笑。