工程化
Monorepo 构建缓存原理与配置
构建缓存的本质是:用任务的输入指纹(hash)作 key,把任务的输出(产物 + 终端日志)作 value 存起来。下次构建时指纹命中就直接复用,跳过执行。 配合任务依赖图(DAG),它能保证「上游变了下游一定重建、上游没变就吃缓存」。
一、核心原理
1. 输入指纹(Hash)怎么算
一个任务的 hash 由这些输入组合而成:
hash = SHA(源文件 + 配置 + env + 命令 + 依赖项的 hash)| 输入 | 说明 |
|---|---|
| 源代码 | 该 package 内参与构建的文件内容 |
| 环境变量 | 任务依赖的 env(如 NODE_ENV) |
| 命令 | build 脚本本身的内容 |
| 工具链版本 | Node、构建工具版本 |
| 依赖包的 hash | 关键:上游依赖变了,下游缓存必须失效 |
2. 缓存命中流程
3. 两个支撑机制
- 任务依赖图(Task Graph):根据
package.json依赖关系和 pipeline 配置构建 DAG,决定执行顺序和 hash 的传递。app依赖ui,则ui的 hash 会进入app的 hash 计算。 - 输入裁剪:只有声明的
inputs变化才让 hash 变化。改 README 不该使构建缓存失效,靠精确声明输入来实现。
二、本地缓存 vs 远程缓存
| 本地缓存 | 远程缓存 | |
|---|---|---|
| 存储位置 | node_modules/.cache 等磁盘目录 | 远端(Vercel、自建 S3) |
| 受益对象 | 单台机器、单人 | 整个团队 + CI |
| 典型场景 | 本地反复构建 | CI 复用同事 / 上次 CI 的产物 |
远程缓存是团队 / CI 场景提速的关键:A 构建过的 package,B 和 CI 可以直接拉产物。
三、配置(以 Turborepo 为例)
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json", "package.json"],
"outputs": ["dist/**"],
"env": ["NODE_ENV"]
},
"test": {
"dependsOn": ["build"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}| 字段 | 含义 |
|---|---|
dependsOn: ["^build"] | ^ 表示先构建所有上游依赖,声明 DAG 拓扑顺序,并让上游 hash 进入下游计算 |
inputs | 声明参与 hash 的文件,裁剪无关文件(README 改动不触发重建) |
outputs | 声明产物路径,命中时从缓存还原;漏声明会导致命中后产物缺失 |
env | 环境变量纳入 hash,避免 NODE_ENV 不同却复用错误产物 |
cache: false / persistent: true | dev、watch 这类长驻或无产物任务不缓存 |
远程缓存接入:
turbo login
turbo link # 关联远程缓存
turbo run build # CI 自动读写远程 cache四、易错点与对比
缓存失效的常见坑
outputs漏声明 → 命中后dist是空的。- 全局环境变量没纳入
globalDependencies→ 改了.env仍命中旧缓存。 - 锁文件(
pnpm-lock.yaml)变化应让依赖相关任务失效。
Turborepo vs Nx
两者原理一致(hash + task graph + remote cache)。Nx 额外有「受影响项目检测」(nx affected),基于 Git diff 只跑受影响的项目,与缓存配合更激进;Turborepo 更轻量、配置更简单。
为什么不只要缓存,还要任务依赖图
缓存解决「要不要重复执行」,task graph 解决「按什么顺序执行 + hash 如何传递」。两者结合才能保证上游变了下游一定重建,不会用到脏缓存。
一句话记忆:构建缓存 = 输入 hash 当 key、产物与日志当 value;任务依赖图让上游 hash 渗透到下游,从而「没变吃缓存、变了必重建」;远程缓存把这份复用从单人扩展到整个团队与 CI。