在现有原生应用中使用 EAS Update

编辑页面

了解如何将 EAS Update 集成到你现有的原生 Android 和 iOS 应用中,以启用空中更新。


For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.

如果你的项目是一个 greenfield React Native app — 从一开始主要使用 React Native 构建,并且应用的入口点就是 React Native,那么请跳过本指南,直接继续阅读 Get started with EAS Update

本指南说明如何在现有原生应用中集成 EAS Update,这类应用有时也称为 brownfield app。本文假设你使用的是 Expo SDK 52 或更高版本,以及 React Native 0.76 或更高版本。

较旧的 Expo SDK 和 React Native 版本不提供相关说明。对于旧版本集成的额外手把手支持,仅可为企业客户提供(联系我们)。

前提条件

以下说明可能并不适用于所有项目。将 EAS Update 集成到现有项目中的具体方式高度依赖于你的应用细节,因此你可能需要根据自己的独特配置对这些步骤进行调整。如果遇到问题,请在 GitHub 上创建 issue 或提交 pull request 来建议改进本指南。

你应该已经有一个 brownfield 原生项目,其中安装并配置了 React Native,且能够渲染根视图。如果你还没有完成这些,请先阅读 React Native 文档中的 Integration with Existing Apps 指南,然后在完成步骤后再回来继续。

  • 你的应用必须使用最新的 Expo SDK 版本及其支持的 React Native 版本
  • 从应用中移除任何其他更新库集成,例如 react-native-code-push,并确保你的应用在所支持的平台上以 debug 和 release 模式都能成功编译和运行。
  • 项目中必须安装并配置对 Expo modules(通过 expo 包)的支持。了解更多
  • 你的 metro.config.js 必须扩展 expo/metro-config
  • 你的 babel.config.js 必须扩展 babel-preset-expo
  • 如果项目支持 Android,则命令 npx expo export -p android 必须能在项目中成功运行;如果项目支持 iOS,则 npx expo export -p ios 也必须能成功运行。

安装和基础配置

按照 Get started with EAS Update 指南中的第 1、2、3、4 步操作。

完成后,你将已经安装并通过 eas-cli 完成身份验证,已将 expo-updates 安装到项目中,已初始化关联的 EAS 项目,并为原生项目添加了基础配置。

取消自动设置

下一步是禁用 expo-updates 的默认行为,使其不再以适用于 greenfield React Native 项目的方式自动完成自身设置。

在 Android 上禁用自动设置

修改 android/gradle.properties,设置用于禁用自动更新初始化的属性,如下例所示:

在 iOS 上禁用自动设置

通过向 CocoaPods 安装传入环境变量来禁用自动更新初始化。

Terminal
EX_UPDATES_CUSTOM_INIT=1 npx pod-install

设置 React Native 应用以在加载发布版 bundle 时使用 expo-updates

下一步是将 expo-updates 集成到你的 Android 和 iOS 项目中,这样你的应用在 release 构建中会使用 expo-updates 作为 JavaScript 代码来源。

将 expo-updates 集成到 React Native 打包流程中

  1. 确保你的 Metro 配置扩展了 Expo 配置,如下例所示:

    metro.config.js
    // 了解更多 https://docs.expo.dev/guides/customizing-metro const { getDefaultConfig } = require('expo/metro-config'); /** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname); // eslint-disable-line no-undef // 根据项目需要直接修改 "config" // 以添加任何自定义更改 module.exports = config;
  2. 如果你使用自定义入口点,请务必在那里包含 Expo 初始化。这可确保 Expo 库(包括 expo-updates)都能正确初始化。下面有两个示例:

    First Custom Entry Point file example
    // Expo 建议使用 registerRootComponent()。 // 它会将组件注册到 react-native 的 AppRegistry, // 并执行所有必需的 Expo 初始化 // (包括 expo-updates 设置) import App from './App'; import { registerRootComponent } from 'expo'; registerRootComponent(App);
    Second custom entry point file example
    // 如果你需要保留一个直接使用 AppRegistry 的现有入口点, // 你需要在注册应用之前先添加对 Expo 初始化的调用, // 如下所示。 import App from './App'; import 'expo/src/Expo.fx'; import { AppRegistry } from 'react-native'; function getApp() { return <App />; } AppRegistry.registerComponent('App', () => getApp());

在 Android 上集成 expo-updates

以下说明假设你的应用使用 Kotlin 编写。你需要更新两个文件:MainApplication.ktMainActivity.kt

MainApplication 更改

