Turborepo 任务编排:缓存、并行与增量构建

本节目标

当包变多(10 个、50 个),你在根目录跑 pnpm build,会傻乎乎地把所有包都重新构建一遍,哪怕你只改了其中一个。Turborepo(简称 Turbo)就是来解决这个问题——它给每个任务建「依赖图」,只跑该跑的,并且把结果缓存下来。

先在根 package.json 装上 Turbo,并加几个「聚合脚本」:

// 根 package.json
{
  "scripts": {
    "build": "turbo run build",
    "test": "turbo run test",
    "lint": "turbo run lint"
  },
  "devDependencies": {
    "turbo": "^2.0.0"
  }
}

核心是 turbo.json——它描述「每个任务依赖什么、怎么算缓存」:

// turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],   // 先构建「依赖的包」(^ 表示上游)
      "outputs": ["dist/**"]      // 把 dist 目录作为缓存产物
    },
    "test": {
      "dependsOn": ["build"],     // 测试前先确保构建好
      "outputs": []               // 测试无产物,不缓存输出
    },
    "lint": {
      "outputs": []
    }
  }
}

dependsOn: ["^build"] 是关键:构建 web 时,Turbo 会先按拓扑把 web 依赖的 ui、types 都构建好(上游优先)。而 outputs: ["dist/**"] 让 Turbo 把 dist 哈希缓存——第二次构建若源码没变,直接命中缓存、秒过,不再重复编译。

# 全量构建(Turbo 自动并行 + 缓存)
pnpm build

# 只构建「相对于上次 commit 改动的包」及其下游
pnpm build --filter=@my/web...

# 只构建某个具体包
pnpm --filter @my/ui build

--filter=@my/web... 里的 ... 表示「web 以及所有依赖/被依赖它的包」,非常适合「我只改了 web,别的全跳过」的场景。结合远程缓存(把缓存上传到云端),团队里任何人第一次构建也能秒级复用别人的结果。

名词解释

Turborepo(Turbo):Monorepo 的任务编排器,按依赖图并行执行 build/test/lint,并用内容哈希做任务缓存,大幅缩短 CI 与本地构建时间。

Pipeline(任务管线):turbo.json 里定义的「任务清单 + 依赖关系」,告诉 Turbo 哪个任务依赖哪个、产物是什么。

任务缓存(Task Cache):Turbo 对「输入(源码+依赖)+ 输出(如 dist)」做哈希,命中则跳过执行直接复用结果,是「二次构建秒过」的原理。

--filter 与 ... 语法:Turbo/pnpm 的包选择器;--filter=@my/web... 选中 web 及其上下游相关包,实现「只动相关的」。

课后练习

练习 1:dependsOn: ["^build"] 里的 ^ 是什么意思?不加会怎样?

答案:^ 表示「上游依赖包」。web 的 build 会先确保它依赖的 ui/types 已构建。不加的话,可能 web 开始构建时 ui 还没好,导致找不到最新的 ui 产物而构建失败或用了旧版本。

练习 2:为什么 lint 的 outputs 通常是空数组?

答案:lint 不产出可复用的文件产物(只是检查报错误),没有「输出物」需要缓存,所以 outputs: [];它的「输入」仍是源码,inputs 变化时才会重跑。

总结

Turbo 解决的是 Monorepo 的「规模化」问题:用 turbo.json 描述任务依赖图,靠哈希缓存让重复构建秒过,靠 --filter 让改动只触动相关包。没有它,包一多构建就寸步难行;有了它,10 个包和 100 个包的体验差不了多少。记住 dependsOn: ["^build"] 是正确性的底线。最后一节我们谈「多包怎么发版」与那些绕不开的工程避坑。