首页
/ 深度解读 @expo/metro-runtime:Expo 生态中驱动高级 Metro 打包特性的运行时基石

深度解读 @expo/metro-runtime:Expo 生态中驱动高级 Metro 打包特性的运行时基石

2026-09-08 13:40:25作者:范垣楠Rhoda

@expo/metro-runtime 是 Expo 官方维护的一个"运行时注入"型包,它自身不负责打包,而是为应用启动时的 JS 运行时注入那些 Metro 高级打包特性(如异步分块加载、Server Components / RSC 边界、window.location 与相对 URL fetch 兼容、开发期错误上报)所必需的补丁与基础设施。它通常不需要开发者手动安装,而是由 expo-routerexpo/metro-config 自动携带,但对于希望了解 Expo 应用在启动瞬间发生了什么、以及想排查"为什么原生端可以发相对路径请求""为什么 Promise 未处理异常会出现在 LogBox"等问题的工程师来说,读懂它的 变更日志 与源码是最高效的切入点。读完本文,你将掌握该包的安装方式、入口执行顺序、四大运行时能力背后的实现位置,以及从 3.x 到 57.x 的版本演进脉络所揭示的 Expo 架构迁移方向。

一、这个包是做什么的:给运行时"打补丁"而非"提供 API"

包自身的 package.json 给出的描述是"Tools for making advanced Metro bundler features work"(让高级 Metro 打包特性得以工作的工具),版本为 57.0.8,授权 MIT,作者为 650 Industries(Expo 团队)。其 README 说得更直接:

Injects runtime code required for advanced Metro bundling features in the Expo ecosystem.

也就是说,它提供的不是 import { something } 这样的业务 API,而是副作用注入:只要在首屏 bundle 里被导入一次,它就会在运行时环境中安装各类 polyfill 和钩子。这一点在源码入口 src/index.ts 中体现得淋漓尽致——该文件没有任何导出符号,只有三件事:

  1. import './location/install':安装 window.location polyfill(原生平台)并为相对 URL 请求包装全局 fetch
  2. import '@expo/metro-runtime/rsc/runtime':接入 RSC / 异步 bundle 分块的运行时加载钩子;
  3. __DEV__ 下分别启用 Metro 终端日志的堆栈捕获(metroServerLogs)、Hermes Promise 拒绝追踪,并把 @expo/log-boxclearAllLogs 挂到 globalThis.__expo_dev_resetErrors

package.json 的依赖可以看出它的技术栈:@expo/log-box(错误覆盖层)、anserstacktrace-parser(ANSI 颜色剥离与堆栈解析)、pretty-format(格式化非 Error 的 rejection 值)、whatwg-fetch(fetch 标准库补丁)。它在 Node 侧通过 build/index.js 暴露,同时提供 expo-source 条件导出指向 src/index.ts,配合 workspace 协议管理 monorepo 内的依赖版本。

二、接入方式:谁负责把它放进你的 bundle

README 给出了唯一的使用姿势:

yarn add @expo/metro-runtime

然后保证它在首屏 bundle 的某个位置被导入(例如 App.js 顶部):

import '@expo/metro-runtime';

这里有两个非常关键的工程细节:

  • expo-router 用户无需安装——它已经被依赖链带上了(README 原文:"expo-router users do not need to install this package, it is already included.")。
  • 导入顺序会被自动纠正:README 明确说明 expo/metro-config 会自动把这个 import 移动到 bundle 的最前面。之所以强调"最前",是因为它要做的安装动作(初始化 RN 核心全局、安装 fetch/URL/window.location)必须在任何应用代码执行前完成,否则后续模块就会拿到"残缺的运行时"。

同时,包内还存在面向未来 Server 运行时的专用入口 ./rsc/runtime(指向 rsc/runtime.js),这也解释了变更日志中多次出现的"entry point 迁移"类条目。

三、四大运行时能力:从变更日志到源码的对照

包的当前形态是由一次次的版本演进堆叠出来的。下面把 CHANGELOG.md 中最重要的功能条目与其在源码中的落点逐一对照。