打开 android/app/src/main/java/com/<your-app-name>/MainApplication.kt 并按以下步骤操作。

  1. 你的 Application 类应实现 ReactApplication
  2. 重写 reactHost,使用 ExpoReactHostFactory.getDefaultReactHost()。这会以正确的 expo-updates 集成来设置 React host。
  3. onCreate() 中调用 loadReactNative()ApplicationLifecycleDispatcher.onApplicationCreate() 来初始化 Expo modules。
android/app/src/main/java/com/<your-app-name>/MainApplication.kt
package com.yourpackagename import android.app.Application import android.content.res.Configuration import com.facebook.react.PackageList import com.facebook.react.ReactApplication import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative import com.facebook.react.ReactHost import com.facebook.react.common.ReleaseLevel import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint import expo.modules.ApplicationLifecycleDispatcher import expo.modules.ExpoReactHostFactory // 步骤 1 class MainApplication : Application(), ReactApplication { // 步骤 2 override val reactHost: ReactHost by lazy { ExpoReactHostFactory.getDefaultReactHost( context = applicationContext, packageList = PackageList(this).packages.apply { // 目前无法自动链接的包可以在这里手动添加,例如: // add(MyReactNativePackage()) } ) } // 步骤 3 override fun onCreate() { super.onCreate() DefaultNewArchitectureEntryPoint.releaseLevel = try { ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase()) } catch (e: IllegalArgumentException) { ReleaseLevel.STABLE } loadReactNative(this) ApplicationLifecycleDispatcher.onApplicationCreate(this) } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) } }

MainActivity 更改

打开 android/app/src/main/java/com/<your-app-name>/MainActivity.kt 并按以下步骤操作。

  1. 你的 React Native Activity 应继承 com.facebook.react.ReactActivity
  2. 重写 getMainComponentName(),返回你在上面 JS 入口点中注册的应用名称。
  3. 按如下方式使用 ReactActivityDelegateWrapper 重写 createReactActivityDelegate()
android/app/src/main/java/com/<your-app-name>/MainActivity.kt
package com.yourpackagename import android.os.Bundle import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled import com.facebook.react.defaults.DefaultReactActivityDelegate import expo.modules.ReactActivityDelegateWrapper // 步骤 1 class MainActivity : ReactActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(null) } // 步骤 2 override fun getMainComponentName(): String = "App" // 步骤 3 override fun createReactActivityDelegate(): ReactActivityDelegate { return ReactActivityDelegateWrapper( this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, object : DefaultReactActivityDelegate( this, mainComponentName, fabricEnabled ) {}) } }

在 iOS 上集成 expo-updates

以下说明假设你的应用使用 Swift 编写,并且有一个或多个带有自定义 UIViewController 的原生界面。我们将添加一个自定义视图控制器来渲染你的 React Native 应用。

AppDelegate 更改

  1. 修改 AppDelegate.swift,使其继承 ExpoAppDelegate
  2. 如果你还没有这样做,请添加一个公共方法来获取正在运行的 AppDelegate 实例,以便你的自定义视图控制器稍后可以访问它。
  3. 添加对 expo-updates 的单例 AppController 类的引用,它负责管理 iOS 上的更新系统。
  4. 添加一个新的类 CustomReactNativeFactoryDelegate,它继承 ExpoReactNativeFactoryDelegate,并重写 bundleUrl() 方法,以便在更新系统运行时返回正确的更新 bundle URL。
  5. didFinishLaunchingWithOptions() 方法需要执行两个步骤:
    1. 使用上面创建的 CustomReactNativeFactoryDelegate 初始化 ExpoReactNativeFactory。之后它将用于创建 React Native 根视图。
    2. 调用 AppController.initializeWithoutStarting()。这会创建控制器实例,但会将更新启动流程的其余部分延后到真正需要时再执行。
