在现有原生应用中使用 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 安装传入环境变量来禁用自动更新初始化。
- 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 打包流程中
-
确保你的 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; -
如果你使用自定义入口点,请务必在那里包含 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.kt 和 MainActivity.kt。
MainApplication 更改
打开 android/app/src/main/java/com/<your-app-name>/MainApplication.kt 并按以下步骤操作。
- 你的 Application 类应实现
ReactApplication。 - 重写
reactHost,使用ExpoReactHostFactory.getDefaultReactHost()。这会以正确的 expo-updates 集成来设置 React host。 - 在
onCreate()中调用loadReactNative()和ApplicationLifecycleDispatcher.onApplicationCreate()来初始化 Expo modules。
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 并按以下步骤操作。
- 你的 React Native Activity 应继承
com.facebook.react.ReactActivity。 - 重写
getMainComponentName(),返回你在上面 JS 入口点中注册的应用名称。 - 按如下方式使用
ReactActivityDelegateWrapper重写createReactActivityDelegate()。
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 更改
- 修改 AppDelegate.swift,使其继承
ExpoAppDelegate。 - 如果你还没有这样做,请添加一个公共方法来获取正在运行的
AppDelegate实例,以便你的自定义视图控制器稍后可以访问它。 - 添加对
expo-updates的单例AppController类的引用,它负责管理 iOS 上的更新系统。 - 添加一个新的类
CustomReactNativeFactoryDelegate,它继承ExpoReactNativeFactoryDelegate,并重写bundleUrl()方法,以便在更新系统运行时返回正确的更新 bundle URL。 didFinishLaunchingWithOptions()方法需要执行两个步骤:- 使用上面创建的
CustomReactNativeFactoryDelegate初始化ExpoReactNativeFactory。之后它将用于创建 React Native 根视图。 - 调用
AppController.initializeWithoutStarting()。这会创建控制器实例,但会将更新启动流程的其余部分延后到真正需要时再执行。
- 使用上面创建的
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 } }
实现自定义视图控制器
- 视图控制器应实现更新协议
AppControllerDelegate。 - 视图控制器初始化应当:
- 设置 app delegate 的 updates controller 实例,以便上面的
bundleURL()方法能够正确用于更新。 - 将
AppController的 delegate 设置为该视图控制器实例 - 启动
AppController
- 设置 app delegate 的 updates controller 实例,以便上面的
- 最后,视图控制器必须实现
AppControllerDelegate协议中的一个方法,appController(_ appController: AppControllerInterface, didStartWithSuccess success: Bool)。当更新系统完全初始化完成,并且最新更新(或内置 bundle)已准备好渲染时,会调用此方法。- 使用 app delegate 创建的
ExpoReactNativeFactory创建 React Native 根视图。传入的应用名称必须与上面 JS 入口点中注册的应用名称一致。 - 将此根视图添加到视图控制器中。
- 使用 app delegate 创建的
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 更改
- 修改 AppDelegate.swift,使其继承
EXAppDelegateWrapper。 - 如果你还没有这样做,请添加一个公共方法来获取正在运行的
AppDelegate实例,以便你的自定义视图控制器稍后可以访问它。 - 添加对
expo-updates的单例AppController类的引用,它负责管理 iOS 上的更新系统。 - 重写
bundleUrl()方法,以便在更新系统运行时返回正确的更新 bundle URL。 didFinishLaunchingWithOptions()方法需要执行两个步骤:- 初始化之后用于创建 React Native 根视图的 root view factory。
- 调用
AppController.initializeWithoutStarting()。这会创建控制器实例,但会将更新启动流程的其余部分延后到真正需要时再执行。
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 } }
实现自定义视图控制器
- 视图控制器应实现更新协议
AppControllerDelegate。 - 视图控制器初始化应当:
- 设置 app delegate 的 updates controller 实例,以便上面的
bundleURL()方法能够正确用于更新。 - 将
AppController的 delegate 设置为该视图控制器实例 - 启动
AppController
- 设置 app delegate 的 updates controller 实例,以便上面的
- 最后,视图控制器必须实现
AppControllerDelegate协议中的一个方法,appController(_ appController: AppControllerInterface, didStartWithSuccess success: Bool)。当更新系统完全初始化完成,并且最新更新(或内置 bundle)已准备好渲染时,会调用此方法。- 使用 app delegate 创建的 root view factory 创建 React Native 根视图。传入的应用名称必须与上面 JS 入口点中注册的应用名称一致。
- 将此根视图添加到视图控制器中。
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 迁移 指南。