news 2026/9/10 18:32:37

expo-updates 实战指南:在 Expo 应用中实现远程代码更新的原理、配置与调试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
expo-updates 实战指南:在 Expo 应用中实现远程代码更新的原理、配置与调试

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 Native(Android / 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 app's JS bundle."(获取并管理托管在远端、针对你应用 JS 包的资源与更新)。当前仓库中该模块版本为57.0.11,采用 MIT 协议开源,依赖expo-manifests(清单解析)、expo-structured-headers(结构化请求头)、expo-updates-interface(原生接口)等内部包。

这个模块有三个关键的使用前提:

  1. 必须搭配一个实现了 Expo Update 协议的服务器才能发挥完整能力;
  2. Expo 官方的EAS Update托管服务实现了该协议,是开箱即用的选择;
  3. 如果你需要自建服务,可以参考官方提供的示例服务器实现(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 Update

README 明确指出:

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 还记录了客户端对服务端行为的两条硬性依赖,自建服务器时必须遵守:

  1. manifest 的id是更新的唯一标识。若服务器托管了两个id相同但其他内容不同的 manifest,expo-updates 不会逐字段比较去发现差异。换句话说,从客户端视角看,更新在服务端本质上不可变
  2. 资源按 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.ts)
isEnabledboolean模块是否启用。以下任一情况会返回false:配置中显式关闭、URL 缺失或无效、缺少 runtimeVersion 或 SDK version、初始化时存储访问出错。为false时直接加载嵌入式更新
updateIdstring \| null当前运行更新的 UUID(小写规范形式)。在本地开发环境或模块未启用时为null
channelstring \| null当前构建的渠道名(用于 EAS Update)。Expo Go 与开发构建不绑定渠道,恒为null
runtimeVersionstring \| null当前构建的 runtime version
checkAutomaticallyenum \| null是否以及何时在启动时自动检查/下载更新,取值见下文
localAssetsLocalAssets本地已存在资源的映射
isEmergencyLaunchboolean是否处于"紧急启动"回退状态(expo-updates 尽力启动单调更新的版本,但极少数情况下会回退到二进制内嵌更新),可用于做特殊兼容处理
emergencyLaunchReasonstringisEmergencyLaunchtrue时的错误信息
launchDurationnumber启动耗时(毫秒)
isEmbeddedLaunchboolean当前运行的更新是否为构建内置的那份
manifestPartial<Manifest>当前运行更新的 manifest 对象;开发模式或模块未启用时为空对象
createdAtDate \| null当前运行更新的创建时间;开发模式下为null

checkAutomatically的映射关系定义在 src/Updates.ts,原生值到 JS 值的对应为:

  • ALWAYSON_LOAD(启动时检查)
  • ERROR_RECOVERY_ONLYON_ERROR_RECOVERY(仅错误恢复时)
  • NEVERNEVER(从不自动检查)
  • WIFI_ONLYWIFI_ONLY(仅在 Wi-Fi 下检查)

4.2 核心方法

  • checkForUpdateAsync(): Promise<UpdateCheckResult>:向服务器询问是否有新更新,不实际下载。源码中该方法在开发模式(__DEV__或开发者工具运行时)会抛出ERR_UPDATES_DISABLEDCodedError,并提示"请用npx expo run:ios --configuration Releasenpx expo run:android --variant Release构建 Release 版来测试"。官方建议:检查更新的合理频率是用户启动或应用回到前台时,避免高频轮询(网络请求消耗流量与电量,Expo 侧可能限流)。

  • fetchUpdateAsync(): Promise<UpdateFetchResult>:把服务器上最新部署的更新下载到设备本地存储。下载完成后若立即调用reloadAsync()可立刻应用;否则将在下次冷启动时应用。同样在开发模式下会拒绝。

  • reloadAsync(options?):使用最近下载的版本重新加载应用。与expo包提供的Expo.reloadAppAsync()不同,它不仅重新加载,还会把 JS bundle 切换到最新下载的更新。源码提示:不要在await Updates.reloadAsync()之后放置关键逻辑,因为 Promise 只保证"重新加载指令已提交"。

  • getExtraParamsAsync() / setExtraParamAsync(key, value):读取/设置"额外参数"。这些参数会以 [Expo Structured Field Value] 格式放进Expo-Extra-Params请求头,符合协议的服务器可利用它来选择返回哪个更新。valuenull表示取消该参数。

  • readLogEntriesAsync(maxAge = 3600000)/clearLogEntriesAsync():读取最近(默认 1 小时内)的 expo-updates 日志条目 / 清空日志。

  • setUpdateURLAndRequestHeadersOverride(configOverride)/setUpdateRequestHeadersOverride(requestHeaders)(实验性):在运行时覆盖构建时的更新 URL 与请求头,用于从自定义 URL 加载特定更新。源码明确警告"使用风险自负",且要求 app.json 中开启disableAntiBrickingMeasures: true才会生效。

  • showReloadScreen(options) / hideReloadScreen():显示/隐藏可定制的"重新加载过渡屏",主要供调试构建中测试 reload 界面的视觉效果。ReloadScreenOptions支持backgroundColorspinner(颜色)、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 明确指出,在纯裸(bare)React Native 项目中的安装方法以官方文档页"Installing expo-updates"为准。结合 DEVELOPMENT.md 的说明,可以归纳出以下要点:

  • 使用expo init选择任一 bare 模板时,最新版 expo-updates 已预装并预配置
  • 原生配置位置:iOS 在Expo.plistAndroid 大多可在AndroidManifest.xml中配置;
  • 必须确保两个核心配置正确:
    • 更新服务 URL:iOS 键EXUpdatesURL,Android 键expo.modules.updates.EXPO_UPDATE_URL
    • 运行时版本:iOS 键EXUpdatesRuntimeVersion,Android 键expo.modules.updates.EXPO_RUNTIME_VERSION

若在 JS 中使用了expo-updates的 API,还需要按官方页面完成原生侧初始化(iOS 设置EXUpdatesAppControllerbridge、Android 调用UpdatesController.initialize或设置ReactNativeHost),否则reloadAsync()在产线模式下会因找不到 JS runtime 引用而拒绝——这一点在 src/Updates.ts 的文档注释中有明确提示。

本地开发时链接本地源码

DEVELOPMENT.md 提供了在本地仓库中联调 expo-updates 的方法:

  • 若不使用任何 JS API,可用yarn link直接链接;
  • 若需要使用 JS 模块方法,由于Metro 不支持符号链接,建议二选一:
    1. 在 package.json 中将依赖替换为"expo-updates": "file:/path/to/expo/expo/packages/expo-updates",每次改动源码后执行yarn --force让 yarn 重新拷贝到 node_modules;
    2. 不等待 yarn,手动把 expo-updates 复制进 node_modules。

六、原生配置与开发调试(DEVELOPMENT.md 详解)

6.1 忽略嵌入式更新(Ignore Embedded Update)

当你在用 expo-updates 测试自己开发的服务器时,很可能希望它忽略内置更新。原因在于:每次新构建都会生成一个"创建时间更新"的 bundle,导致客户端拒绝加载此前发布的所有更新。

解决办法是把EXUpdatesHasEmbeddedUpdate(iOS)与expo.modules.updates.HAS_EMBEDDED_UPDATE(Android)设为false,强制走远程更新。

6.2 附加请求头(Additional Headers)

若需要给 manifest 请求附加自定义请求头:

  • iOS:在EXUpdatesRequestHeaders键下以字典(map)形式配置;
  • Android:目前无法在 AndroidManifest.xml 中配置,需在MainApplication.java中调用UpdatesController.overrideConfiguration(Context, Map<String, Object>)方法,把requestHeaders传入。

6.3 构建与启用时机

默认行为:expo-updates 只在裸应用的 Release 构建中启用,Debug 构建从本地 Metro 服务器加载。

  • iOS Release 构建:Xcode → Product → Scheme → Edit Scheme,把 Run 配置的 Build Configuration 从 "Debug" 改为 "Release",再点击 Run;
  • Android Release 构建:在项目根目录执行react-native run-android --variant Release

6.4 在 Debug 模式下启用 expo-updates

某些场景(例如想打断点单步调试)需要 Debug 构建也启用 expo-updates:

  1. 先按上文配置"忽略嵌入式更新";
  2. 设置环境变量:
export EX_UPDATES_NATIVE_DEBUG=1
  1. iOS 额外两步:
    • 修改工程文件,把SKIP_BUNDLING替换为FORCE_BUNDLING,强制 Debug 与 Release 都打包应用 JS:
sed -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 构建)

  1. 通过 expo-modules-core 与UpdatesPackage(Android)或ExpoUpdatesReactDelegateHandler(iOS)初始化并启动 expo-updates;
  2. 读取原生构建配置,转换为UpdatesConfiguration/EXUpdatesConfig对象,同时初始化数据库、文件系统引用与错误恢复处理器;
  3. 用配置对象初始化并启动LoaderTask
  4. 若配置要求检查新更新,LoaderTasklaunchWaitMs为时长启动计时器;
  5. LoaderTask读取嵌入式 manifest,用SelectionPolicy决定是否通过EmbeddedLoader将嵌入式更新载入 SQLite——每次启动都必须执行,因为二进制随时可能被商店更新;
  6. 在其余动作之前,先启动一个DatabaseLauncher实例,选择并准备好一个"绝对安全可启动"的更新(逐一确认资源存在并取得磁盘路径);
  7. 若配置允许检查更新,LoaderTask在后台线程启动RemoteLoader:请求 manifest → 用SelectionPolicy决定是否入库 → 若入库则下载 SQLite 中缺失的资源;
  8. RemoteLoader完成后回调LoaderTask决策:
    • 计时器未超时:创建新的候选DatabaseLauncher选择刚下载的更新并交给UpdatesController启动;
    • 计时器已超时(旧更新已启动):向 JS 发送"新更新可用"事件,由应用决定何时调用reloadAsync()
  9. 全部完成后,后台运行Reaper进程清理数据库中的旧更新:保留当前运行更新、任何更新的版本以及最近的一个旧版本(作为回滚安全网)。

