- 构建工具
- 移动开发
- CLI
【免费下载链接】metro
🚇 The JavaScript bundler for React Native
Metro(React Native 的 JavaScript 打包器)通过将模块转换(transformation)结果缓存起来,避免了源码或配置未变化时的重复计算,从而显著加速构建。本文以官方文档 docs/Caching.md 为核心,结合metro-cache、metro-config与metro三个包的真实源码,系统讲解 Metro 的缓存工作原理、cacheStores配置方法、内置缓存存储的用法与参数,以及如何实现自定义缓存存储,帮助你在单机与团队场景下正确配置和调优缓存。
一、缓存体系总览:本地缓存与远程缓存
Metro 在开箱状态下即使用本地缓存来存储已转换模块的结果(关于转换阶段的概念,可参考 docs/Concepts.md)。得益于该缓存,Metro 不需要重新转换每个模块——除非自上次转换以来,该模块的源码或当前配置发生了变化。
在本地缓存之外,Metro 还支持远程缓存。对于大型团队和/或大型代码库,远程缓存可以进一步减少本机构建远端变更所花费的时间,从而大幅加速构建。官方文档提到,这正是 Meta 内部使用 Metro 构建 React Native 应用的方式(一个拥有成千上万个文件、数百名日常活跃工程师的代码库)。
典型的远程缓存部署方案
一个标准的远程缓存配置通常包含三个环节:
- 准备一个团队专用的存储后端,例如 S3 bucket。
- 周期性运行
metro build命令(例如放在 CI 任务中)来填充缓存,在 Metro 配置中使用HttpStore(或自定义的读写缓存存储)。 - 在开发机上配置 Metro 从缓存读取,在 Metro 配置中使用
HttpGetStore(或自定义的只读缓存存储)。
metro build是 CLI 提供的构建命令,其常用选项如下(详见 docs/CLI.md):
| 选项 | 别名 | 说明 | 取值 |
|---|---|---|---|
out | O | 输出 bundle 的文件名 | String |
platform | p | 打包的目标平台 | web、android、ios |
minify | z | 是否压缩 bundle | Boolean |
dev | g | 是否生成开发版构建(process.env.NODE_ENV = 'development') | Boolean |
config | c | 使用的metro.config.js路径 | String |
max-workers | j | 并行转换的 worker 数 | Number |
project-roots | P | 项目根目录 | Array |
source-map | — | 是否生成 source map | Boolean |
source-map-url | — | source map 的地址 | String |
resolver-option | — | 自定义 resolver 选项,形式key=value | Array |
transform-option | — | 自定义 transform 选项,形式key=value | Array |
也就是说,CI 中一条类似metro build src/index.js --out dist/bundle.js --platform android的命令即可产出 bundle,并在此过程中通过HttpStore把每个模块的转换结果写入远程缓存;开发机上则通过HttpGetStore命中这些结果,无需在本地重新转换。
二、cacheStores配置:本地优先、远程兜底
Metro 缓存的核心配置项是cacheStores。官方文档明确建议:本地缓存(如FileStore)应排在列表首位,远程缓存(如HttpStore)紧随其后。
这样排列的原因是 Metro 缓存读取的语义:
当 Metro 需要转换一个模块时,它会先为该文件计算一个机器无关的缓存键,然后按顺序尝试从每个 store 中读取。一旦拿到转换结果(无论来自缓存还是重新计算),它会将该结果写入所有对该键返回了
null(缓存未命中)的 store。
这一行为在 Cache.js 中体现得非常清楚:Cache.get()顺序遍历#stores数组,首次命中即返回;Cache.set()则把值写入从第一个 store 起、直到命中该键的那个 store 为止的所有 store(通过#hits这个WeakMap记录“哪个 store 命中了这个键”,避免把已存在的值重复写回)。此外,Cache类为每次get/set记录日志,日志项包含 store 名与十六进制键,并通过action_result: 'hit' | 'miss'标记缓存命中与未命中。
配置cacheStores有两种形式:直接传入 store 实例数组,或传入以metro-cache包导出对象为参数的函数。文档给出的函数形式示例如下:
// metro.config.js const os = require('node:os'); const path = require('node:path'); module.exports = { cacheStores: ({ FileStore }) => [ new FileStore({ root: path.join(os.tmpdir(), 'metro-cache'), }), ], };注意:FileStore、AutoCleanFileStore、HttpStore、HttpGetStore等类既可以从metro-cache包直接导入,也可以通过cacheStores的函数形式(MetroCache参数)拿到,后者避免了在配置文件中额外引入包依赖。配置解析层会在调用该函数时自动注入metro-cache的导出(见 loadConfig.js 与测试 loadConfig-test.js)。cachestores.config.js 是仓库中一个真实的函数形式配置示例:
module.exports = { cacheStores: ({ FileStore }) => { return [new FileStore({ root: __dirname })]; }, };如果不配置该选项,Metro 的默认行为是仅使用一个临时目录下的FileStore。默认配置位于 defaults/index.js:
cacheStores: [ new FileStore({ root: path.join(os.tmpdir(), 'metro-cache'), }), ],相关配置项还有两个值得关注:
cacheVersion(默认'1.0'):参与全局缓存键的计算。当你调整了会改变转换结果的配置时,应提升该版本号使旧缓存失效(详见 docs/Configuration.md)。resetCache:设为true时,Metro 会在启动时重置转换缓存与文件映射缓存(见 docs/Configuration.md)。
三、内置缓存存储逐个解析
Metro 为cacheStores提供了若干内置实现(源码位于 packages/metro-cache/src/stores,统一从 packages/metro-cache/src/index.js 导出)。
FileStore({root})
将缓存条目作为文件存放到root指定的目录下。从 FileStore.js 可以看出其磁盘布局:
#getFilePath(key: Buffer): string { return path.join( this.#root, key.slice(0, 1).toString('hex'), // 键的第一个字节作为子目录 key.slice(1).toString('hex'), // 剩余字节作为文件名 ); }即每个缓存键对应root/<首字节 hex>/<其余字节 hex>路径下的一个文件,文件内容为 JSON 序列化的值;如果值是Buffer,则在前面加一个0x00字节作为类型标记(读取时据此还原,见 FileStore.js)。写入时会自动递归创建目录,读取时遇到ENOENT或 JSON 解析错误一律视为未命中返回null;clear()则会删除root下的全部 256 个子目录。
AutoCleanFileStore()(已弃用)
一个会定期清理旧条目的FileStore,接受与FileStore相同的root参数,另加两个选项:
intervalMs: number:两次清理尝试之间的时间间隔(毫秒),默认10 分钟;cleanupThresholdMs: number:条目距上次修改至少多久才允许被删除(毫秒),默认3 天。
对应源码见 AutoCleanFileStore.js(默认值分别为10 * 60 * 1000与3 * 24 * 60 * 60 * 1000)。文档与源码注释都明确标注该实现已弃用:其实现不够高效,缓存较大时可能产生大量冗余 I/O。官方建议改用你自己的清理脚本,或使用基于监听(watches)、挂钩 get/set、实现 LRU 的自定义缓存存储。
HttpStore(options)
一个轻量级(bare-bones)的远程缓存客户端,通过 HTTP/HTTPS 执行读取(GET)与写入(PUT),传输的是压缩后的缓存产物。其核心选项如下:
endpoint: string:缓存服务器的基础 URL。文档给出的示例是:HttpStore配'http://www.example.com/endpoint'时,发出的请求形如http://www.example.com/endpoint/c083bff944879d9f528cf185eba0f496bc10a47d——即键的十六进制字符串直接拼在endpoint路径之后(见 HttpStore.js 中path: ${path}/${key.toString('hex')})。timeout: number:请求超时时间(毫秒),默认5000。family: 4 | 6:与 Node.jshttp.request的family参数含义一致。cert、ca、key:HTTPS 选项,直接透传给 Node.js 内置的 HTTPS 客户端。
从源码看,HttpStore还支持更多未在文档中展开的选项,值得了解:
getOptions/setOptions:可以为读、写分别指定不同的端点配置(HttpStore.js),例如读走只读端点、写走可写端点;params: URLSearchParams、headers:附加在请求 URL 与请求头上的额外参数;additionalSuccessStatuses:额外视为成功的 HTTP 状态码(默认仅 2xx 与 404 特例);maxAttempts、retryNetworkErrors、retryStatuses:重试配置。重试基于exponential-backoff实现,采用 full jitter,最大延迟 30 秒;仅在错误属于HttpError且状态码命中retryStatuses、或属于NetworkError且开启retryNetworkErrors时才重试(HttpStore.js);proxy:通过https-proxy-agent支持代理;debug:出错时在错误信息中附加响应体,便于排查。
在协议层面:GET返回 404 视为未命中(返回null),返回 200 时响应体会被 gzip 解压,再按“0x00前缀的 Buffer 或 JSON”规则还原;PUT写入时用 gzip(压缩级别 9)压缩请求体。相关的HttpError(带 HTTP 状态码)与NetworkError分别定义在 HttpError.js 与 NetworkError.js。
HttpGetStore(options)
HttpStore的只读版本。它继承HttpStore的全部读能力,但:
set()为空操作(HttpGetStore.js);get()遇到HttpError或NetworkError时不抛出,而是吞掉错误并返回null,同时只发出一次process.emitWarning警告,提示无法连接 HTTP 缓存(HttpGetStore.js)。
这正是它适合放在开发机上的原因:远程缓存不可用时,开发构建不至于失败,只是退化为本地计算。
四、缓存键的计算与命中流程(源码级)
理解缓存的粒度有助于正确配置。Metro 的缓存单位是单个模块的转换结果,键的构造在 Transformer.js 中完成:
- 全局缓存键(globalCacheKey):基于
cacheVersion、projectRoot与 transformer 配置计算(Transformer.js);若cacheStores为空数组(缓存被禁用),则全局缓存键为空串(Cache.isDisabled在 Cache.js 中定义)。 - 部分键(partialKey):由全局缓存键、项目相对路径(POSIX 分隔)、asset URL 路径、以及
dev、platform、minify、type等转换选项经stableHash计算得到(Transformer.js)。 - 完整键(fullKey):
partialKey拼接模块内容的 SHA-1 后得到(Transformer.js)。
也就是说,同一个文件只要内容(SHA-1)没变、转换选项没变,就能命中缓存;而stableHash使用带 canonicalize 的 JSON 序列化加 MD5 实现机器无关的稳定哈希(见 stableHash.js),保证了不同机器之间缓存键一致、可共享远程缓存。
命中后的流程是:cache.get(fullKey)命中则直接使用结果;未命中则交给workerFarm.transform()重新转换,转换完成后cache.set()以 fire-and-forget 方式异步写回所有未命中该键的 store(Transformer.js)。读取与写入失败会分别通过 reporter 上报cache_read_error/cache_write_error事件。
五、自定义缓存存储
cacheStores接收的是实现了以下接口的类的实例(官方文档 docs/Caching.md 原样给出,仓库中的类型定义见 types.js):
interface CacheStore<T: Buffer | JsonSerializable> { // 从缓存读取条目,未找到时返回 null get(key: Buffer): ?T | Promise<?T>; // 写入缓存条目(只读存储可空操作) set(key: Buffer, value: T): void | Promise<void>; // 清空缓存(如可行),否则空操作 clear(): void | Promise<void>; } type JsonSerializable = /* Any JSON-serializable value */;实现时有几点需要严格遵守:
- 缓存条目的值要么是
Buffer实例,要么是 JSON 可序列化的值(内部结构两者均不指定); - 对同一个缓存键,
get()必须返回与set()时相同类型的值(例如不能写入 Buffer 却按 JSON 解析返回); set与clear是“尽力而为”的:只读存储直接空操作即可;- 按需提供
name属性(或依赖类名),用于Cache的日志标识(Cache.js)。
如果你的远程存储无法直接支持按 key 读写(例如对象存储需要额外索引),可以仿照HttpGetStore的容错思路:读取失败时返回null并告警,让缓存不可用时退化为本地计算;写入失败时则交由Cache.set()内部捕获——源码中set()会收集所有 store 的写失败并抛出AggregateError(Cache.js),因此自定义 store 的set()建议在需要时自行吞掉可恢复错误,避免影响主构建流程。
六、实战:完整的本地 + 远程缓存配置
将以上内容组合起来,一个面向团队场景的完整metro.config.js大致如下:
// metro.config.js const os = require('node:os'); const path = require('node:path'); module.exports = { cacheStores: ({ FileStore, HttpStore, HttpGetStore }) => [ // 1. 本地缓存优先,目录置于系统临时目录 new FileStore({ root: path.join(os.tmpdir(), 'metro-cache'), }), // 2. CI/写端:完整的读写远程缓存客户端 new HttpStore({ endpoint: 'https://cache.example.com/metro', timeout: 5000, // HTTPS 双向认证所需选项(如需要) // cert: '...', ca: '...', key: '...', // 可选:读、写使用不同端点 // getOptions: { endpoint: 'https://cache-ro.example.com/metro' }, // setOptions: { endpoint: 'https://cache.example.com/metro' }, }), // 3. 开发机/只读端:远程缓存不可用时静默降级 // new HttpGetStore({ endpoint: 'https://cache.example.com/metro' }), ], };配置落地后:
- CI 环境:定期执行
metro build <entry> --out bundle.js(可配合--platform、--minify等选项),用HttpStore将转换结果灌入团队缓存; - 开发环境:使用
HttpGetStore(或去掉写端 store),构建时优先命中本地FileStore,未命中则从远程缓存读取,远程不可用时静默回退; - 版本升级:升级 Metro 或改变 transformer 配置后,通过提升
cacheVersion让旧缓存整体失效,避免错误复用旧格式的缓存产物。
结语
Metro 的缓存体系本质上是“本地文件缓存兜底 + 可插拔的远程缓存提速”:cacheStores决定存储后端,cacheVersion与模块 SHA-1 决定缓存键的有效性,Cache类决定多存储间的读写语义。把握住“本地优先、远程在后、读取失败降级”这三条原则,再结合metro-cache内置实现与自定义接口,就能在单机开发、CI 预热、团队共享三种场景下充分发挥 Metro 缓存的加速能力。相关实现细节可继续研读 packages/metro-cache、Transformer.js 以及 docs/Configuration.md 中的cacheStores小节。
- 构建工具
- 移动开发
- CLI
【免费下载链接】metro
🚇 The JavaScript bundler for React Native
相关推荐
sbt构建缓存终极指南:远程缓存和本地缓存配置详解
sbt构建缓存终极指南:远程缓存和本地缓存配置详解 在Scala项目开发中, sbt构建缓存 是提升开发效率的关键技术。无论是个人开发还是团队协作,合理配置缓存
构建工具Turborepo 缓存实战指南:Monorepo 本地与远程构建缓存配置与调优
Turborepo 缓存实战指南:Monorepo 本地与远程构建缓存配置与调优 本文以 agents24 仓库中 developer essentials 插
AI 插件AI 技能开发工具Cangjie/HarmonyOS-Examples 缓存机制实现:本地缓存与网络缓存
Cangjie/HarmonyOS Examples 缓存机制实现:本地缓存与网络缓存 引言:为什么需要缓存机制? 在移动应用开发中,缓存(Cache)机制是提
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考