Rush Monorepo 构建缓存指南

前言

作为一个使用 Rush 管理 Monorepo 的开发者,你是否也遇到过以下痛点:

  • 切换 Git 分支后,必须重新构建所有项目
  • 新同事 clone 项目后,第一次构建要等很久
  • rush build 虽然支持增量构建,但一旦 clean 后状态全丢
  • 团队成员之间无法共享构建产物,每个人都在重复造轮子

如果你正被这些问题困扰,那么恭喜你,Rush 构建缓存(Build Cache) 正是你的解决方案!

本文将基于我自己的 Rush 项目实战经验,从零开始带你一步步开启构建缓存,并对比 local-only 本地磁盘缓存http 远程缓存 两种模式的差异,让你的构建速度提升。


一、先看看我的项目长什么样

在讲缓存之前,先简单介绍一下我的 Rush Monorepo 结构,方便你对号入座:

bash 复制代码
rush-cache/
├── common/
│   └── config/rush/
│       ├── build-cache.json        # 构建缓存配置(本文重点)
│       └── rush.json               # Rush 主配置
├── my-app/                         # Vue 主应用
│   ├── config/rush-project.json
│   └── src/App.vue                 # 实际运行效果见下文
├── components/                     # Vue 组件库(被 my-app 引用)
│   ├── config/rush-project.json
│   └── src/components/HelloWorld.vue
├── tools/                          # 工具函数库(被 my-app 引用)
│   ├── config/rush-project.json
│   └── src/index.js
└── server/                         # HTTP 缓存服务(http 模式专用)
    ├── index.js
    └── rush-build-cache/           # 缓存文件存储目录

Workspace 依赖关系

通过 Rush 的 workspace 协议,my-app 直接引用本地包:

json 复制代码
// my-app/package.json
{
  "name": "my-app",
  "dependencies": {
    "vue": "^3.5.13",
    "my-components": "workspace:*",
    "my-tools": "workspace:*"
  }
}

App.vue 的实际效果

来看 my-app 中的实际调用代码:

vue 复制代码
<!-- my-app/src/App.vue -->
<script setup>
import { HelloWorld } from 'my-components'
import { isNumber } from 'my-tools'

console.log('isNumber', isNumber(123));
console.log('isNumber', isNumber('123'));
</script>

<template>
  <HelloWorld msg="my-app" />
</template>

被引用的两个包的实现:

vue 复制代码
<!-- components/src/components/HelloWorld.vue -->
<script setup>
  defineProps({ msg: String })
</script>

<template>
  <h1>这是 my-components 组件:{{ msg }}</h1>
</template>
javascript 复制代码
// tools/src/index.js
const toString = (obj) => Object.prototype.toString.call(obj).replace(/\[object (\S+)\]/, '$1')

export const isNumber = (obj) => toString(obj) === 'Number'
export const isObject = (obj) => toString(obj) === 'Object'
export const isArray = (obj) => toString(obj) === 'Array'
// ... 其他工具函数

运行效果:

控制台输出:

scss 复制代码
isNumber true     <- isNumber(123) 正确判断数字
isNumber false    <- isNumber('123') 正确排除字符串

页面显示:

perl 复制代码
这是 my-components 组件:my-app

三个包通过 Rush workspace 完美联动,这也是接下来验证构建缓存是否生效的基础 ------因为缓存命中后,从 tar 还原的 dist 目录必须能正确支持 workspace:* 的引用。


二、先理解:什么是 Rush 构建缓存?

2.1 增量构建 vs 构建缓存

在 Rush 中,有两种「加速构建」的机制:

机制 原理 存储位置 切换分支后 团队共享
增量构建 对比文件哈希,跳过未变项目 项目文件夹内的临时文件 失效 不支持
构建缓存 将构建产物打包成 tar,按哈希缓存 本地磁盘 / HTTP 服务 / 云端 可用 支持(http/云端模式)

简单来说,增量构建是「跳过」 ,而构建缓存是「还原」。构建缓存不仅能在同一台机器的不同分支间共享,http / 云端模式甚至可以让整个团队共享同一份缓存。

2.2 构建缓存的存储位置(四种 cacheProvider)

Rush 的 build-cache.json 中通过 cacheProvider 字段选择缓存后端:

