Updating your App
编辑页面
For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.
本文档已于 2022 年 8 月归档,不会再收到任何进一步更新。请改用 EAS Update。了解更多
expo-updates 模块为在 React Native 应用中加载更新提供了客户端实现。更新允许你在不构建新的二进制文件的情况下,将新的 JavaScript 和资源部署到应用的现有构建版本中。
在本指南中,update 指的是一个单独的、原子性的更新,它可以由一个 JavaScript bundle、其他资源(例如图片或字体)以及关于该更新的元数据组成。
Setup
如果可以,我们强烈建议从一个已经安装了 expo-updates 库的样板项目开始,例如运行:npx create-expo-app -t bare-minimum。
要在现有的 bare workflow 应用中安装 expo-updates 模块,请按照安装说明进行操作。
此外,你还需要在某个服务器上托管你的更新及其对应的资源(JavaScript bundle、图片、字体等),以便已部署的客户端应用可以访问。expo-cli 提供了几个简单的选项:(1) expo export 会创建预构建的更新包,你可以将其上传到任意静态托管站点(例如 GitHub Pages);(2) expo publish 会将你的更新打包并部署到 Expo 的更新服务,这是我们提供的服务之一。
你也可以运行自己的服务器来托管更新,只要它符合 expo-updates 所期望的协议即可。你可以在下面阅读更多关于这些要求的内容。
Served update requirements
如果你使用的是
expo export或expo publish,可以跳过这一节,因为这些都会由系统为你处理!
expo-updates 的实现需要一个在构建时提供的单一 URL,它会向该 URL 发起新更新请求。这些请求可能会在用户在生产环境中启动应用时发生(取决于你的应用配置设置),也可能在应用调用 Updates.fetchUpdateAsync() 时发生。请求将带有以下 header:
这些请求的响应应该是一个 manifest JSON 对象,其中包含与请求的应用二进制文件兼容的最新更新的元数据。(关于兼容性的更多内容见下文。)该 manifest 至少应包含以下字段:
| Key | Type | Description |
|---|---|---|
releaseId | string | 一个唯一标识此更新的 UUID。 |
commitTime | string | 一个 JavaScript Date 字符串,表示此更新被提交/发布的时间。此字段用于比较两个更新,以确定哪个是最新的。 |
runtimeVersion | object | 一个包含 ios 和 android 键的对象,其对应值为此更新兼容的 Runtime Version。仅当未提供 sdkVersion 时必填。 |
sdkVersion | string | 此更新所使用的 Expo SDK 版本。仅当未提供 runtimeVersion 时必填。 |
bundleUrl | string | 指向该元数据所代表的 JavaScript bundle 的 URL。 |
bundledAssets | array | 作为此更新一部分需要下载的资源文件名数组。 |
assetUrlOverride | string | 用于解析 bundledAssets 中列出的所有文件名的基础 URL。 |
expo-updates 假定资源和 JavaScript bundle 的 URL 是不可变的;也就是说,如果它已经从某个 URL 下载过某个资源或 bundle,就不会尝试重新下载。因此,如果你在更新中更改了任何资源,必须将它们托管在不同的 URL 下。
如果你使用 expo export 来创建更新的预构建包,那么 ios-index.json 和 android-index.json 中的 manifest 满足这些要求。Expo 的更新服务(如果你使用 expo publish,则会发布到该服务)会在每次更新请求时动态创建这些 manifest 对象。
Update compatibility
更新的一个关键考量是 JavaScript bundle 与原生运行时之间的兼容性(即某个二进制文件中存在的原生模块及其导出的方法)。为了说明这一点,请看下面的例子:
假设你有一个现有的构建版本,即在生产环境中运行的 build A。build A 运行 JavaScript bundle 版本 1,并且一切正常。到了应用的下一个版本,你需要一些新功能,所以在开发中你安装了一个新的原生模块,比如 expo-media-library,并使用了它的一些函数。你创建了应用的 build B,其中包含 MediaLibrary 原生模块。build B 运行 JavaScript bundle 版本 2,并调用 MediaLibrary.getAlbumsAsync(),这会正常工作。
然而,如果你应用的 build A 获取到了作为更新的 JavaScript 版本 2 并尝试运行它,那么在调用 MediaLibrary.getAlbumsAsync() 方法时会报错,因为 build A 中不存在 MediaLibrary 原生模块。如果你的 JavaScript 没有捕获这个错误,它就会继续向上传播,导致你的应用崩溃,从而使 JavaScript 版本 2 无法在你应用的 build A 上使用。
我们需要一种方法来防止将 JavaScript 版本 2 部署到 build A,或者更一般地,控制哪些更新会部署到应用的特定构建版本。expo-updates 提供了两种控制方式:Runtime Version 和 Release Channels。
Runtime version
托管在你自己服务器上的更新可以使用一个称为 Runtime Version 的概念。Runtime Version 表示用于原生-JavaScript 接口的版本方案,或者说原生模块及其导出方法的版本方案。换句话说,每当你对原生模块层做出更改时,例如添加、删除或更新一个原生模块,你都应该递增 Runtime Version 编号。
某个二进制文件的 Runtime Version 应该在构建时进行配置(见下文的配置选项)。配置好的 Runtime Version 将包含在该二进制文件发送的每个更新请求的 header 中。服务器应使用这个 header 来选择一个合适的更新进行响应。
某个更新所期望的 Runtime Version 也必须作为一个字段(runtimeVersion)提供在返回给 expo-updates 的 manifest 中。expo-updates 会跟踪它已下载的所有更新的 Runtime Version;这样一来,如果用户通过 App Store 更新了应用二进制文件,它就不会尝试运行一个之前已下载但现在已不兼容的更新。
Release channels
由于当前 Expo 更新服务的实现高度依赖 SDK 版本(一个 managed-workflow 概念),如果你使用的是 expo publish,你还不能使用 Runtime Version 来管理更新与二进制文件的兼容性。相反,你可以使用release channels。一个典型的工作流是:为你构建的每一个新二进制文件创建一个新的 release channel(或者至少为每一个在原生-JavaScript 接口上存在不兼容变更的新二进制文件创建一个新的 release channel),方法是使用 expo publish --release-channel <channel-name> 发布到该新的 release channel。创建完一个配置了此 release channel 名称的构建版本后,只要未来的更新与该构建版本保持兼容,你就可以继续将这些更新发布到同一个 release channel。只有配置为使用该 release channel 的构建版本才会收到这些更新。
Statically hosted updates
由于 expo-updates 在请求中发送的 header 不会影响静态托管的更新(例如由 expo export 创建的更新包),因此你必须将不兼容的更新托管在不同的静态 URL 上,以控制兼容性。
Embedding assets
除了从远程服务器加载更新之外,安装了 expo-updates 的应用还包含将更新嵌入应用二进制文件中的必要能力。这一点至关重要,它能确保你的应用在安装后立即就能让所有用户离线启动,而无需互联网连接。
当你为应用创建发布构建时,构建过程会将你的 JavaScript 源代码打包成一个压缩后的 bundle,并将其嵌入二进制文件中,同时还会包含你的应用通过 require、import 或在 app.json 中使用的任何其他资源。expo-updates 会在每个平台上额外包含一个脚本,用于嵌入一些关于这些嵌入式资源的附加元数据——也就是该更新的一个最小化 manifest JSON 对象。
Including assets in updates
你在 JavaScript 源码中导入的资源,也可以作为已发布更新的一部分被原子性下载。expo-updates 不会认为某个更新已经“准备好”,也不会启动该更新,除非它已经下载了所有必需的资源。
如果你在项目中使用了 expo-asset(如果你安装了 expo 包,默认会包含),你可以通过在 app.json 中使用 assetBundlePatterns 键来提供项目目录中的路径列表,从而控制哪些导入的资源会作为这个原子更新的一部分被包含进去:
"assetBundlePatterns": [ "**/*" // 或 "assets/images/*" 等。 ],
路径与给定模式匹配的资源会在使用它们的更新启动之前,由客户端预先下载。如果你有某个资源希望在运行时延迟下载,而不是在 JavaScript 被求值之前下载,你可以使用 assetBundlePatterns 将其排除,同时仍然在 JavaScript 源码中导入它。
请注意,要成功使用 expo-asset,在创建 JavaScript bundle 时,你必须使用 --assetPlugins 选项,向 Metro bundler 提供 node_modules/expo-asset/tools/hashAssetFiles 插件。如果你使用 expo export 或 expo publish 来创建更新,这一步会自动为你完成。
配置选项
有一些构建时配置选项可用于控制 expo-updates 库的各种行为。你可以设置应用托管的 URL、设置兼容性/版本信息,并选择应用是否应在启动时自动更新。
在 iOS 上,这些属性作为键设置在 Expo.plist 中;在 Android 上,则作为 meta-data 标签设置在 AndroidManifest.xml 中,位置紧邻安装期间添加的标签。
在 Android 上,你也可以通过将一个 Map 作为 UpdatesController.initialize() 的第二个参数来在运行时定义这些属性。如果提供了该参数,此 Map 中的值将覆盖 AndroidManifest.xml 中指定的任何值。在 iOS 上,你可以在调用 start 或 startAndShowLaunchScreen 之前的任何时刻,通过调用 [UpdatesController.sharedInstance setConfiguration:] 在运行时设置这些属性,而该字典中的值将覆盖 Expo.plist。
| iOS plist/dictionary key | Android Map key | Android meta-data name | Default | Required? |
|---|---|---|---|---|
EXUpdatesEnabled | enabled | expo.modules.updates.ENABLED | true | ❌ |
是否启用更新。将其设置为 false 会禁用所有更新功能、所有模块方法,并强制应用使用打包进应用二进制文件中的 manifest 和资源来加载。
| iOS plist/dictionary key | Android Map key | Android meta-data name | Default | Required? |
|---|---|---|---|---|
EXUpdatesURL | updateUrl | expo.modules.updates.EXPO_UPDATE_URL | (none) | ✅ |
应用应从中检查更新的远程服务器 URL。向此 URL 发出的请求应返回一个有效的 manifest 对象,描述最新可用更新,以告诉 expo-updates 如何获取构成更新的 JS bundle 和其他资源。(示例:对于使用 expo publish 发布的应用,此 URL 将是 https://exp.host/@username/slug。)
| iOS plist/dictionary key | Android Map key | Android meta-data name | Default | Required? |
|---|---|---|---|---|
EXUpdatesSDKVersion | sdkVersion | expo.modules.updates.EXPO_SDK_VERSION | (none) | (exactly one of sdkVersion or runtimeVersion is required) |
在 manifest 请求中作为 Expo-SDK-Version 标头发送的 SDK 版本字符串。对于托管在 Expo 服务器上的应用是必需的。
| iOS plist/dictionary key | Android Map key | Android meta-data name | Default | Required? |
|---|---|---|---|---|
EXUpdatesRuntimeVersion | runtimeVersion | expo.modules.updates.EXPO_RUNTIME_VERSION | (none) | (exactly one of sdkVersion or runtimeVersion is required) |
在 manifest 请求中作为 Expo-Runtime-Version 标头发送的 Runtime Version 字符串。
| iOS plist/dictionary key | Android Map key | Android meta-data name | Default | Required? |
|---|---|---|---|---|
EXUpdatesReleaseChannel | releaseChannel | expo.modules.updates.EXPO_RELEASE_CHANNEL | default | ❌ |
在 manifest 请求中作为 Expo-Release-Channel 标头发送的发布通道字符串。
| iOS plist/dictionary key | Android Map key | Android meta-data name | Default | Required? |
|---|---|---|---|---|
EXUpdatesCheckOnLaunch | checkOnLaunch | expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH | ALWAYS | ❌ |
expo-updates 应在应用启动时自动检查(如果存在则下载)更新的条件。可能的值为 ALWAYS、NEVER(如果你希望完全通过该模块的 JS API 控制更新)、WIFI_ONLY(如果你希望应用仅在设备启动时连接到未计费的 Wi-Fi 网络时自动下载更新),或 ERROR_RECOVERY_ONLY(如果你希望应用仅在启动时遇到致命错误时自动下载更新)。
无论此设置的值如何,只要更新已启用,你的应用始终可以使用 JS API 在应用运行时手动检查并在后台下载更新。
| iOS plist/dictionary key | Android Map key | Android meta-data name | Default | Required? |
|---|---|---|---|---|
EXUpdatesLaunchWaitMs | launchWaitMs | expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS | 0 | ❌ |
expo-updates 在尝试下载更新时,应延迟应用启动并停留在启动画面上的毫秒数,然后再回退到之前下载的版本。将其设置为 0 将使应用始终使用先前下载的更新启动,并带来尽可能快的应用启动速度。
下面解释了一些常见的配置模式:
自动更新
默认情况下,当用户从关闭状态打开应用时,expo-updates 会立即使用之前下载的(或内嵌的)更新启动应用。它还会在后台异步检查更新,并尝试获取最新发布版本。如果有新更新可用,expo-updates 会尝试下载它,并通过 事件 向正在运行的 JavaScript 通知成功或失败。新获取到的更新会在用户下次滑掉并重新打开应用时启动;如果你希望更早运行它,可以在合适的时间在应用代码中调用 Updates.reloadAsync。
你也可以通过使用 launchWaitMs 设置,将 expo-updates 配置为在用户打开应用时等待特定时长再启动。如果在这段时间内可以下载到新更新,那么新更新会立即启动,而不是等用户滑掉并重新打开应用。(不过请注意,如果用户的网络连接较慢,你的应用可能会在启动画面上延迟长达 launchWaitMs 毫秒,因此除非用户每次启动都必须获得最新更新,否则我们建议对该设置保持保守。)如果没有可用更新,一旦 expo-updates 能够确定这一点,之前下载的更新就会立即启动。
如果你希望这种自动更新行为仅在用户处于 Wi-Fi 连接时发生,可以将 checkOnLaunch 设置为 WIFI_ONLY。
手动更新
也可以关闭这些自动更新,而完全在你的 JS 代码中控制更新。如果你希望围绕获取更新实现一些自定义逻辑,这会很有用(例如,仅在用户在 UI 中执行特定操作时才获取更新)。
将 checkOnLaunch 设置为 NEVER 将阻止 expo-updates 在每次应用启动时自动获取最新更新。此时只会加载你 bundle 的最近缓存版本。
然后,你可以使用此库中包含的 expo-updates 模块来下载新更新,并在适当时通知用户并重新加载体验。
try { const update = await Updates.checkForUpdateAsync(); if (update.isAvailable) { await Updates.fetchUpdateAsync(); // ... 通知用户有更新 ... Updates.reloadAsync(); } } catch (e) { // 处理或记录错误 }