使用 Turborepo 管理你的 Monorepo 项目
1. 为什么选择 Monorepo?
Monorepo(单一代码仓库)是一种将多个独立项目或包(package)存放在同一个代码仓库中的策略。与每个项目拥有独立仓库的 Polyrepo 模式相比,Monorepo 具有一些显著优势:
- 代码共享: 在不同项目间共享组件、工具函数或类型定义变得非常容易。
- 原子化提交: 一个功能的修改如果涉及多个包,可以通过一次提交完成,保证了版本的一致性。
- 简化的依赖管理: 所有项目共享同一个
node_modules(或通过 pnpm/yarn workspace 优化),减少了依赖冲突和版本不一致的问题。
然而,当仓库规模变大时,Monorepo 的挑战也随之而来:构建性能。
2. Monorepo 的痛点
想象一下,你的仓库里有 docs, webapp, 和一个共享的 ui 组件库。当你只修改了 docs 中的一个错字时,你最不希望发生的事情就是整个仓库的所有项目(包括 webapp 和 ui)都被重新构建和测试。
传统的 Monorepo 管理工具(如 Lerna)在任务编排和构建缓存方面存在不足,导致:
- 重复工作: 每次 CI/CD 运行时,都会从头构建和测试所有内容,即使大部分代码没有变化。
- 构建时间长: 无法有效利用多核 CPU 并行执行任务。
- 复杂的脚本: 需要编写复杂的
package.json脚本来手动控制任务的执行顺序。
3. Turborepo:为速度而生
Turborepo 是一个专为 JavaScript/TypeScript Monorepo 设计的高性能构建系统(后被 Vercel 收购)。它通过两大核心技术解决了上述痛点:增量构建 和 远程缓存。
a. 增量构建与任务流水线 (Pipelines)
Turborepo 让你在根目录的 turbo.json 文件中定义任务之间的依赖关系。
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
},
"dev": {
"cache": false
}
}
}dependsOn: ["^build"]:^符号表示,一个包的build任务依赖于它所依赖的所有包的build任务。Turborepo 会根据这个关系图,以最高效的并行方式执行任务。outputs: 告诉 Turborepo 这个任务会产生哪些输出文件。
当你运行 pnpm turbo build 时,Turborepo 会计算出哪些文件被修改了,并且只重新构建那些受影响的包。如果一个包的源码和依赖没有变化,Turborepo 会直接使用上次的构建产物,这个过程是瞬时的。
b. 远程缓存 (Remote Caching)
这是 Turborepo 的“杀手级”特性。它不仅在你的本地机器上缓存构建结果,还能将这些缓存上传到一个共享的远程服务器(如 Vercel 或你自己的 S3 存储桶)。
这意味着:
- 你的同事 在拉取最新代码后,如果他要构建的部分你已经构建过,他可以直接下载缓存,无需在本地重新构建。
- CI/CD 服务器 也能连接到这个远程缓存。一个 PR 在 CI 中被构建和缓存后,其他开发者或后续的 CI 任务都能立刻享受到这个成果。
这能将整个团队的构建时间从几十分钟缩短到几分钟甚至几十秒。
结论
Turborepo 并没有发明 Monorepo,但它通过引入先进的缓存和任务调度系统,极大地优化了 Monorepo 的开发体验,解决了其最核心的性能瓶颈。如果你正在使用或准备采用 Monorepo 架构,Turborepo 绝对是一个能为你和你的团队节省大量时间的利器。