cacheProvider 存储位置 适用场景 是否需要额外基础设施
local-only 本地磁盘 common/temp/build-cache 个人开发、初次体验
http 自建 HTTP 服务(本项目 server/index.js 团队共享、本地开发服务器 是(一个 Node 进程)
azure-blob-storage Azure Blob Storage 企业级云端共享 是(Azure 账号)
amazon-s3 AWS S3 企业级云端共享 是(AWS 账号)

本文将聚焦于前两种:local-onlyhttp,因为它们都不依赖云厂商,是中小团队最容易上手的方案。

两种模式的核心配置思路完全一致,只是缓存后端不同,可随时切换。


三、实战一:开启 local-only 本地磁盘缓存

这是最简单的模式,适合先跑通流程,验证缓存是否生效。

Step 1:创建 build-cache.json 全局配置

common/config/rush/ 下创建(或修改)build-cache.json

json 复制代码
{
  "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/build-cache.schema.json",

  /**
   * 核心配置 1:开启构建缓存
   */
  "buildCacheEnabled": true,

  /**
   * 核心配置 2:缓存提供者
   * "local-only": 只使用本地磁盘缓存,零基础设施成本
   */
  "cacheProvider": "local-only"
}

Tips :早期版本需要在 experiments.json 中设置 "buildCache": true,新版本已经统一用 build-cache.json"buildCacheEnabled"

Step 2:为每个项目配置 rush-project.json

光开启全局缓存还不够,Rush 还需要知道哪些目录是构建产物(需要缓存还原的目录)。三个子包的配置完全一致:

json 复制代码
// tools/config/rush-project.json
// components/config/rush-project.json
// my-app/config/rush-project.json
{
  "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-project.schema.json",
  "operationSettings": [
    {
      "operationName": "build",
      "outputFolderNames": ["dist"]
    }
  ]
}

重要outputFolderNames 中的目录必须满足:

  1. 不被 Git 跟踪(在 .gitignore 中已忽略)
  2. 不能包含符号链接
  3. 是构建脚本真正的输出目录

Step 3:第一次构建写入缓存

bash 复制代码
# 先安装依赖
rush install

# 执行重建并观察日志
rush rebuild --verbose

第一次构建(写入缓存)的日志示例:

erlang 复制代码
==[ my-tools ]==============================================[ 1 of 3 ]==

This project was not found in the build cache.
Invoking: unbuild
...
Caching build output folders: dist
Successfully set cache entry.
"my-tools" completed successfully in 3.12 seconds.

==[ my-components ]==============================================[ 2 of 3 ]==

This project was not found in the build cache.
Invoking: vite build
...
Caching build output folders: dist
Successfully set cache entry.
"my-components" completed successfully in 5.47 seconds.

==[ my-app ]==============================================[ 3 of 3 ]==

This project was not found in the build cache.
Invoking: vite build
...
Caching build output folders: dist
Successfully set cache entry.
"my-app" completed successfully in 8.23 seconds.

此时可以在 common/temp/build-cache/ 下看到生成的 3 个 .tar.gz 缓存文件。

Step 4:验证缓存还原

bash 复制代码
# 删除所有构建产物和临时文件(模拟:clean 后、切换分支后)
rush purge

# 再次构建 - 验证缓存还原
rush rebuild --verbose

缓存命中的日志:

erlang 复制代码
==[ my-tools ]==============================================[ 1 of 3 ]==

Build cache hit.
Clearing cached folders: dist
Successfully restored output from the build cache.
my-tools was restored from the build cache.

==[ my-components ]==============================================[ 2 of 3 ]==

Build cache hit.
Clearing cached folders: dist
Successfully restored output from the build cache.
my-components was restored from the build cache.

==[ my-app ]==============================================[ 3 of 3 ]==

Build cache hit.
Clearing cached folders: dist
Successfully restored output from the build cache.
my-app was restored from the build cache.

原本需要十几秒的构建,现在瞬间完成!

local-only 模式小结

bash 复制代码
缓存写入:rush build  →  common/temp/build-cache/[hash].tar.gz
缓存读取:rush build  →  解压 [hash].tar.gz  →  项目 dist/

优点 :零配置、零基础设施、开箱即用。 缺点:仅当前机器可用,无法团队共享。

接下来我们升级到 http 模式,把缓存集中存储到团队共享的 HTTP 服务上。


四、实战二:升级到 http 远程缓存

http 模式的核心思想是:把缓存写入到一个独立的 HTTP 服务,所有开发者都从同一个服务读写缓存,实现团队级共享。

4.1 准备 HTTP 缓存服务

本项目在 server/index.js 提供了一个最小化的实现(基于 Node 原生 http 模块,无任何依赖):

javascript 复制代码
// server/index.js (核心逻辑摘录)
const http = require('http');
const fs = require('fs');
const path = require('path');

const PORT = process.env.PORT || 3000;
const CACHE_DIR = path.join(__dirname, 'rush-build-cache');

if (!fs.existsSync(CACHE_DIR)) {
  fs.mkdirSync(CACHE_DIR, { recursive: true });
}

const server = http.createServer((req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);
  const cacheKey = url.pathname.replace(/^\//, '');
  const cacheFilePath = path.join(CACHE_DIR, cacheKey);

  switch (req.method) {
    case 'GET':  // 读取缓存
      // ...返回缓存文件或 404
    case 'PUT':  // 写入缓存
      // ...将请求体写入文件
    case 'DELETE':  // 删除缓存
      // ...
  }
});

server.listen(PORT, () => {
  console.log(`Rush build cache server running at http://localhost:${PORT}/`);
  console.log(`Cache directory: ${CACHE_DIR}`);
});

完整代码见 server/index.js。它实现了 Rush HTTP 缓存协议要求的四个操作:

HTTP 方法 用途 Rush 调用时机
GET 读取缓存 构建前查询缓存是否存在
PUT / POST / PATCH 写入缓存 构建完成后上传产物
DELETE 删除缓存 手动清理或缓存淘汰
OPTIONS CORS 预检 跨域场景

启动服务:

bash 复制代码
node server/index.js
# 输出:
# Rush build cache server running at http://localhost:3000/
# Cache directory: .../server/rush-build-cache

生产环境建议:用 nginx 反向代理 + 持久化存储 + 鉴权,或直接换成 S3 / Azure Blob。

4.2 修改 build-cache.json 切换到 http 模式

json 复制代码
{
  "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/build-cache.schema.json",
  "buildCacheEnabled": true,

  /**
   * 切换为 http 模式
   */
  "cacheProvider": "http",

  /**
   * http 模式专用配置
   */
  "httpConfiguration": {
    /**
     * (Required) 缓存服务地址
     */
    "url": "http://localhost:3000/",

    /**
     * (Optional) 写入缓存的 HTTP 方法,默认 PUT
     */
    // "uploadMethod": "PUT",

    /**
     * (Optional) 自定义请求头,可用于鉴权
     */
    // "headers": { "X-HTTP-Company-Id": "109283" },

    /**
     * (Optional) 鉴权 token 命令,输出会作为 Authorization 头
     */
    // "tokenHandler": { "exec": "node", "args": ["common/scripts/auth.js"] },

    /**
     * (Optional) 缓存 key 前缀,用于多团队隔离
     */
    // "cacheKeyPrefix": "my-company-",

    /**
     * (Optional) 是否允许写入缓存,默认 false
     * 配合环境变量 RUSH_BUILD_CACHE_WRITE_ALLOWED 可在运行时覆盖
     */
    "isCacheWriteAllowed": true
  }
}

rush-project.json 的配置与 local-only 模式完全相同,无需任何修改

4.3 验证 http 模式缓存效果

确保 HTTP 服务已启动,然后:

bash 复制代码
# 第一次构建,写入远程缓存
rush rebuild --verbose

日志与 local-only 模式几乎一致,区别在于缓存实际写到了 server/rush-build-cache/

erlang 复制代码
==[ my-tools ]==============================================[ 1 of 3 ]==

This project was not found in the build cache.
Invoking: unbuild
...
Caching build output folders: dist
Successfully set cache entry.
"my-tools" completed successfully in 3.12 seconds.

此时查看 server/rush-build-cache/ 目录,可以看到 3 个以哈希命名的缓存文件。

模拟 clean 后重新构建:

bash 复制代码
rush purge
rush rebuild --verbose

缓存命中日志:

erlang 复制代码
==[ my-tools ]==============================================[ 1 of 3 ]==

Build cache hit.
Clearing cached folders: dist
Successfully restored output from the build cache.
my-tools was restored from the build cache.
...

最后验证功能 :启动 my-app,浏览器看到与 local-only 模式完全一致的效果------控制台打印 isNumber true/false,页面显示 这是 my-components 组件:my-app。证明http 缓存还原不仅快,而且完全正确workspace:* 的依赖关系通过还原的 dist 目录正常工作。

4.4 两种模式对比

维度 local-only http
配置复杂度 极简(2 个字段) 需配置 httpConfiguration + 运行 HTTP 服务
基础设施 需运行一个 HTTP 服务
缓存存储位置 common/temp/build-cache/ server/rush-build-cache/(或远程)
团队共享 不支持 支持
切换分支命中 支持 支持
适用场景 个人开发、初次体验 团队协作

切换模式只需修改 build-cache.jsoncacheProvider 字段,rush-project.json 完全通用


五、进阶:缓存键(Cache Key)是如何计算的?

很多人会好奇:Rush 怎么知道什么时候该用缓存,什么时候该重新构建?答案就在缓存键的计算逻辑中。

默认情况下,以下内容的哈希会参与缓存键的计算:

因素 说明
项目源文件 项目文件夹下的所有文件,排除 .gitignore 忽略的
依赖项目源文件 该项目依赖的其他 Rush 项目的源文件
NPM 依赖版本 所有直接和间接依赖的版本(含 shrinkwrap 中的哈希)
命令行参数 比如 rush build --mode devrush build --mode prod 会产生不同缓存键
缓存 key 前缀 http 模式下 httpConfiguration.cacheKeyPrefix

举个例子 :在我的项目中,修改 tools/src/index.js 中的 isNumber 实现 -> my-tools 缓存失效 -> 因为依赖链 my-app -> my-toolsmy-app 的缓存也会同步失效(需要重建)。而修改 my-app/src/App.vue 的文案 -> 只有 my-app 缓存失效,my-toolsmy-components 都命中。

自定义缓存输入

如果需要更精细的控制,可以在 rush-project.json 中配置:

json 复制代码
{
  "operationSettings": [
    {
      "operationName": "build",
      "outputFolderNames": ["dist"],
      "globalEnvNames": ["NODE_ENV"],
      "globalSkipForDependenciesCommandLineArguments": ["--mode"]
    }
  ]
}

六、常见问题 & 踩坑指南

Q1:为什么提示「Project does not support caching」?

原因 :项目缺少 rush-project.json,或没有配置 outputFolderNames

解决:参照 Step 2,为每个项目补全配置。

Q2:http 模式下缓存不生效,日志显示「not found in the build cache」但确实构建了?

最常见的原因(按出现频率排序):

  1. HTTP 服务没启动curl http://localhost:3000/ 看是否能连通
  2. isCacheWriteAllowed 为 falsehttpConfiguration.isCacheWriteAllowed 默认 false,需要显式设为 true,或通过环境变量 RUSH_BUILD_CACHE_WRITE_ALLOWED=1 覆盖
  3. 环境变量覆盖了配置 :环境变量 RUSH_BUILD_CACHE_WRITE_ALLOWED=0 被设置,导致只读
  4. 缓存键不一致:不同环境的 Node/rush/pnpm 版本不同,或命令行参数不同
  5. 服务端缓存目录权限问题server/rush-build-cache/ 无写入权限

排查命令

bash 复制代码
# 1. 确认 HTTP 服务可访问
curl -i http://localhost:3000/

# 2. 用 --verbose 观察是否真的发起了 HTTP 请求
rush build --verbose

# 3. 检查服务端是否收到请求并写入了文件
ls server/rush-build-cache/

Q3:缓存命中率很低,经常不命中?

排查思路:

  1. 检查 rush-project.json 是否把不该参与哈希的文件也算进来了
  2. 构建产物是否包含时间戳、随机数等不稳定内容(如 source-map 的随机注释)
  3. 确认不同环境使用的 Node、rush、pnpm 版本一致
  4. --verbose 参数观察日志,看看哪个输入变了
  5. http 模式下,确认所有环境访问的是同一个缓存服务地址

Q4:缓存还原后项目报错?

大概率是 outputFolderNames 不完整 。检查构建脚本是否还输出到了其他目录(比如 .next.cachetypes 等)。例如我的 tools 项目用的是 unbuild,它默认输出 dist,如果哪天改成了 lib,配置也要同步改。

Q5:rush rebuild 和缓存的关系?

  • rush rebuild = 强制重新构建,仍然会写缓存(不会从缓存读)
  • rush build = 优先读缓存,未命中才构建
  • 如果想让 rebuild 也不写缓存:
bash 复制代码
RUSH_BUILD_CACHE_WRITE_ALLOWED=0 rush rebuild

Q6:本地缓存太大,怎么清理?

bash 复制代码
# 清理 common/temp(包含 local-only 模式的 build-cache)
rush purge

# 手动删除 local-only 缓存目录
rm -rf common/temp/build-cache

# http 模式:清理服务端缓存目录
rm -rf server/rush-build-cache/*
# 或通过 HTTP DELETE 接口清理单个缓存
curl -X DELETE http://localhost:3000/<cache-key>

Q7:http 模式下如何切换回 local-only?

只需修改 build-cache.json

json 复制代码
{
  "buildCacheEnabled": true,
  "cacheProvider": "local-only"
}

rush-project.json 完全通用,无需改动。


七、最佳实践总结

应该做的

  1. 先跑通 local-only :验证缓存还原后 App.vue 还能正常运行,确保 rush-project.json 配置正确
  2. 再升级到 http:在团队内搭建共享 HTTP 缓存服务,让团队共享缓存
  3. 定期清理旧缓存 :local-only 用 rush purge,http 模式定期清理 server/rush-build-cache/
  4. 使用 rig 包统一配置 :项目多了之后,用 rig packages 复用 rush-project.json,避免每个项目拷贝一份

不应该做的

  1. 不要把 node_modules 放进缓存rush install 自己有优化
  2. 不要缓存 .rush 临时目录:只缓存真正的构建产物
  3. 不要在 http 模式下暴露无鉴权的服务到公网 :本地开发够用,生产环境务必加鉴权(tokenHandler)和反向代理

八、总结

开启 Rush 构建缓存,你只需要做这几件事:

  1. build-cache.json 设置 buildCacheEnabled: true,选择 cacheProviderlocal-onlyhttp
  2. 每个项目的 rush-project.json 配置 outputFolderNames(不要漏了任何一个子包)
  3. 如果用 http 模式:启动 server/index.js 缓存服务
  4. 验证缓存还原后项目功能正常(比如跑一下 App.vue 看看 HelloWorld 组件和 isNumber 工具函数是否正常)

整个过程不到 30 分钟,但能为团队每天节省数小时的构建等待时间。这就是工程化的魅力------一次投入,持续受益


参考资料


项目源码

完整示例项目已开源:

GitHub: github.com/qq853580228...

项目文件索引

文件 说明
build-cache.json 构建缓存全局配置(local-only / http 切换)
rush.json Rush 主配置(含 3 个项目注册)
server/index.js HTTP 缓存服务最小化实现(http 模式专用)
App.vue 主应用入口
HelloWorld.vue 组件库示例组件
index.js 工具函数库
my-app/rush-project.json my-app 项目级缓存配置
components/rush-project.json components 项目级缓存配置
tools/rush-project.json tools 项目级缓存配置
相关推荐
windliang2 小时前
Claude Code 源码分析(八):Memory 如何被写入、整理与按需召回
前端·算法·面试
木公子2 小时前
Vue3源码精读03:响应式核心依赖追踪机制|track与trigger底层源码全解析
前端·vue.js
睡觉时不困4422 小时前
Obsidian 三端同步完整流程:电脑、手机、平板通过 Gitee 实时同步
前端
渣波3 小时前
React Hooks 核心基石:深度解析 `useState` 的类型推断、泛型约束与空值安全
前端·typescript
YIAN3 小时前
吃透这 5 个核心点,你的 React+TS 代码直接上一个台阶
前端·react.js·typescript
Lear3 小时前
Vite+Vue3 模块自动导入实战:彻底告别繁琐的 import 语句
前端
Csvn3 小时前
📌 position: sticky 失效的 6 个经典坑:明明写了 sticky,为什么就是不吸顶?
前端
恋猫de小郭3 小时前
Flutter 的另外一种形态?社区 DartNative 要来了。
android·前端·flutter
秋天的一阵风3 小时前
🔥 Network 里那坨 "data:" 我真看吐了,自制开源 Chrome 插件,AI 流式调试直接开挂
前端·人工智能·后端