使用 Turborepo 管理你的 Monorepo 项目

5 min

1. 为什么选择 Monorepo?

Monorepo(单一代码仓库)是一种将多个独立项目或包(package)存放在同一个代码仓库中的策略。与每个项目拥有独立仓库的 Polyrepo 模式相比,Monorepo 具有一些显著优势:

  • 代码共享: 在不同项目间共享组件、工具函数或类型定义变得非常容易。
  • 原子化提交: 一个功能的修改如果涉及多个包,可以通过一次提交完成,保证了版本的一致性。
  • 简化的依赖管理: 所有项目共享同一个 node_modules(或通过 pnpm/yarn workspace 优化),减少了依赖冲突和版本不一致的问题。

然而,当仓库规模变大时,Monorepo 的挑战也随之而来:构建性能

2. Monorepo 的痛点

想象一下,你的仓库里有 docs, webapp, 和一个共享的 ui 组件库。当你只修改了 docs 中的一个错字时,你最不希望发生的事情就是整个仓库的所有项目(包括 webappui)都被重新构建和测试。

传统的 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 绝对是一个能为你和你的团队节省大量时间的利器。