各位前端工程化爱好者、Monorepo 倡导者、以及那些深夜还在等 npm ci 跑完的“代码苦力”们,大家晚上好!
我是你们的老朋友,一个在 Web 技术栈里摸爬滚打多年的老兵。
今天我们不聊 API,不聊 Redux 的中间件设计模式,也不聊 CSS Grid 的那些奇技淫巧。我们聊聊点更“硬核”、更“底层”、更能拯救你发际线的东西——构建流。
你们有没有过这种经历?早上 9 点,你提交了代码。然后你打开电脑,打开终端,输入 npm ci,然后你就去喝咖啡,回来的时候咖啡凉了,咖啡机冒烟了,你的终端还在转圈圈,显示着类似这样的信息:
# 🚧 正在安装依赖... (已经跑了 40 分钟了)
# 🚧 正在编译 utils-core...
# 🚧 正在编译 shared-components...
# 🚧 正在编译 main-app...
# 🚧 正在编译 backend-api...
# 🚧 正在运行 lint...
# 🚧 正在运行 test...
# 💀 构建失败。你在错误日志中看到的第 403 行代码,你两年前就写完了。
那种感觉,就像是你去吃自助餐,结果端上来一盘生面条,告诉你这叫“匠心手作”。
这就是传统的单体构建流在 Monorepo 面前的原罪。它像是一个强迫症晚期的老奶奶织毛衣,一针一线,必须先织完胳膊才能织身子,中间还时不时停下来检查一下有没有断线。
今天,我要给你们介绍一位“工业革命”级别的救星——Turborepo。它不仅能并行构建,还能把你的构建结果缓存到外太空(远程缓存),让你在第二天提交代码时,构建速度比眨眼还快。
准备好你们的 CPU 和 GPU,我们开始吧!
第一部分:Monorepo 的崛起与构建的“哀嚎”
首先,我们要搞清楚为什么我们需要 Turborepo。
在过去的几年里,大公司(比如 Meta、Google、Vercel)发现了一个规律:把一堆相关的代码放在一个 Git 仓库里(Monorepo),比把它们拆成 50 个微服务要香得多。为什么?因为改一个 UI 组件,往往要顺手把相关的逻辑和测试都改了,放在一个 repo 里,这叫“顺手”。
但是,天下没有免费的午餐。当你在一个仓库里放了一个 React 应用、一个 Node.js 后端、一堆共享的 TS 工具库,还有一个 Next.js 服务端渲染页面时,你的 package.json 就会变成一个巨大的、令人窒息的怪物。
传统工具(比如 lerna 或简单的 npm)怎么处理构建?它们通常只能“串行”。它们不知道 app-a 依赖 shared-ui,它们只知道你这么写了,但它执行起来就像个无脑的流水线工人。
这就导致了 CPU 的巨大浪费。你的 CPU 有 16 个核心,每个核心闲得都在玩俄罗斯方块,而构建脚本却只占用了 1 个核心在苦哈哈地编译。
Turborepo 的核心哲学只有一个: 别傻乎乎地等,先把能跑的都跑起来!
第二部分:并行构建的艺术 —— “一锅乱炖”变“米其林盛宴”
想象一下,你在家里做饭。如果你先把米煮上(构建基础依赖),然后切菜,再炒菜,再烧汤,最后吃饭。这是串行,慢。
如果你把电饭煲插上(启动构建),然后同时切菜(并行构建 utils),同时点火炒肉(并行构建 components),同时烧水(并行运行测试)。这就是并行。虽然锅还是那个锅,但出一顿热饭的速度翻倍了。
Turborepo 做的就是这个事。它读取你的 package.json,分析出依赖关系,生成一张“依赖图”。然后,它像一个精明的调度员,把没有依赖关系的任务扔进不同的线程里跑。
代码示例:构建配置
假设你有这样一个结构:
apps/
web/ (React)
api/ (Node.js)
packages/
ui/ (React Components)
utils/ (Pure JS/TS)
在 apps/web/package.json 里,你会看到:
{
"scripts": {
"build": "tsc && vite build"
}
}
在 packages/utils/package.json 里:
{
"scripts": {
"build": "tsc"
}
}
以前,你会这样跑构建:
# 像个老奶奶一样,慢吞吞的
npm run build --filter=utils
npm run build --filter=ui
npm run build --filter=web
现在,打开你的上帝终端:
npx turbo run build
发生了什么?
- Turborepo 看到了
web依赖ui和utils。 - 它同时启动
utils的构建和ui的构建。恭喜你,这两个任务并行了! - 当
utils和ui构建完成后,Turborepo 才会启动web的构建。
如果你的硬件够强,你会看到 CPU 利用率飙升,风扇开始狂转,你会感到一种原始的快感。这就是管道(Pipeline)的力量。
Turborepo 的配置:turbo.json
为了控制这个调度员的行为,我们需要一份说明书,也就是 turbo.json。
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"], // 这里的 ^ 表示前置依赖,先构建依赖库
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {
"dependsOn": ["^lint"],
"outputs": [] // Lint 不需要输出文件,不需要缓存
},
"test": {
"dependsOn": ["^build", "^test"], // 测试通常依赖构建完成
"outputs": ["coverage/**"]
}
}
}
dependsOn: 告诉 Turbo 谁得先干活。outputs: 告诉 Turbo 哪些文件是构建的产物。如果文件没变,Turbo 就会跳过这个任务,直接说“Done”,就像你下班回家发现老婆做的晚饭没动过,你不用吃了,直接去睡觉。这就是增量构建。
第三部分:远程缓存 —— 你的私人构建传送门
这是 Turborepo 最疯狂、最厉害的功能。你可以把它想象成一个全球共享的构建快照服务器。
通常,构建是很慢的,因为编译 TypeScript、打包 JS、运行测试都需要时间。但是,一旦你构建成功了一次,这个结果就被打包进了一个缓存里。
现在,你的同事小王提交了代码。他去跑构建:
npx turbo run build --global
如果他本地的 ui 包没变,Turborepo 会看一眼缓存:“哦,这包我两年前构建过,直接把那个结果复用一下!”
如果你把你的缓存连接到远程(比如 Vercel 的远程缓存,或者你自己搭建的 S3 + Redis),那么当你提交代码时,CI 系统(GitHub Actions、GitLab CI)会去远程缓存里找这个包。
如果缓存里没有?好吧,那只能硬着头皮重新构建了。
如果缓存里有?恭喜,你的构建时间从 3 分钟变成了 30 秒。
这就是所谓的“远程缓存优化”。
代码示例:配置远程缓存
在 turbo.json 中,你可以开启 global 模式,并将缓存指向远程。
{
"globalDependencies": [".env.*local"], // 即使是本地环境变量,也要视为全局依赖
"globalEnv": ["NODE_ENV", "TZ"],
"pipeline": { /* ... */ },
"remoteCache": {
"signature": {
"type": "content-hash"
},
"enabled": true
}
}
在 CI 环境中,你通常会配置环境变量 TURBO_TOKEN 和 TURBO_TEAM(如果是 Vercel)或者配置 AWS S3 作为存储。
# GitHub Actions 示例
- name: Setup Turbo
run: npx turbo login
- name: Build with Turbo
run: npx turbo run build --global
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: my-awesome-team
想象一下,你的测试环境之前构建失败了,你修复了 bug。当你的 PR 合并后,CI 拉取代码,发现所有依赖都有缓存,它只需要编译你改动的那个包,然后串接起来。这比喝口水还快。
第四部分:全栈 React 生态的深度融合
Turborepo 并不是只玩 Node.js 的。它是专门为 React 生态设计的。为什么?因为 React 生态最头疼的问题就是“依赖地狱”。
1. Next.js 的最佳拍档
Next.js 的构建过程是出了名的复杂。它既要编译页面,又要优化图片,还要处理 SSG/SSR。
如果你在 Turborepo 里跑 Next.js 项目,你需要告诉 Turbo 不要把 .next/cache 当作缓存输出(因为它太大了,而且经常出错),只缓存 .next/BUILD_ID。
{
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"]
}
}
而且,Turborepo 可以帮你把 Next.js 的构建速度提升到极致。它会并行编译不同的路由,只有当所有页面都编译完,它才会生成最终的生产包。
2. TypeScript 的并发检查
TypeScript 的类型检查是串行的,这也是为什么 tsc --noEmit 有时候会卡死你的编辑器。
Turborepo 允许你并行运行类型检查!你可以配置一个任务:
{
"typecheck": {
"dependsOn": ["^typecheck"], // 依赖库先检查
"cache": false // 类型检查结果不缓存,因为文件变了类型可能就变了
}
}
然后在 package.json 里:
{
"scripts": {
"build": "turbo run build",
"check": "turbo run typecheck"
}
}
3. React Testing Library 的并行测试
测试是慢的。在大型 Monorepo 里,几百个测试跑下来,你可能得等到天荒地老。
Turborepo 可以并行执行测试!这意味着你可以在 4 个 CPU 核心上同时跑 4 个测试文件。
{
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"],
"cache": true
}
}
虽然 Turbo 不直接帮你的测试断言提速,但它通过并行运行文件,大幅缩短了总耗时。
第五部分:实战演练 —— 从零搭建 React Turborepo
光说不练假把式。我们来搭建一个最简化的 React 全栈项目。
1. 初始化
首先,你要有个地方安身立命。
npx create-turbo@latest my-react-monorepo
# 或者,如果你已经有一个 repo 了,直接初始化:
npx turbo init
2. 目录结构
我们的目标结构长这样:
my-react-monorepo/
├── apps/
│ ├── web/ # Next.js React 应用
│ │ ├── src/
│ │ ├── package.json
│ │ └── turbo.json
│ └── api/ # Express 或 Next.js API 路由
│ ├── src/
│ ├── package.json
│ └── turbo.json
├── packages/
│ ├── ui/ # 共享 React 组件库
│ │ ├── src/
│ │ ├── package.json
│ │ └── turbo.json
│ └── utils/ # 共享工具函数
│ ├── src/
│ ├── package.json
│ └── turbo.json
├── turbo.json # 全局配置
└── package.json
3. 关键代码:全局配置
我们需要一个强有力的全局配置。
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [".env.*local"],
"globalEnv": ["NODE_ENV", "NEXT_PUBLIC_*", "VITE_*"], // 自动注入环境变量
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {
"dependsOn": ["^lint"],
"outputs": []
},
"dev": {
"cache": false,
"persistent": true // 开发模式要持久运行,不能缓存
},
"clean": {
"cache": false
}
}
}
4. 依赖管理
在 Turborepo 里,最头疼的问题之一是依赖版本冲突。ui 包依赖 react@18,web 包也依赖 react@18,版本号一模一样还好。
但如果 web 想升级到 react@19,而 ui 还卡在 react@18 怎么办?
Turborepo 配合 pnpm(强烈推荐)或者 npm 的 Workspaces 可以完美解决这个问题。
在 apps/web/package.json 里:
{
"name": "web",
"private": true,
"scripts": {
"build": "next build"
},
"dependencies": {
"next": "14.2.5",
"react": "18.3.1",
"react-dom": "18.3.1",
"ui": "*", // 关键点:使用 * 号,让它去根目录找,或者引用 packages/ui
"utils": "*"
}
}
在 packages/ui/package.json 里:
{
"name": "ui",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "tsc"
},
"dependencies": {
"react": "18.3.1" // 锁定版本
}
}
Turborepo 会在构建时,确保 ui 使用它自己的 react 版本,而 web 使用自己的版本。它们互不干扰。
第六部分:诊断与优化 —— 当构建挂掉时
再好的工具也不是完美的。有时候,构建失败了,或者没有并行起来。
1. 查看依赖图
如果你不知道 Turbo 为什么先构建 A 再构建 B,你可以用 --graph 选项。
npx turbo run build --graph
这会生成一个 DOT 格式的图,你可以把它丢到 https://dreampuf.github.io/GraphvizOnline/ 里看看。
2. 调试缓存问题
有时候 Turbo 不会重新构建,因为它的缓存记录说“这次没变”。但实际上你改了代码。
你可以强制清除缓存:
npx turbo run build --force
或者,只清除特定包的缓存:
npx turbo run build --filter=utils
3. 性能瓶颈分析
Turborepo 有一个内置的 --summary 选项,可以让你看到每个包的构建时间。
npx turbo run build --summary
如果发现某个包耗时 1 分钟,而它其实只改了几行代码,那可能是它的构建脚本本身有问题(比如它里面调用了极其低效的编译器选项)。
第七部分:进阶话题 —— 管道与确定性
Turborepo 的强大不仅在于并行,还在于管道。这其实是借鉴了构建工具界的大佬 Buck 的理念。
在 turbo.json 里,你可以定义管道链。
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"cache": true
},
"test": {
"dependsOn": ["build"], // 必须先构建才能测试
"outputs": ["coverage/**"]
},
"deploy": {
"dependsOn": ["test", "build"], // 必须测试通过并构建完成才能部署
"outputs": []
}
}
}
这就是流控制。你不能跳步。你必须老老实实地按照 build -> test -> deploy 的顺序来。这保证了代码质量。
确定性
构建必须是“确定性”的。同样的输入(代码 + 依赖版本 + 环境变量),必须产生同样的输出(编译产物)。Turborepo 通过内容哈希(Content Hash)来保证这一点。如果输入没变,哈希就不变,直接复用旧文件。这就是缓存的数学基础。
第八部分:全栈的闭环 —— 从 UI 到 API
让我们来看看全栈是怎么协同工作的。
假设你的 apps/api 是一个 Node.js 服务(用 Express 或 NestJS)。
apps/web 是个 Next.js 客户端。
packages/ui 是共享的组件。
在 Next.js 中,你可能想直接引用 packages/ui 的组件,或者引用 API 的类型。
// apps/web/src/pages/index.tsx
import { Button } from "ui"; // 引用 UI 包
import { UserResponse } from "api"; // 引用 API 包的类型
export default function Home({ data }: { data: UserResponse }) {
return (
<div>
<Button>Click me</Button>
<p>User: {data.name}</p>
</div>
);
}
当你运行 npx turbo run build 时,Turborepo 会:
- 构建
packages/ui。 - 构建
packages/api(如果 API 依赖了ui)。 - 构建
apps/web。
如果在 Web 中,你在构建时(SSG)调用了 API,那 Turborepo 会确保 API 包先构建完成,Web 包再开始构建。没有硬编码的脚本顺序,全靠依赖图自动解析。
第九部分:总结与展望
好了,同学们,今天的讲座就要接近尾声了。
我们回顾一下今天的重点:
- 痛苦根源:传统的单体构建流在 Monorepo 面前不堪一击,串行、重复劳动是性能的杀手。
- Turborepo 之道:通过并行化利用 CPU 多核能力,通过依赖图智能调度任务。
- 远程缓存:通过内容哈希和远程存储,实现跨机器、跨时间的构建结果复用。
- React 全栈集成:完美支持 Next.js、React Testing Library、TypeScript,让构建流与开发流无缝对接。
现在,当你手握 Turborepo 这个神器,面对成千上万行代码的 Monorepo,你不再是那个坐在终端前等咖啡凉掉的可怜虫。你是一个掌控全局的指挥官,你的构建流如同瀑布般倾泻而下,效率惊人。
不要再用 npm ci 这种石器时代的命令了。 拿起 npx turbo run build,让 Turborepo 带你飞!
记住,代码是用来构建产品的,不是用来等待构建的。去优化你的构建流吧,去拥抱并行,去拥抱缓存。你的发际线会感谢你的。
(此处应有掌声,或者更有节奏的键盘敲击声)
谢谢大家!