ios/<your-app-name>/AppDelegate.swift
import Expo import EXUpdates import React import ReactAppDependencyProvider import UIKit @UIApplicationMain // 步骤 1 class AppDelegate: ExpoAppDelegate { var launchOptions: [UIApplication.LaunchOptionsKey: Any]? // 步骤 2 public static func shared() -> AppDelegate { guard let delegate = UIApplication.shared.delegate as? AppDelegate else { fatalError("Could not get app delegate") } return delegate } // 步骤 3 var updatesController: (any InternalAppControllerInterface)? // 步骤 5 private func initializeReactNativeAndUpdates(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { // 步骤 5.1 self.launchOptions = launchOptions let delegate = CustomReactNativeFactoryDelegate() let factory = ExpoReactNativeFactory(delegate: delegate) delegate.dependencyProvider = RCTAppDependencyProvider() reactNativeFactoryDelegate = delegate reactNativeFactory = factory // 步骤 5.2 AppController.initializeWithoutStarting() } /** 应用启动时会初始化自定义视图控制器;所有 React Native 和更新初始化都在其中处理 */ override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { initializeReactNativeAndUpdates(launchOptions) // 创建自定义视图控制器,React Native 视图将在其中创建 self.window = UIWindow(frame: UIScreen.main.bounds) let controller = CustomViewController() controller.view.clipsToBounds = true self.window?.rootViewController = controller window?.makeKeyAndVisible() return true } override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options) } } // 步骤 4 class CustomReactNativeFactoryDelegate: ExpoReactNativeFactoryDelegate { let bundledUrl = Bundle.main.url(forResource: "main", withExtension: "jsbundle") override func sourceURL(for bridge: RCTBridge) -> URL? { // 需要返回适用于 expo-dev-client 的正确 URL。 bridge.bundleURL ?? bundleURL() } override func bundleURL() -> URL? { if let updatesUrl = AppDelegate.shared().updatesController?.launchAssetUrl() { return updatesUrl } return bundledUrl } }

实现自定义视图控制器

  1. 视图控制器应实现更新协议 AppControllerDelegate
  2. 视图控制器初始化应当:
    1. 设置 app delegate 的 updates controller 实例,以便上面的 bundleURL() 方法能够正确用于更新。
    2. AppController 的 delegate 设置为该视图控制器实例
    3. 启动 AppController
  3. 最后,视图控制器必须实现 AppControllerDelegate 协议中的一个方法,appController(_ appController: AppControllerInterface, didStartWithSuccess success: Bool)。当更新系统完全初始化完成,并且最新更新(或内置 bundle)已准备好渲染时,会调用此方法。
    1. 使用 app delegate 创建的 ExpoReactNativeFactory 创建 React Native 根视图。传入的应用名称必须与上面 JS 入口点中注册的应用名称一致。
    2. 将此根视图添加到视图控制器中。
ios/<your-app-name>/CustomViewController.swift
import UIKit import EXUpdates import ExpoModulesCore /** 自定义视图控制器,负责处理 React Native 和 expo-updates 初始化 */ // 步骤 1 public class CustomViewController: UIViewController, AppControllerDelegate { let appDelegate = AppDelegate.shared() // 步骤 2 public convenience init() { self.init(nibName: nil, bundle: nil) self.view.backgroundColor = .clear // 步骤 2.1 appDelegate.updatesController = AppController.sharedInstance // 步骤 2.2 AppController.sharedInstance.delegate = self // 步骤 2.3 AppController.sharedInstance.start() } required public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } @available(*, unavailable) required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } // 步骤 3 public func appController( _ appController: AppControllerInterface, didStartWithSuccess success: Bool ) { createView() } private func createView() { // 步骤 3.1 guard let rootViewFactory: RCTRootViewFactory = appDelegate.reactNativeFactory?.rootViewFactory else { fatalError("rootViewFactory has not been initialized") } let rootView = rootViewFactory.view( withModuleName: "main", initialProperties: [:], launchOptions: appDelegate.launchOptions ) // 步骤 3.2 let controller = self controller.view.clipsToBounds = true controller.view.addSubview(rootView) rootView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ rootView.topAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.topAnchor), rootView.bottomAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.bottomAnchor), rootView.leadingAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.leadingAnchor), rootView.trailingAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.trailingAnchor) ]) } }

AppDelegate 更改

  1. 修改 AppDelegate.swift,使其继承 EXAppDelegateWrapper
  2. 如果你还没有这样做,请添加一个公共方法来获取正在运行的 AppDelegate 实例,以便你的自定义视图控制器稍后可以访问它。
  3. 添加对 expo-updates 的单例 AppController 类的引用,它负责管理 iOS 上的更新系统。
  4. 重写 bundleUrl() 方法,以便在更新系统运行时返回正确的更新 bundle URL。
  5. didFinishLaunchingWithOptions() 方法需要执行两个步骤:
    1. 初始化之后用于创建 React Native 根视图的 root view factory。
    2. 调用 AppController.initializeWithoutStarting()。这会创建控制器实例,但会将更新启动流程的其余部分延后到真正需要时再执行。