该流程体现了 guides/general.md 提到的核心架构原则:Loader类负责"装载进 SQLite"(写),Launcher类负责"从 SQLite 启动"(读),两者可以独立、同时、分离地运行;且"几乎任何情况都好过崩溃"——开发者依赖本模块不破坏用户对应用的信任,除错误恢复模式下由开发者代码引起的崩溃外,都应尽量避免崩溃。

7.2 下载更新的详细步骤

Loader装载更新的过程(远程或内置皆同):

  1. 通过子类方法加载 manifest(从 URL 下载或从应用包内读取);
  2. RemoteLoader检查数据库是否已有此更新且状态为READY——若是则直接触发成功回调,不再做任何事;
  3. 否则遍历 manifest 中的每个资源:检查是否 (a) 已在数据库中且 (b) 已存在于磁盘(约定"文件名相同即同一资源")。磁盘缺失则发起下载;
  4. 全部下载完成后,为"已在磁盘但不在数据库"的资源补写 SQLite 行(例如破坏性数据库迁移后可能出现);
  5. 若无错误且所有资源齐备,将更新标记为READY并触发成功回调;否则触发错误回调。

7.3 资源意外缺失的自愈机制

正常情况资源文件不会被系统清理,但若存储损坏或代码缺陷导致资源缺失,DatabaseLauncher会依次尝试:从嵌入式 manifest 中找回缺失资源并复制 → 从 SQLite 记录的 URL 下载 → 若启动资源缺失则触发失败回调,否则仍触发成功回调(期望更新可带缺失资源运行)。

