
expo-updates 实战指南在 Expo 应用中实现远程代码更新的原理、配置与调试【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo导读expo-updates是 Expo 生态中负责应用代码远程更新的核心模块它让 React NativeAndroid / iOS / Web应用能够在无需重新发布商店包的情况下从符合Expo Update 协议的服务器拉取并应用新的 JS 代码与资源。本文以 packages/expo-updates/README.md 为主线结合仓库内 DEVELOPMENT.md、guides/examples.md 与 src/Updates.ts 源码完整讲解 expo-updates 的更新模型、协议、配置项、JS API、安装方式以及本地开发与调试技巧读完即可上手为应用接入远程更新能力。一、expo-updates 是什么根据 packages/expo-updates/README.md 的定位expo-updates模块使你的应用能够管理应用到应用代码的远程更新。它本身并不提供更新托管服务而是扮演客户端运行时的角色负责发现、下载、校验、存储并最终切换运行更新update。从 package.json 的描述中可以进一步确认其职责Fetches and manages remotely-hosted assets and updates to your apps JS bundle.获取并管理托管在远端、针对你应用 JS 包的资源与更新。当前仓库中该模块版本为57.0.11采用 MIT 协议开源依赖expo-manifests清单解析、expo-structured-headers结构化请求头、expo-updates-interface原生接口等内部包。这个模块有三个关键的使用前提它必须搭配一个实现了 Expo Update 协议的服务器才能发挥完整能力Expo 官方的EAS Update托管服务实现了该协议是开箱即用的选择如果你需要自建服务可以参考官方提供的示例服务器实现README 中指向 custom-expo-updates-server 示例仓库。二、核心概念Update更新由什么构成理解 expo-updates 之前先要建立更新的原子模型。仓库的 guides/general.md 给出了非常清晰的表述这是理解整个模块的基石一个更新是一个原子单元由两部分组成manifest清单/元数据描述该更新是什么、何时创建、包含哪些资源一组 assets资源JS bundle、Hermes 字节码HBC、图片、字体以及该更新运行所需的其他媒体文件其中有一个资源被指定为launch asset启动资源通常是 JS bundle / HBC应用启动时会把它交给宿主React Native执行。更新有多个来源远程更新从远程服务器下载嵌入式更新embedded update除开发构建外每个应用二进制包内都内置一份更新。一个值得注意的设计细节是expou-updates 会把嵌入式更新也复制进 SQLite 与资源存储尽管它已经存在于磁盘上。这样做的优势在于全模块只保留一条启动更新的代码路径嵌入式更新中的资源若被远程更新复用则无需重新下载即使应用被新构建覆盖带来了新的嵌入式更新旧的嵌入式更新也依然可用。排序问题与 SelectionPolicy由于客户端必须能够在不访问服务器的情况下确定多个更新的先后顺序expo-updates 引入了SelectionPolicy类作为可插拔的排序接口。举例来说用户安装 build 1内置更新 A开发者发布更新 B 并被下载随后开发者发布 build 2内置更新 C用户从应用商店更新后首次启动时expo-updates 必须在没有服务器往返的情况下立刻比较 B 与 C 并决定启动哪一个。如果单纯依赖createdAt字段排序那么基于服务器的回滚rollback就必须以创建时间更晚的新更新形式发布才能被客户端执行——这是选择排序策略时需要考虑的权衡。三、服务端协议Expo Update protocol 与 EAS UpdateREADME 明确指出This module works with a server that implements the [Expo Update protocol]. The [EAS Update] hosted service implements this protocol.也就是说expo-updates 是面向通用协议实现编写的。仓库 guides/general.md 特别强调了一条设计原则expo-updates should be written for a general server implementation (using the Expo Updates specification) and should not make any assumptions or allowances specifically for the EAS Update service.即 expo-updates 不应为 EAS Update 服务做任何专属假设其他符合协议的服务器实现不应沦为二等公民实践中仅少数小功能存在 EAS 专属代码大型特性都刻意设计为可通用。客户端对协议服务器的两个关键假设guides/general.md 还记录了客户端对服务端行为的两条硬性依赖自建服务器时必须遵守manifest 的id是更新的唯一标识。若服务器托管了两个id相同但其他内容不同的 manifestexpo-updates 不会逐字段比较去发现差异。换句话说从客户端视角看更新在服务端本质上不可变资源按 manifest 中的key属性命名落盘客户端假设任何两个key相同的文件是同一资源、可以互相替换。因此服务端必须保证key在全部资源中唯一EAS Update 与经典 Expo 更新服务目前都遵守。四、JS API 全面解析基于源码expo-updates 的 JS API 全部实现在 src/Updates.ts由 src/index.ts 导出。以下逐一说明其导出的常量与方法均以import * as Updates from expo-updates使用。4.1 只读状态常量常量类型说明依据 src/Updates.tsisEnabledboolean模块是否启用。以下任一情况会返回false配置中显式关闭、URL 缺失或无效、缺少 runtimeVersion 或 SDK version、初始化时存储访问出错。为false时直接加载嵌入式更新updateIdstring \| null当前运行更新的 UUID小写规范形式。在本地开发环境或模块未启用时为nullchannelstring \| null当前构建的渠道名用于 EAS Update。Expo Go 与开发构建不绑定渠道恒为nullruntimeVersionstring \| null当前构建的 runtime versioncheckAutomaticallyenum \| null是否以及何时在启动时自动检查/下载更新取值见下文localAssetsLocalAssets本地已存在资源的映射isEmergencyLaunchboolean是否处于紧急启动回退状态expo-updates 尽力启动单调更新的版本但极少数情况下会回退到二进制内嵌更新可用于做特殊兼容处理emergencyLaunchReasonstringisEmergencyLaunch为true时的错误信息launchDurationnumber启动耗时毫秒isEmbeddedLaunchboolean当前运行的更新是否为构建内置的那份manifestPartialManifest当前运行更新的 manifest 对象开发模式或模块未启用时为空对象createdAtDate \| null当前运行更新的创建时间开发模式下为nullcheckAutomatically的映射关系定义在 src/Updates.ts原生值到 JS 值的对应为ALWAYS→ON_LOAD启动时检查ERROR_RECOVERY_ONLY→ON_ERROR_RECOVERY仅错误恢复时NEVER→NEVER从不自动检查WIFI_ONLY→WIFI_ONLY仅在 Wi-Fi 下检查4.2 核心方法checkForUpdateAsync(): PromiseUpdateCheckResult向服务器询问是否有新更新不实际下载。源码中该方法在开发模式__DEV__或开发者工具运行时会抛出ERR_UPDATES_DISABLED的CodedError并提示请用npx expo run:ios --configuration Release或npx expo run:android --variant Release构建 Release 版来测试。官方建议检查更新的合理频率是用户启动或应用回到前台时避免高频轮询网络请求消耗流量与电量Expo 侧可能限流。fetchUpdateAsync(): PromiseUpdateFetchResult把服务器上最新部署的更新下载到设备本地存储。下载完成后若立即调用reloadAsync()可立刻应用否则将在下次冷启动时应用。同样在开发模式下会拒绝。reloadAsync(options?)使用最近下载的版本重新加载应用。与expo包提供的Expo.reloadAppAsync()不同它不仅重新加载还会把 JS bundle 切换到最新下载的更新。源码提示不要在await Updates.reloadAsync()之后放置关键逻辑因为 Promise 只保证重新加载指令已提交。getExtraParamsAsync() / setExtraParamAsync(key, value)读取/设置额外参数。这些参数会以 [Expo Structured Field Value] 格式放进Expo-Extra-Params请求头符合协议的服务器可利用它来选择返回哪个更新。value传null表示取消该参数。readLogEntriesAsync(maxAge 3600000)/clearLogEntriesAsync()读取最近默认 1 小时内的 expo-updates 日志条目 / 清空日志。setUpdateURLAndRequestHeadersOverride(configOverride)/setUpdateRequestHeadersOverride(requestHeaders)实验性在运行时覆盖构建时的更新 URL 与请求头用于从自定义 URL 加载特定更新。源码明确警告使用风险自负且要求 app.json 中开启disableAntiBrickingMeasures: true才会生效。showReloadScreen(options) / hideReloadScreen()显示/隐藏可定制的重新加载过渡屏主要供调试构建中测试 reload 界面的视觉效果。ReloadScreenOptions支持backgroundColor、spinner颜色、image支持require的图片资源、imageResizeMode等。4.3 事件监听UseUpdates Hook除命令式 API 外模块还提供 React Hook见 src/UseUpdates.ts 与 src/UpdatesEmitter.ts典型用法是新更新已下载完成时提示用户重启应用import { useUpdates } from expo-updates; function UpdateManager() { const { isUpdateAvailable, isUpdatePending, downloadedUpdate } useUpdates(); useEffect(() { if (isUpdateAvailable) { // 提示用户新版本已就绪 } }, [isUpdateAvailable]); }当LoaderTask在启动超时后才下载完新更新时客户端会向正在运行的 JS 实例发送事件监听方即可借此调用reloadAsync()让新更新立刻生效详见下文启动流程。五、安装bare React Native 项目接入方式README 明确指出在纯裸bareReact Native 项目中的安装方法以官方文档页Installing expo-updates为准。结合 DEVELOPMENT.md 的说明可以归纳出以下要点使用expo init选择任一 bare 模板时最新版 expo-updates 已预装并预配置原生配置位置iOS 在Expo.plistAndroid 大多可在AndroidManifest.xml中配置必须确保两个核心配置正确更新服务 URLiOS 键EXUpdatesURLAndroid 键expo.modules.updates.EXPO_UPDATE_URL运行时版本iOS 键EXUpdatesRuntimeVersionAndroid 键expo.modules.updates.EXPO_RUNTIME_VERSION。若在 JS 中使用了expo-updates的 API还需要按官方页面完成原生侧初始化iOS 设置EXUpdatesAppController的bridge、Android 调用UpdatesController.initialize或设置ReactNativeHost否则reloadAsync()在产线模式下会因找不到 JS runtime 引用而拒绝——这一点在 src/Updates.ts 的文档注释中有明确提示。本地开发时链接本地源码DEVELOPMENT.md 提供了在本地仓库中联调 expo-updates 的方法若不使用任何 JS API可用yarn link直接链接若需要使用 JS 模块方法由于Metro 不支持符号链接建议二选一在 package.json 中将依赖替换为expo-updates: file:/path/to/expo/expo/packages/expo-updates每次改动源码后执行yarn --force让 yarn 重新拷贝到 node_modules不等待 yarn手动把 expo-updates 复制进 node_modules。六、原生配置与开发调试DEVELOPMENT.md 详解6.1 忽略嵌入式更新Ignore Embedded Update当你在用 expo-updates 测试自己开发的服务器时很可能希望它忽略内置更新。原因在于每次新构建都会生成一个创建时间更新的 bundle导致客户端拒绝加载此前发布的所有更新。解决办法是把EXUpdatesHasEmbeddedUpdateiOS与expo.modules.updates.HAS_EMBEDDED_UPDATEAndroid设为false强制走远程更新。6.2 附加请求头Additional Headers若需要给 manifest 请求附加自定义请求头iOS在EXUpdatesRequestHeaders键下以字典map形式配置Android目前无法在 AndroidManifest.xml 中配置需在MainApplication.java中调用UpdatesController.overrideConfiguration(Context, MapString, Object)方法把requestHeaders传入。6.3 构建与启用时机默认行为expo-updates 只在裸应用的 Release 构建中启用Debug 构建从本地 Metro 服务器加载。iOS Release 构建Xcode → Product → Scheme → Edit Scheme把 Run 配置的 Build Configuration 从 Debug 改为 Release再点击 RunAndroid Release 构建在项目根目录执行react-native run-android --variant Release。6.4 在 Debug 模式下启用 expo-updates某些场景例如想打断点单步调试需要 Debug 构建也启用 expo-updates先按上文配置忽略嵌入式更新设置环境变量export EX_UPDATES_NATIVE_DEBUG1iOS 额外两步修改工程文件把SKIP_BUNDLING替换为FORCE_BUNDLING强制 Debug 与 Release 都打包应用 JSsed -i s/SKIP_BUNDLING/FORCE_BUNDLING/g; ios/project name.xcodeproj/project.pbxproj重新安装 CocoaPods在项目顶层目录执行npx pod-install。完成后再构建 Debug 包它就会表现得像 Release 构建只是不带嵌入式更新。七、启动与下载的运行时流程源码级guides/examples.md 详细记录了 expo-updates 的运行时行为是理解其内部架构的最好材料。7.1 应用启动流程Release 构建通过 expo-modules-core 与UpdatesPackageAndroid或ExpoUpdatesReactDelegateHandleriOS初始化并启动 expo-updates读取原生构建配置转换为UpdatesConfiguration/EXUpdatesConfig对象同时初始化数据库、文件系统引用与错误恢复处理器用配置对象初始化并启动LoaderTask若配置要求检查新更新LoaderTask以launchWaitMs为时长启动计时器LoaderTask读取嵌入式 manifest用SelectionPolicy决定是否通过EmbeddedLoader将嵌入式更新载入 SQLite——每次启动都必须执行因为二进制随时可能被商店更新在其余动作之前先启动一个DatabaseLauncher实例选择并准备好一个绝对安全可启动的更新逐一确认资源存在并取得磁盘路径若配置允许检查更新LoaderTask在后台线程启动RemoteLoader请求 manifest → 用SelectionPolicy决定是否入库 → 若入库则下载 SQLite 中缺失的资源RemoteLoader完成后回调LoaderTask决策计时器未超时创建新的候选DatabaseLauncher选择刚下载的更新并交给UpdatesController启动计时器已超时旧更新已启动向 JS 发送新更新可用事件由应用决定何时调用reloadAsync()全部完成后后台运行Reaper进程清理数据库中的旧更新保留当前运行更新、任何更新的版本以及最近的一个旧版本作为回滚安全网。该流程体现了 guides/general.md 提到的核心架构原则Loader类负责装载进 SQLite写Launcher类负责从 SQLite 启动读两者可以独立、同时、分离地运行且几乎任何情况都好过崩溃——开发者依赖本模块不破坏用户对应用的信任除错误恢复模式下由开发者代码引起的崩溃外都应尽量避免崩溃。7.2 下载更新的详细步骤Loader装载更新的过程远程或内置皆同通过子类方法加载 manifest从 URL 下载或从应用包内读取RemoteLoader检查数据库是否已有此更新且状态为READY——若是则直接触发成功回调不再做任何事否则遍历 manifest 中的每个资源检查是否 (a) 已在数据库中且 (b) 已存在于磁盘约定文件名相同即同一资源。磁盘缺失则发起下载全部下载完成后为已在磁盘但不在数据库的资源补写 SQLite 行例如破坏性数据库迁移后可能出现若无错误且所有资源齐备将更新标记为READY并触发成功回调否则触发错误回调。7.3 资源意外缺失的自愈机制正常情况资源文件不会被系统清理但若存储损坏或代码缺陷导致资源缺失DatabaseLauncher会依次尝试从嵌入式 manifest 中找回缺失资源并复制 → 从 SQLite 记录的 URL 下载 → 若启动资源缺失则触发失败回调否则仍触发成功回调期望更新可带缺失资源运行。7.4 嵌入式更新的特殊处理Android 多分辨率资源Android 上通过require(./image.png)引用图片时系统可能按屏幕密度映射到image.png/image2x.png/image3x.png。Expo 侧把每个文件当作独立资源全部下载后才算READY但嵌入式更新在 Android 上会按dpi目录存放于res中运行时ResourcesAPI 只允许访问与当前设备匹配的分辨率资源导致EmbeddedLoader无法复制其他密度的资源。为此这些更新被标记为特殊的EMBEDDED状态直接从应用包启动不经.expo-internal目录、不做资源覆盖由 RN 直接从应用包读取资源——这是 expo-updates 极少把嵌入式更新区别对待的场景之一启动时必须额外校验嵌入式更新仍是预期的那份用户可能已更新构建。八、二进制补丁支持BSPatch 与 BZip2DEVELOPMENT.md 说明 expo-updates 内置了对资源应用二进制补丁binary patches的支持采用修改版FreeBSD BSPatch为保证移动端环境安全、不导致用户应用崩溃而做了改造BSPatch 依赖BZip2iOS 链接系统自带 BZip2 库Android 则在仓库中内置一份 BZip2 源码随原生构建一起编译且只包含解压缩部分需要升级 BZip2 时下载官方源码后将必要文件替换到packages/expo-updates/android/src/main/cpp/third-party/bzip2目录。九、常见问题排查如果 expo-updates 没有加载你期望的更新优先检查两点依据 DEVELOPMENT.md Troubleshooting 一节目标更新的创建时间是否晚于嵌入式 bundle或者已配置忽略嵌入式更新Expo.plist / AndroidManifest.xml 中配置的 SDK 或 runtime version 是否与所加载 manifest 中的一致——版本不匹配会被拒绝加载。另外可结合两个调试入口readLogEntriesAsync()读取模块日志定位失败原因isEmergencyLaunchemergencyLaunchReason判断是否发生了紧急启动回退并获取原因。十、总结expo-updates 把远程更新这件事抽象成一套清晰且可扩展的运行时模型原子化的 updatemanifest assets、客户端可独立排序的SelectionPolicy、读写分离的Loader/Launcher、以及遵循通用 Expo Update 协议的服务器交互。无论你选择官方 EAS Update 托管服务还是基于协议自建服务器掌握本文涉及的配置键、JS API、启动流程与调试手段都能让远程更新在你的应用中稳定、可控地运行。进一步阅读仓库材料可参考 DEVELOPMENT.md开发与调试、guides/general.md设计哲学与假设、guides/examples.md完整启动/下载流程以及 src/Updates.tsJS API 全量源码。【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考