3.1 window.location polyfill 与相对 URL fetch(原生端也能用 /api/...

变更日志证据链4.0.0-preview.0(2024-10-22)"Enable relative fetch requests by default"(默认启用相对 fetch 请求);同一版本的 "Use src directory for source code";4.0.1 的 "Use window.location polyfill for server requests"(#32099);4.0.0-preview.0 还包含 "Support location.reload() in native production builds"(#29572)。

源码落点:原生实现位于 src/location/install.native.ts。它的顶部注释非常诚实地说出了一段"依赖地狱"的解决顺序:

// 确保 react-native 核心全局先初始化,而不是依赖不稳定的 getModulesRunBeforeMainModule
import 'react-native/Libraries/Core/InitializeCore';
// 先装 fetch,保证 Headers / Request 全局可用
import 'whatwg-fetch';
// 必须导入 expo,确保 URL 已被安装
import 'expo';

随后它会依据来源解析出 origin:

  • 开发模式:优先读取开发服务器 bundle origin(通过 expo/internal/bundle-origingetBundleOrigin(),这也正是 Unpublished 版本中 "Read the development server URL from expo/internal/bundle-origin instead of duplicating its accessor"(#48278)所对应的重构);
  • 生产模式 / 无 bundle origin:回退到 Constants.expoConfig.extra.router.origin,若为空再取 release 构建时自动写入的 extra.router.generatedOrigin

只要 extra.router.origin !== false,它就会:

  1. 在原生端 window 上安装 location getter(若尚未存在),并用解析到的 origin 调用 setLocationHref(url)
  2. wrapFetchWithWindowLocation 包一层全局 fetch,把所有以 / 开头的相对 URL(无论是字符串形式还是 { url } 对象形式)解析成绝对 URL 后再发出请求。

对外的 Location 类(src/location/Location.native.ts)借鉴了 Deno 的 Location 实现,只暴露只读属性:hashhosthostnamehreforiginpathnameportprotocolsearch 全部只可读;对 location.hash = ...location.assign() 等写入行为统一抛出 NotSupportedError。唯一的例外是 reload():开发模式下调用 RN 的 DevSettings.reload(),生产模式下若 globalThis.expo.reloadAppAsync 存在(Expo SDK 51+)则调用它实现应用内重载——这正是"native production builds 支持 location.reload()"那条功能条目的直接产物。

而在 web 端,这些文件有对应的空实现(src/location/install.tssrc/location/Location.ts),因为浏览器本身就提供了这些标准能力,无需 polyfill——这也体现了该包"按平台裁剪、尽量少干扰"的设计哲学。

测试佐证src/location/tests/wrapFetchWithWindowLocation.test.native.ts 用 4 个用例锁定了 fetch 包装器的行为契约:

  • 相对 URL 会基于 location origin 解析('/api/route''http://proxy.test:4443/api/route');
  • request-like 对象中的 url 同样会被解析;
  • 绝对 URL 保持原样;
  • 当没有可解析的 location 时,相对 URL 原样放行,让请求"以自己的方式失败",而不是在这里抛出 Invalid URL

3.2 Metro 终端日志:把错误栈与组件栈送回开发服务器

变更日志证据链6.1.0(2025-08-19)"Pass errors, synthetic and owners stacks to Metro Dev Server terminal"(#38871);6.1.1(2025-08-26)"Avoid sending compilation errors back to Metro terminal from the application runtime"(#39142);Unpublished 中还有 "Update logbox imports"(#46640)。

源码落点:开发模式入口通过 captureStackForServerLogs()(原生端实现 src/metroServerLogs.native.ts)替换 react-nativeHMRClient.log,只拦截 error 级别日志(warn 被有意排除在捕获范围外)。其核心逻辑是:

  • 检测 preventSymbolication 标记:若存在则直接丢弃,避免编译错误被重复打印(一次来自 Metro 自身、一次来自应用运行时);
  • 对携带 stack 字段的错误,把原始堆栈显式 push 进 data(否则会被 pretty-format 吞掉);
  • 对没有错误栈的日志,合成一个 syntheticStack(以空名错误捕获当前调用栈),并提醒开发者"向上查找真正的报错源头";
  • 通过 React 19 的 captureOwnerStack() 捕获组件 owner 栈,并附带检测新老两种组件栈格式( in / at / @...\n)以决定是否补充组件栈。

这些细节解释了为什么开发者在 Expo CLI 终端里能看到比原生报错更完整、可 symbolicate 的调用栈信息。web 端对应实现(src/metroServerLogs.ts)为空函数,说明终端日志回传主要服务于原生平台。

3.3 Promise 拒绝追踪:让"未处理 rejection"现身 LogBox

变更日志证据链6.0.1(2025-08-15)"Show console.error and LogBox for unhandled promise rejection in development"(#38834)。

源码落点src/promiseRejectionTracking.native.tsHermesInternal.hasPromise()enablePromiseRejectionTracker 均存在时启用 Hermes 原生追踪器,并传入:

global.HermesInternal.enablePromiseRejectionTracker({
  allRejections: true,
  onUnhandled: (id, rejection) => { /* 组装并抛出 rejectionError */ },
  onHandled: (id) => { /* console.warn 提醒可忽略先前的提示 */ },
});

实现细节值得一提:对非 Error 的 rejection 值,优先用 pretty-format 格式化,失败再退化为 JSON.stringify;拼装出的错误统一交给 src/ExceptionsManagerhandleException 处理,从而进入 Expo / LogBox 的既有错误管线。同时保留了原始 rejection 的堆栈(若有),并给每条错误加上 Uncaught (in promise, id: N) 前缀,便于开发者把"稍后出现的一条 handled 警告"与之前的未处理异常对应起来。由于它只在 __DEV__ 下被 require(见入口 index.ts),生产包不会携带这份额外的追踪开销。

3.4 RSC / 异步 bundle 加载的运行时接线

变更日志证据链:这一块是 4.x 版本最密集的功能区。4.0.0-preview.0 一口气引入了 "Always enable async bundle loading on native"(原生端始终启用异步 bundle 加载)、"Add server HMR for native"、"Add streaming fetch polyfill"、"Add support for CSS in server components"、"Add initial version of DOM Components"、"Enable relative fetch requests by default";3.0.2 则先在 web 端为生产模式开启了异步代码加载。

源码落点rsc/runtime.js 是这段历史沉淀下来的"接线层"。它以 3 行为核心:

globalThis.__webpack_chunk_load__ = (id) => global`${__METRO_GLOBAL_PREFIX__}__loadBundleAsync`;
globalThis.__webpack_require__ = (id) => global`${__METRO_GLOBAL_PREFIX__}__r`;

即把 Metro/Webpack 语义的 chunk 加载与模块执行分别桥接到 Expo 运行时的 __loadBundleAsync__r 上,让 RSC 的虚拟客户端边界(virtual client boundary)能够在原生端按需拉取分块代码。同时它针对生产原生构建做了一处"防御性"修正:React Native 的缺失模块错误处理会导致生产崩溃,而这里期望的效果是把错误抛给最近的 error boundary 展示给用户,因此会临时把 ErrorUtils.reportFatalError 替换为 throw err 并在模块执行后恢复(注释中还给出了可测试方式:在无虚拟客户端边界的生产 iOS 构建上运行,即可看到所有 split chunk 缺失并抛错)。变更日志中随后的 5.0.4 "Move virtual RSC client boundary entry point to expo"(#36408)则表明这一接线层的入口职责已随架构演进移交给了 expo 包本体。

四、错误呈现能力的迁移史:从自带覆盖层到 @expo/log-box

阅读变更日志时会发现一条清晰的"减法"主线——Expo 正在把错误 UI、日志覆盖层逐步从 @expo/metro-runtime 中抽出:

  • 55.0.0(2026-01-21):"Move error overlay UI to @expo/log-box package"(#39958),正式把错误覆盖层 UI 迁出;
  • 6.1.2(2025-09-12):把 @expo/log-box 调整为 peer dependency(#39603),团队认为 peer 依赖更能准确表达二者之间的边界意图;
  • 5.0.4(2025-04-28):移除 /symbolicate 导入(#36409),并同步把 RSC 客户端边界入口点迁入 expo
  • 4.0.0-preview.0 的多个修复:"Fix logbox usage on native for router errors"、"Prevent LogBoxStateSubscription calling setState while rendering a <Suspense /> boundary"(#32047)等。

即便如此,@expo/metro-runtime 与 LogBox 之间仍保留了一层薄薄的耦合:入口 index.ts

globalThis.__expo_dev_resetErrors = require('@expo/log-box/LogBox').default.clearAllLogs;

把"清空既有错误日志"的能力暴露成运行时全局,供刷新逻辑复用(代码注释明确标注这是过渡期的 TODO)。当前版本依赖中 @expo/log-box: workspace:^ 正是这条迁移链的终点形态。

五、能力边界的持续"上移":Fast Refresh 与 async-require 的归属变化

除了向 @expo/log-box 迁移,变更日志还记录了一条把通用能力移交给 expo 包本身的路径:

  • 6.0.0(2025-08-13,Breaking):"Move async-require and fast refresh to expo"(#36405),作为破坏性变更,@expo/metro-runtime 不再直接承担 async-require 与 Fast Refresh 的运行时;同版本还通过 noop 掉 native 上未使用的代码来抑制 react-native 导入警告(#38495);
  • 5.0.3:移除 web-streams-polyfill,改为依赖 expo 自身的支持(#36407);
  • 5.0.0(Breaking):移除对已废弃的 setImmediate 的全局 polyfill(#35373)——一个典型的"砍掉历史包袱"式清理;同时修复了 Windows 上把 Babel code frame 解析为语法错误的问题(#34017)以及异步导入被破坏的回归(#34824);
  • 3.2.0:为 React 客户端组件标记 "use client" 指令(#27300);3.1.2:为 split chunks 增加 Metro 构建错误的错误处理(#26609)。

可以把它理解为一种持续进行的"架构收敛":那些需要在任何 Expo 应用中无条件生效的基础运行时交给 expo 本体,而 @expo/metro-runtime 只保留与 Metro 构建管线强绑定、且主要服务于 dev 体验的部分。

六、版本节奏解读:55/56/57 与 SDK、React Native 版本的同步

细看 CHANGELOG.md 会发现明显的版本断层与节奏变化:

  • 2023-2024 年间版本停留在 3.x → 4.0.0-preview.x → 5.x → 6.x,采用独立小步快跑;
  • 进入 2026 年后版本号跳升至 55/56/57 并紧密跟随 Expo SDK 版本,且绝大多数补丁版本标注为 This version does not introduce any user-facing changes.(如 57.0.1 ~ 57.0.856.0.1 ~ 56.0.1555.0.1 ~ 55.0.11),说明该阶段主要是 SDK 发布流水线驱动的同步发布;
  • 与 React Native 主版本对齐的"Notices"条目值得关注:55.0.0 声明支持 React Native 0.82.x(#39678),56.0.0 声明支持 React Native 0.84.x(#43018)——这解释了为何在某些版本(如 56.0.0)没有任何用户可见变化时依然会触发 major 版本号变更;
  • 每个版本条目使用统一的分类规范:🛠 Breaking changes / 🎉 New features / 🐛 Bug fixes / 💡 Others / ⚠️ Notices,其中 "Others" 大量承载内部重构(如依赖解耦、导入路径调整、平台空实现),是观察架构演进的第一手材料。

而文档顶部 Unpublished 区块(当前 57.0.8 之后的待发布内容)预告了接下来两个内部改动方向:"Update logbox imports"(#46640)与"Read the development server URL from expo/internal/bundle-origin"(#48278)——后者正对应本文 3.1 节里从 Constants.expoConfig.extra 读取 origin 改为复用 expo 内部工具的重构。

七、源码导航:一份可继续深入的地图

若想亲自验证上文结论,可以按下面的路径继续阅读当前仓库:

关注点 推荐文件
包入口与副作用装配顺序 src/index.ts
window.location polyfill 原生实现(只读 Location、reload 行为) src/location/Location.native.ts
origin 解析、location/fetch 的安装逻辑 src/location/install.native.ts
相对 URL fetch 包装器的行为契约测试 src/location/tests/wrapFetchWithWindowLocation.test.native.ts
Metro 终端错误栈 / 组件栈捕获 src/metroServerLogs.native.ts
Hermes Promise 拒绝追踪 src/promiseRejectionTracking.native.ts
异常统一处理出口 src/ExceptionsManager
RSC 分块加载与错误边界兜底 rsc/runtime.js
包元数据、导出与依赖策略 package.json
安装与使用说明 README.md
版本演进全记录(本文的事实依据) CHANGELOG.md

提示:文中多处提到的 web 平台空实现(如 src/metroServerLogs.tssrc/location/install.ts)是一个值得留意的阅读技巧——同一文件名带 .native 后缀的版本才是原生平台真正执行的逻辑,不带后缀的通用版本往往只做平台占位,这是 Expo monorepo 中"按平台裁剪运行时"的惯用实现模式。

八、开发者视角:何时需要关注这个包

总结起来,理解 @expo/metro-runtime 能在三类场景里真正帮到你:

  1. 排查 dev 体验问题:当你发现终端里错误栈缺失、错误被重复打印,或 LogBox 无法展示某条未处理 Promise rejection 时,排查起点应是 src/metroServerLogs.native.tssrc/promiseRejectionTracking.native.ts 这两份源码,而非 react-native 内部;
  2. 搭建使用 RSC / 异步分块的 Expo 应用:需要理解代码是如何通过 rsc/runtime.js 桥接到 __loadBundleAsync 的,以及为什么生产原生构建中缺失 chunk 会表现为"被 error boundary 捕获的错误"而不是直接崩溃;
  3. 评估升级风险:阅读 CHANGELOG.md 时,重点看 🛠 Breaking changes(例如 5.0.0 移除 setImmediate polyfill、6.0.0 把 async-require/Fast Refresh 移入 expo)以及 ⚠️ Notices(React Native 版本支持声明),它们决定了一次 Expo SDK 升级是否会影响你的自定义 Metro 配置或运行时假设。

需要再次强调的是:除非你脱离 expo-router 与默认 expo/metro-config 手动搭建原生 + Metro 链路,否则无需也通常不应该直接安装这个包——它的存在意义恰恰是"在你不感知的情况下,让高级 Metro 特性正常工作"。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391