更新你的应用


本文档于2022年8月归档,不会再有任何更新。请改用EAS Update。了解更多

expo-updates 模块提供了一个客户端实现,用于在 React Native 应用中加载更新。更新允许你在不重新构建新的安装包的情况下,将新的 JavaScript 和资源部署到现有的应用版本中。

🌐 The expo-updates module provides a client-side implementation for loading updates in React Native apps. Updates allow you to deploy new JavaScript and assets to existing builds of your app without building a new binary.

在本指南中,更新指的是一次单一的、原子性的更新,这可能包括一个 JavaScript 打包包、其他资源(如图片或字体),以及关于更新的元数据。

🌐 In this guide, an update refers to a single, atomic update, which may consist of a JavaScript bundle, other assets (such as images or fonts), and metadata about the update.

设置

🌐 Setup

如果可能,我们强烈建议从已经安装了 expo-updates 库的模板项目开始,例如,通过运行:npx create-expo-app -t bare-minimum

🌐 If possible, we highly recommend starting with a boilerplate project that has the expo-updates library already installed, for example, by running: npx create-expo-app -t bare-minimum.

要在现有的裸工作流应用中安装 expo-updates 模块,请按照安装说明操作。

🌐 To install the expo-updates module in an existing bare workflow app, follow the installation instructions.

此外,你需要将你的更新及其相关资源(JavaScript 包、图片、字体等)托管在某个已部署客户端应用可以访问的服务器上。expo-cli 提供了几种简单的选项: (1) expo export 创建预构建的更新包,你可以将其上传到任何静态托管网站(例如,GitHub Pages);(2) expo publish 打包并将你的更新部署到 Expo 的更新服务中,这也是我们提供的服务的一部分。

🌐 Additionally, you'll need to host your updates and their respective assets (JavaScript bundles, images, fonts, and so on) on a server somewhere that deployed client apps can access. expo-cli provides a couple of easy options for this: (1) expo export creates prebuilt update packages that you can upload to any static hosting site (for example, GitHub Pages), and (2) expo publish packages and deploys your updates to Expo's updates service, which is part of the services we offer.

你也可以运行自己的服务器来托管更新,只要它符合 expo-updates 所期望的协议。你可以在下面阅读有关这些要求的更多信息。

🌐 You can also run your own server to host your updates, provided it conforms to the protocol expo-updates expects. You can read more about these requirements below.

满足更新要求

🌐 Served update requirements

如果你正在使用 expo exportexpo publish,你可以跳过本节内容,因为系统会为你处理!

expo-updates 实现需要一个单一的 URL(在构建时提供),它将向该 URL 发出获取新更新的请求。这些请求可能会在用户在生产环境中启动你的应用时发生(取决于你的应用配置设置),以及当你的应用调用 Updates.fetchUpdateAsync() 时发生。请求将会带有以下头信息:

🌐 The expo-updates implementation requires a single URL (provided at build-time) to which it will make requests for new updates. These requests may happen when users launch your app in production (depending on your app's configuration settings) and when your app calls Updates.fetchUpdateAsync(). Requests will be sent with the following headers:

对这些请求的响应应当是一个清单 JSON 对象,其中包含与请求的应用二进制文件兼容的最新更新的元数据。(兼容性相关内容见下文。)清单应至少包含以下字段:

🌐 The response to these requests should be a manifest JSON object with metadata about the latest update that's compatible with the requesting app binary. (More on compatibility below.) The manifest should have at least the following fields:

KeyTypeDescription
releaseIdstringA UUID uniquely identifying this update.
commitTimestringA JavaScript Date string representing the time this update was committed/published. This is used to compare two updates to determine which is newest.
runtimeVersionobjectAn object with keys ios and android whose corresponding values are the Runtime Version this update is compatible with. Required only if sdkVersion is not provided.
sdkVersionstringThe Expo SDK version this update uses. Required only if runtimeVersion is not provided.
bundleUrlstringA URL pointing to the JavaScript bundle this metadata represents.
bundledAssetsarrayAn array of asset filenames to download as part of this update.
assetUrlOverridestringBase URL from which to resolve all of the filenames listed in bundledAssets.

expo-updates 假设资源和 JavaScript 包的 URL 是不可变的;也就是说,如果它已经在某个 URL 下载过某个资源或包,它不会尝试重新下载。因此,如果你在更新中更改了任何资源,你必须将它们托管在不同的 URL 上。

如果你使用 expo export 来创建你的更新的预构建包,ios-index.jsonandroid-index.json 中的清单满足这些要求。如果你使用 expo publish 发布,Expo 的更新服务会在每次更新请求时动态创建这些清单对象。

🌐 If you use expo export to create a prebuilt package of your update, the manifests in ios-index.json and android-index.json satisfy these requirements. Expo's update service, which you publish to if you use expo publish, dynamically creates these manifest objects upon each update request.

更新兼容性

🌐 Update compatibility

更新时一个关键的考虑因素是 JavaScript 包与本地运行时之间的兼容性(给定二进制文件中存在的本地模块及其导出的方法)。举例来说,考虑以下示例:

🌐 A critical consideration with updates is compatibility between the JavaScript bundle and the native runtime (the native modules present in a given binary and the methods they export). To illustrate, consider the following example:

假设你有一个现有的构建版本 A 的应用正在生产环境中运行。构建 A 运行的是 JavaScript 打包版本 1,一切运行顺利。在应用的下一个版本中,你需要一些新功能,所以在开发中你安装了一个新的原生模块,比如 expo-media-library,并使用了它的一些功能。你创建了应用的构建版本 B,其中包含 MediaLibrary 原生模块。构建 B 运行的是调用 MediaLibrary.getAlbumsAsync() 的 JavaScript 打包版本 2,并且这能正常工作。

🌐 Say you have an existing build, build A, of your app running in production. Build A runs JavaScript bundle version 1 and everything works smoothly. In the next version of your app, you need some new functionality, so in development you install a new native module like expo-media-library, and use some of its functions. You create build B of your app which includes the MediaLibrary native module. Build B runs JavaScript bundle version 2 which calls MediaLibrary.getAlbumsAsync(), and this works.

然而,如果你的应用的 A 版本获取了 JavaScript 版本 2 作为更新并尝试运行它,它将在调用 MediaLibrary.getAlbumsAsync() 方法时出错,因为 A 版本中不存在 MediaLibrary 原生模块。如果你的 JavaScript 没有捕获这个错误,它将向上传播,你的应用会崩溃,使 JavaScript 版本 2 在应用的 A 版本上无法使用。

🌐 However, if build A of your app fetches JavaScript version 2 as an update and tries to run it, it will error on the MediaLibrary.getAlbumsAsync() method call because the MediaLibrary native module is not present in build A. If your JavaScript doesn't catch this error, it will propagate and your app will crash, rendering JavaScript version 2 unusable on build A of your app.

我们需要某种方法来防止 JavaScript 版本 2 被部署到构建 A——或者,更一般地说,控制哪些更新被部署到应用的特定构建中。expo-updates 提供了两种控制方法:运行时版本和发布渠道。

🌐 We need some way of preventing JavaScript version 2 from being deployed to build A - or, in general, controlling which updates are deployed to specific builds of your app. expo-updates provides two ways to control this: Runtime Version and Release Channels.

运行时版本

🌐 Runtime version

托管在你自己服务器上的更新可以使用一个称为运行时版本的概念。运行时版本表示本地 JavaScript 接口或本地模块及其导出方法的版本控制方案。换句话说,每当你对本地模块层进行更改,例如添加、移除或更新本地模块时,你都需要增加运行时版本号。

🌐 Updates hosted on your own server can make use of a concept called Runtime Version. Runtime Version represents a versioning scheme for the native-JavaScript interface, or the native modules and the methods they export. In other words, anytime you make a change to your native module layer, such as adding, removing, or updating a native module, you would increment the Runtime Version number.

特定二进制文件的运行时版本应在构建时配置(见下文配置选项)。配置的运行时版本将包含在从该二进制文件发送的每个更新请求的头部中。服务器应使用此头部来选择适当的更新进行响应。

🌐 The Runtime Version of a particular binary should be configured at build time (see Configuration Options below). The configured Runtime Version will be included in the header of every update request sent from that binary. The server should use this header to select an appropriate update to serve in response.

给定更新所期望的运行时版本也必须作为一个字段(runtimeVersion)包含在返回给 expo-updates 的清单中。expo-updates 会跟踪它下载的所有更新的运行时版本;这样,如果用户通过 App Store 更新他们的应用二进制文件,它将不会尝试运行之前下载的、现在不兼容的更新。

🌐 The Runtime Version expected by a given update must also be provided as a field (runtimeVersion) in the manifest returned to expo-updates. expo-updates keeps track of the Runtime Version of all updates it has downloaded; this way, if a user updates their app binary through the App Store, it will not attempt to run a previously downloaded and newly incompatible update.

发布渠道

🌐 Release channels

由于当前 Expo 更新服务的实现高度依赖于 SDK 版本(一个管理工作流的概念),如果你正在使用 expo publish,你暂时无法使用运行时版本来管理更新和二进制文件的兼容性。相反,你可以使用 发布通道。一个典型的工作流程是为每一个新构建的二进制文件(或至少每个在原生与 JavaScript 接口中有不兼容更改的二进制文件)创建一个新的发布通道,通过 expo publish --release-channel <channel-name> 发布到该新发布通道。配置好该发布通道名称并创建构建后,只要后续更新与该构建保持兼容,你可以继续将这些更新发布到同一发布通道。只有配置为使用该发布通道的构建才会接收到这些更新。

🌐 Because the current implementation of the Expo updates service relies heavily on SDK version (a managed-workflow concept), if you're using expo publish you cannot yet use Runtime Version to manage compatibility of your updates and binaries. Instead, you can use release channels. A typical workflow would be to create a new release channel for each new binary you build (or at least every new binary with an incompatible change in the native-JavaScript interface) by publishing to that new release channel with expo publish --release-channel <channel-name>. After creating a build with this release channel name configured, you can continue to publish future updates to this same release channel as long as they remain compatible with that build. Only builds that were configured to use that release channel will receive those updates.

静态托管更新

🌐 Statically hosted updates

由于 expo-updates 在请求中发送的头信息不会影响静态托管的更新(例如由 expo export 创建的更新包),因此必须在不同的静态 URL 上托管不兼容的更新,以控制兼容性。

🌐 Since headers sent in requests by expo-updates do not affect statically hosted updates (such as update packages created by expo export), you must host incompatible updates at different static URLs to control compatibility.

嵌入资源

🌐 Embedding assets

除了从远程服务器加载更新外,安装了 expo-updates 的应用还具备从应用二进制文件中加载内嵌更新的必要功能。这对于确保你的应用能够在安装后立即离线启动、无需互联网连接对所有用户都至关重要。

🌐 In addition to loading updates from remote servers, apps with expo-updates installed also include the necessary capability to load updates embedded in the app binary. This is critical to ensure that your app can launch offline for all users immediately upon installation, without needing an internet connection.

当你制作应用的发布版本时,构建过程会将你的 JavaScript 源代码打包成一个压缩包,并将其嵌入到二进制文件中,同时包含应用导入的其他资源(通过 requireimport 导入,或在 app.json 中使用)。expo-updates 会在每个平台上包含一个额外的脚本,用于嵌入一些关于已嵌入资源的附加元数据——即用于更新的最小化清单 JSON 对象。

🌐 When you make a release build of your app, the build process will bundle your JavaScript source code into a minified bundle and embed this in the binary, along with any other assets your app imports (with require or import or used in app.json). expo-updates includes an extra script on each platform to embed some additional metadata about the embedded assets -- namely, a minimal manifest JSON object for the update.

在更新中包含资源

🌐 Including assets in updates

你在 JavaScript 源代码中导入的资源也可以作为发布更新的一部分被原子下载。expo-updates 不会将更新视为“就绪”,也不会启动更新,除非它已下载所有必需的资源。

🌐 Assets that you import in your JavaScript source can also be atomically downloaded as part of a published update. expo-updates will not consider an update "ready" and will not launch the update unless it has downloaded all required assets.

如果你在项目中使用 expo-asset(如果安装了 expo 包,则默认包含),你可以通过在 app.json 中使用 assetBundlePatterns 键来提供项目目录中的路径列表,从而控制哪些导入的资源将作为此原子更新的一部分包含进来:

🌐 If you use expo-asset in your project (included by default if you have the expo package installed), you can control which imported assets will be included as part of this atomic update by using the assetBundlePatterns key in app.json to provide a list of paths in your project directory:

"assetBundlePatterns": [ "**/*" // or "assets/images/*", etc. ],

路径与给定模式匹配的资源将在使用它们的更新启动之前被客户端预先下载。如果你有一个资源应该在运行时懒加载,而不是在你的 JavaScript 被执行之前下载,你可以使用 assetBundlePatterns 将其排除,同时仍在你的 JavaScript 源代码中导入它。

🌐 Assets with paths matching the given patterns will be pre-downloaded by clients before the update that uses them will launch. If you have an asset that should be lazily downloaded at runtime rather than before your JavaScript is evaluated, you can use assetBundlePatterns to exclude it while still importing it in your JavaScript source.

请注意,要成功使用 expo-asset,必须在创建 JavaScript 包时使用 --assetPlugins 选项向 Metro 打包器提供 node_modules/expo-asset/tools/hashAssetFiles 插件。如果你使用 expo exportexpo publish 来创建更新,这将自动为你完成。

🌐 Note that to use expo-asset successfully, you must use the --assetPlugins option to provide the Metro bundler with the node_modules/expo-asset/tools/hashAssetFiles plugin when you create your JavaScript bundle. If you use expo export or expo publish to create your update, this will be done automatically for you.

配置选项

🌐 Configuration options

有一些构建时配置选项可用于控制 expo-updates 库的各种行为。你可以设置应用托管的 URL,设置兼容性/版本信息,以及选择应用是否在启动时自动更新。

🌐 Some build-time configuration options are available to control various behaviors of the expo-updates library. You can set the URL where your app is hosted, set compatibility/version information, and choose whether your app should update automatically on launch.

在 iOS 上,这些属性作为键设置在 Expo.plist 中,而在 Android 上则作为 meta-data 标签设置在 AndroidManifest.xml 中,位于安装时添加的标签旁边。

🌐 On iOS, these properties are set as keys in Expo.plist and on Android as meta-data tags in AndroidManifest.xml, adjacent to the tags added during installation.

在 Android 上,你也可以通过将 Map 作为 UpdatesController.initialize() 的第二个参数来在运行时定义这些属性。如果提供,这个 Map 中的值将覆盖 AndroidManifest.xml 中指定的任何值。在 iOS 上,你可以通过在调用 startstartAndShowLaunchScreen 之前的任何时候调用 [UpdatesController.sharedInstance setConfiguration:] 来在运行时设置这些属性,这个字典中的值将覆盖 Expo.plist。

🌐 On Android, you may also define these properties at runtime by passing a Map as the second parameter of UpdatesController.initialize(). If provided, the values in this Map will override any values specified in AndroidManifest.xml. On iOS, you may set these properties at runtime by calling [UpdatesController.sharedInstance setConfiguration:] at any point before calling start or startAndShowLaunchScreen, and the values in this dictionary will override Expo.plist.

iOS plist/字典键Android 映射键Android 元数据名称默认值是否必填
EXUpdatesEnabledenabledexpo.modules.updates.ENABLEDtrue

是否启用了更新。将此设置为 false 会禁用所有更新功能、所有模块方法,并强制应用使用打包到应用二进制文件中的清单和资源进行加载。

🌐 Whether updates are enabled. Setting this to false disables all update functionality, all module methods, and forces the app to load with the manifest and assets bundled into the app binary.

iOS plist/字典键Android Map 键Android 元数据名称默认值是否必填?
EXUpdatesURLupdateUrlexpo.modules.updates.EXPO_UPDATE_URL(无)

应用应检查更新的远程服务器的 URL。对该 URL 的请求应返回一个有效的清单对象,用于提供最新可用更新的信息,并告诉 expo-updates 如何获取包含更新的 JS 包和其他资源。(例如:对于使用 expo publish 发布的应用,此 URL 将是 https://exp.host/@username/slug。)

🌐 The URL to the remote server where the app should check for updates. A request to this URL should return a valid manifest object for the latest available update that tells expo-updates how to fetch the JS bundle and other assets that comprise an update. (Example: for apps published with expo publish, this URL would be https://exp.host/@username/slug.)

iOS plist/字典键Android Map 键Android 元数据名称默认值是否必填
EXUpdatesSDKVersionsdkVersionexpo.modules.updates.EXPO_SDK_VERSION(无)(必须填写 sdkVersionruntimeVersion 中的一个)

在清单请求中通过 Expo-SDK-Version 头发送的 SDK 版本字符串。对于托管在 Expo 服务器上的应用,这是必需的。

🌐 The SDK version string to send under the Expo-SDK-Version header in the manifest request. Required for apps hosted on Expo's server.

iOS plist/字典键Android Map 键Android 元数据名称默认值是否必填
EXUpdatesRuntimeVersionruntimeVersionexpo.modules.updates.EXPO_RUNTIME_VERSION(无)(必须填写 sdkVersionruntimeVersion 中的一个)

在清单请求中,需要在 Expo-Runtime-Version 头下发送的运行时版本字符串。

🌐 The Runtime Version string to send under the Expo-Runtime-Version header in the manifest request.

iOS plist/字典键Android Map 键Android 元数据名称默认值必需?
EXUpdatesReleaseChannelreleaseChannelexpo.modules.updates.EXPO_RELEASE_CHANNELdefault

在清单请求中,需要在 Expo-Release-Channel 头下发送的发布渠道字符串。

🌐 The release channel string to send under the Expo-Release-Channel header in the manifest request.

iOS plist/字典键Android Map 键Android 元数据名称默认值是否必填
EXUpdatesCheckOnLaunchcheckOnLaunchexpo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCHALWAYS

expo-updates 应在应用启动时自动检查更新(如果存在则下载)的条件。可能的值有 ALWAYSNEVER(如果你只想通过这个模块的 JS API 来控制更新)、WIFI_ONLY(如果你希望应用仅在设备启动时连接到非计量 Wi-Fi 时自动下载更新)、或者 ERROR_RECOVERY_ONLY(如果你希望应用仅在启动时遇到致命错误时自动下载更新)。

🌐 The condition under which expo-updates should automatically check for (and download, if one exists) an update upon app launch. Possible values are ALWAYS, NEVER (if you want to exclusively control updates via this module's JS API), WIFI_ONLY (if you want the app to automatically download updates only if the device is on an unmetered Wi-Fi connection when it launches), or ERROR_RECOVERY_ONLY (if you want the app to automatically download updates only if it encounters a fatal error when launching).

无论此设置的值如何,只要启用了更新,你的应用始终可以在应用运行时使用 JS API 在后台手动检查和下载更新。

🌐 Regardless of the value of this setting, as long as updates are enabled, your app can always use the JS API to manually check for and download updates in the background while your app is running.

iOS plist/字典键Android Map 键Android 元数据名称默认值必填?
EXUpdatesLaunchWaitMslaunchWaitMsexpo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS0

毫秒数 expo-updates 应延迟应用启动,并在尝试下载更新时停留在启动画面上,然后再回退到之前下载的版本。将此设置为 0 将导致应用始终使用之前下载的更新启动,并且会实现应用最快的启动速度。

🌐 The number of milliseconds expo-updates should delay the app launch and stay on the splash screen while trying to download an update, before falling back to a previously downloaded version. Setting this to 0 will cause the app to always launch with a previously downloaded update and will result in the fastest app launch possible.

一些常见的配置模式解释如下:

🌐 Some common configuration patterns are explained below:

自动更新

🌐 Automatic updates

默认情况下,当用户从关闭状态打开应用时,expo-updates 会立即使用之前下载的(或嵌入的)更新启动你的应用。同时,它还会在后台异步检查更新,并尝试获取最新发布的版本。如果有新更新可用,expo-updates 会尝试下载,并通过 事件 通知正在运行的 JavaScript 代码更新成功或失败。新获取的更新将在用户下次关闭并重新打开应用时启动;如果你希望更早运行更新,可以在应用代码中适当的时候调用 Updates.reloadAsync

🌐 By default, expo-updates will immediately launch your app with a previously downloaded (or embedded) update when a user opens your app from being closed. It will additionally check for updates asynchronously in the background, and will try to fetch the latest published version. If a new update is available, expo-updates will try to download it and notify the running JavaScript of its success or failure using events. A newly fetched update will be launched next time the user swipes closed and reopens the app; if you want to run it sooner, you can call Updates.reloadAsync in your application code at an appropriate time.

你还可以通过使用 launchWaitMs 设置配置 expo-updates 在用户打开应用时等待特定的时间再启动。如果在此时间内可以下载到新更新,新的更新将立即启动,而无需等待用户滑动关闭并重新打开应用。(但请注意,如果用户的网络连接较慢,你的应用在启动屏幕上可能会延迟长达 launchWaitMs 毫秒,因此除非对用户在每次启动时获取最新更新至关重要,否则建议对此设置保持保守。)如果没有可用更新,先前下载的更新将在 expo-updates 能够确定这一点后立即启动。

🌐 You may also configure expo-updates to wait a specific amount of time to launch when a user opens the app by using the launchWaitMs setting. If a new update can be downloaded within this time, the new update will be launched right away, rather than waiting for the user to swipe closed and reopen the app. (Note, however, that if users have a slow network connection, your app can be delayed on the launch screen for as many milliseconds as launchWaitMs, so we recommend being conservative with this setting unless it's critically important for users to have the most recent update on each launch.) If no update is available, a previously downloaded update will be launched as soon as expo-updates is able to determine this.

如果你希望这个自动更新行为仅在用户连接 Wi-Fi 时发生,可以将 checkOnLaunch 设置为 WIFI_ONLY

🌐 If you want this automatic update behavior to occur only when your users are on a Wi-Fi connection, you can set the checkOnLaunch setting to WIFI_ONLY.

手动更新

🌐 Manual updates

也可以关闭这些自动更新,而完全在你的 JS 代码中控制更新。如果你希望在获取更新时有一些自定义逻辑(例如,仅在用户在你的界面中执行特定操作时),这是很理想的做法。

🌐 It's also possible to turn off these automatic updates, and to instead control updates entirely within your JS code. This is desirable if you want some custom logic around fetching updates (for example, only when users take a specific action in your UI).

checkOnLaunch 设置为 NEVER 将阻止 expo-updates 在每次启动应用时自动获取最新更新。只会加载最近缓存的 bundle 版本。

🌐 Setting checkOnLaunch to NEVER will prevent expo-updates from automatically fetching the latest update every time your app is launched. Only the most recent cached version of your bundle will be loaded.

然后,你可以使用此库中包含的 expo-updates 模块来下载新更新,并在适当的情况下通知用户并重新加载体验。

🌐 You can then use the expo-updates module included with this library to download new updates and, if appropriate, notify the user and reload the experience.

try { const update = await Updates.checkForUpdateAsync(); if (update.isAvailable) { await Updates.fetchUpdateAsync(); // ... notify user of update ... Updates.reloadAsync(); } } catch (e) { // handle or log error }