ios/<your-app-name>/AppDelegate.swift
import ExpoModulesCore import EXUpdates import React import UIKit @UIApplicationMain // 步骤 1 class AppDelegate: EXAppDelegateWrapper { let bundledUrl = Bundle.main.url(forResource: "main", withExtension: "jsbundle") var launchOptions: [UIApplication.LaunchOptionsKey: Any]? // 步骤 2 public static func shared() -> AppDelegate { guard let delegate = UIApplication.shared.delegate as? AppDelegate else { fatalError("Could not get app delegate") } return delegate } // 步骤 3 var updatesController: (any InternalAppControllerInterface)? // 步骤 4 override func bundleURL() -> URL? { if let updatesUrl = updatesController?.launchAssetUrl() { return updatesUrl } return bundledUrl } // 步骤 5 private func initializeReactNativeAndUpdates(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { // 步骤 5.1 self.launchOptions = launchOptions self.moduleName = "App" self.initialProps = [:] self.rootViewFactory = createRCTRootViewFactory() // 步骤 5.2 AppController.initializeWithoutStarting() } /** * 应用启动时会初始化自定义视图控制器;所有 React Native * 和更新初始化都在其中处理 */ override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { initializeReactNativeAndUpdates(launchOptions) // 创建自定义视图控制器,React Native 视图将在其中创建 self.window = UIWindow(frame: UIScreen.main.bounds) let controller = CustomViewController() controller.view.clipsToBounds = true self.window.rootViewController = controller window.makeKeyAndVisible() return true } }

实现自定义视图控制器

  1. 视图控制器应实现更新协议 AppControllerDelegate
  2. 视图控制器初始化应当:
    1. 设置 app delegate 的 updates controller 实例,以便上面的 bundleURL() 方法能够正确用于更新。
    2. AppController 的 delegate 设置为该视图控制器实例
    3. 启动 AppController
  3. 最后,视图控制器必须实现 AppControllerDelegate 协议中的一个方法,appController(_ appController: AppControllerInterface, didStartWithSuccess success: Bool)。当更新系统完全初始化完成,并且最新更新(或内置 bundle)已准备好渲染时,会调用此方法。
    1. 使用 app delegate 创建的 root view factory 创建 React Native 根视图。传入的应用名称必须与上面 JS 入口点中注册的应用名称一致。
    2. 将此根视图添加到视图控制器中。
ios/<your-app-name>/CustomViewController.swift
import UIKit import EXUpdates import ExpoModulesCore // 步骤 1 public class CustomViewController: UIViewController, AppControllerDelegate { let appDelegate = AppDelegate.shared() // 步骤 2 public convenience init() { self.init(nibName: nil, bundle: nil) self.view.backgroundColor = .clear // 步骤 2.1 appDelegate.updatesController = AppController.sharedInstance // 步骤 2.2 AppController.sharedInstance.delegate = self // 步骤 2.3 AppController.sharedInstance.start() } required public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } @available(*, unavailable) required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } // 步骤 3 public func appController( _ appController: AppControllerInterface, didStartWithSuccess success: Bool ) { createView() } private func createView() { // 步骤 3.1 guard let rootViewFactory: RCTRootViewFactory = appDelegate.reactNativeFactory?.rootViewFactory else { fatalError("rootViewFactory has not been initialized") } let rootView = rootViewFactory.view( withModuleName: appDelegate.moduleName, initialProperties: appDelegate.initialProps, launchOptions: appDelegate.launchOptions ) // 步骤 3.2 let controller = self controller.view.clipsToBounds = true controller.view.addSubview(rootView) rootView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ rootView.topAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.topAnchor), rootView.bottomAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.bottomAnchor), rootView.leadingAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.leadingAnchor), rootView.trailingAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.trailingAnchor) ]) } }

常见问题

将其添加到我的应用需要多长时间?

假设你使用的是 Expo SDK 支持的最新版本 React Native,并且你对原生项目中的 React Native 集成很熟悉,那么你很可能可以在与集成 CodePush 或 Sentry 等工具相近的时间内集成 EAS Update。

最重要的因素是你的应用所使用的 React Native 版本。如果你的应用使用的是比 Expo SDK 当前支持的最新版本更旧的版本(如本指南顶部所引用的版本),那么你应该先升级到该版本,而所需时间将高度取决于应用的规模和复杂度,以及负责该工作的团队的技能和经验水平。

我正在从 CodePush 迁移,我还需要知道什么?

要了解更多信息,请参阅 从 CodePush 迁移 指南。