在现有原生应用中使用 EAS Update
编辑页面
了解如何将 EAS Update 集成到你现有的原生 Android 和 iOS 应用中,以启用空中更新。
For the complete documentation index, see llms.txt. Use this file to discover all available pages.
如果你的项目是一个 greenfield React Native 应用——也就是从一开始主要使用 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 来建议改进本指南。
7 requirements
7 requirements
1.
你应该已经有一个安装并配置好 React Native、能够渲染根视图的 brownfield 原生项目。如果你还没有,请先阅读 React Native 文档中的 与现有应用集成 指南,然后再回到这里。
2.
你的应用必须使用 最新的 Expo SDK 版本及其所支持的 React Native 版本。
3.
从你的应用中移除任何其他更新库集成,例如 react-native-code-push,并确保你的应用在受支持的平台上能够在 debug 和 release 模式下都成功编译并运行。
4.
项目中必须安装并配置对 Expo modules(通过 expo 包)的支持。更多信息请参阅 将 Expo 工具集成到现有原生应用中。
5.
你的 metro.config.js 必须扩展 expo/metro-config。
6.
你的 babel.config.js 必须扩展 babel-preset-expo。
7.
npx expo export 能成功运行如果你的项目支持 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 尚未初始化") } 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 尚未初始化") } 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 迁移 指南。