7.4 嵌入式更新的特殊处理(Android 多分辨率资源)

Android 上通过require('./image.png')引用图片时,系统可能按屏幕密度映射到image.png/image@2x.png/image@3x.png。Expo 侧把每个文件当作独立资源,全部下载后才算READY;但嵌入式更新在 Android 上会按dpi目录存放于res中,运行时ResourcesAPI 只允许访问与当前设备匹配的分辨率资源,导致EmbeddedLoader无法复制其他密度的资源。为此这些更新被标记为特殊的EMBEDDED状态,直接从应用包启动(不经.expo-internal目录、不做资源覆盖),由 RN 直接从应用包读取资源——这是 expo-updates 极少把嵌入式更新区别对待的场景之一,启动时必须额外校验嵌入式更新仍是预期的那份(用户可能已更新构建)。

八、二进制补丁支持:BSPatch 与 BZip2

DEVELOPMENT.md 说明 expo-updates 内置了对资源应用二进制补丁(binary patches)的支持:

  • 采用修改版FreeBSD BSPatch(为保证移动端环境安全、不导致用户应用崩溃而做了改造);
  • BSPatch 依赖BZip2:iOS 链接系统自带 BZip2 库;Android 则在仓库中内置一份 BZip2 源码随原生构建一起编译,且只包含解压缩部分
  • 需要升级 BZip2 时,下载官方源码后将必要文件替换到packages/expo-updates/android/src/main/cpp/third-party/bzip2目录。

九、常见问题排查

如果 expo-updates 没有加载你期望的更新,优先检查两点(依据 DEVELOPMENT.md Troubleshooting 一节):

  1. 目标更新的创建时间是否晚于嵌入式 bundle,或者已配置忽略嵌入式更新;
  2. Expo.plist / AndroidManifest.xml 中配置的 SDK 或 runtime version 是否与所加载 manifest 中的一致——版本不匹配会被拒绝加载。

另外可结合两个调试入口:readLogEntriesAsync()读取模块日志定位失败原因;isEmergencyLaunch+emergencyLaunchReason判断是否发生了"紧急启动"回退并获取原因。

十、总结

expo-updates 把"远程更新"这件事抽象成一套清晰且可扩展的运行时模型:原子化的 update(manifest + assets)、客户端可独立排序的SelectionPolicy、读写分离的Loader/Launcher、以及遵循通用 Expo Update 协议的服务器交互。无论你选择官方 EAS Update 托管服务,还是基于协议自建服务器,掌握本文涉及的配置键、JS API、启动流程与调试手段,都能让远程更新在你的应用中稳定、可控地运行。进一步阅读仓库材料,可参考 DEVELOPMENT.md(开发与调试)、guides/general.md(设计哲学与假设)、guides/examples.md(完整启动/下载流程)以及 src/Updates.ts(JS 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 18:31:17

CANN/GE RT2.0动态Shape执行器特性分析

RT2.0 动态 Shape 执行器特性分析 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 Py…

作者头像 李华
网站建设 2026/9/10 18:28:34

用金字塔原则做项目计划:从目标到任务树的结构化方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 18:26:17

JAVA计算机毕设之基于 SpringBoot 的健身房运营管理平台的设计与实现 基于 SpringBoot 的健身教练与课程管理系统(完整前后端代码+说明文档+LW,调试定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华