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

问题场景

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

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

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

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


原因分析 & 方案选型

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

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

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

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

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


解决方案 & 实操步骤

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

perl 复制代码
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 配置

yaml 复制代码
# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

根目录 .npmrc

ini 复制代码
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 任务编排

json 复制代码
// 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"]
    }
  }
}
jsonc 复制代码
// package.json (根目录)
{
  "scripts": {
    "dev": "turbo dev",
    "build": "turbo build",
    "lint": "turbo lint",
    "test": "turbo test"
  }
}

关键理解:dependsOn 中的 ^ 前缀

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

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

Step 4: 跨包引用

jsonc 复制代码
// apps/web/package.json
{
  "dependencies": {
    "@repo/ui": "workspace:*",
    "@repo/utils": "workspace:*"
  }
}
tsx 复制代码
// 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 默认有本地缓存,第二次构建相同输入直接秒出:

bash 复制代码
# 第一次:正常构建,耗时 45s
turbo build

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

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

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

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

要点总结

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

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

相关推荐
星栈2 小时前
Dioxus 多页面怎么做:`dioxus-router`、嵌套路由、`Outlet` 和页面组织,一篇给你讲顺
前端·rust·前端框架
用户987409238872 小时前
用 Remotion + edge-tts 打造中文教学视频全自动流水线
前端
风骏时光牛马2 小时前
Less前端工程化实战:变量混合器与项目样式分层落地
前端
假如让我当三天老蒯2 小时前
Options API(选项式 API) 和 Composition API(组合式 API)
前端·vue.js·面试
SameX2 小时前
iOS 独立开发实践:用 MapKit + 像素渲染实现 Citywalk 轨迹地图 App「雁过留痕」
前端
skyey2 小时前
页面加载时,深色模式闪白的问题解决
前端
IT_陈寒2 小时前
Java 并行流把我坑惨了,这6小时加班值了
前端·人工智能·后端
anOnion12 小时前
构建无障碍组件之Menu Button pattern
前端·html·交互设计
用户479492835691512 小时前
claude Fable用不了?把Gpt 5.5pro接到你的claude code里
前端·后端