在现有原生应用中使用 EAS 更新
了解如何将 EAS 更新集成到你现有的原生 Android 和 iOS 应用中以启用无线更新。
For the complete documentation index, see llms.txt. Use this file to discover all available pages.
信息 如果你的项目是一个 全新 React Native 应用 —— 从一开始主要使用 React Native 构建,并且应用的入口就是 React Native,那么请跳过本指南,直接前往 开始使用 EAS Update。
本指南说明了如何在现有原生应用中集成 EAS Update,这类应用有时也被称为棕地应用。假设你使用的是 Expo SDK 52 或更高版本,以及 React Native 0.76 或更高版本。
🌐 This guide explains how to integrate EAS Update in an existing native app, sometimes referred to as a brownfield app. It assumes that you are using Expo SDK 52 or later, and React Native 0.76 or later.
旧版本的 Expo SDK 和 React Native 不提供说明。对于旧版本的集成,额外的实际操作支持仅提供给企业客户(联系我们)。
🌐 Instructions are not available for older Expo SDK and React Native versions. Additional hands-on support for integrating with older versions can only be provided for enterprise customers (contact us).
警告 以下说明可能并不适用于所有项目。将 EAS 更新集成到现有项目中的具体方式很大程度上取决于你的应用的具体情况,因此你可能需要根据你的特定设置调整这些说明。如果遇到问题,请在 GitHub 上创建问题或提交拉取请求以建议改进本指南。
7 requirements
7 requirements
1.
You should have a brownfield native project with React Native installed and configured to render a root view. If you don't have this yet, follow the Integration with Existing Apps guide from the React Native documentation and then come back here.
2.
Your app must be using the latest Expo SDK version and its supported React Native version.
3.
Remove any other update library integration from your app, such as react-native-code-push, and
ensure that your app compiles and runs successfully in both debug and release on your supported
platforms.
4.
Support for Expo modules (through the expo package) must be installed and configured in your
project. See Integrating Expo tools into existing native apps for more
information.
5.
Your metro.config.js must extend
expo/metro-config.
6.
Your babel.config.js must extend babel-preset-expo.
7.
npx expo export runs successfullyThe command npx expo export -p android must run successfully in your project if it supports
Android, and npx expo export -p ios if it supports iOS.
安装和基本配置
🌐 Installation and basic configuration
请按照 开始使用 EAS 更新 指南中的步骤 1、2、3 和 4 操作。
🌐 Follow steps 1, 2, 3, and 4 from the Get started with EAS Update guide.
完成此步骤后,你将已经安装并通过 eas-cli 完成认证,将 expo-updates 安装到你的项目中,初始化一个关联的 EAS 项目,并向你的原生项目添加基本配置。
🌐 After this is complete, you will have installed and authenticated with eas-cli, installed expo-updates to your project, initialized an associated EAS project, and added basic configuration to your native projects.
退出自动设置
🌐 Opt out of automatic setup
下一步是禁用 expo-updates 的默认行为,使其不会自动以支持全新 React Native 项目的方式进行设置。
🌐 The next step is to disable the default behavior of expo-updates to automatically set itself up in a way that supports greenfield React Native projects.
禁用 Android 上的自动设置
🌐 Disable automatic setup on Android
修改 android/gradle.properties 来设置禁用自动更新初始化的属性,如下例所示:
🌐 Modify android/gradle.properties to set the property that disables automatic updates initialization, as in the example below:
iOS 上禁用自动设置
🌐 Disable automatic setup on iOS
将环境变量传递给 CocoaPods 安装以禁用自动更新初始化。
🌐 Pass in the environment variable to CocoaPods installation to disable automatic updates initialization.
- EX_UPDATES_CUSTOM_INIT=1 npx pod-install设置你的 React Native 应用以使用 expo-updates 加载发布包
🌐 Set up your React Native app to use expo-updates for loading the release bundle
下一步是将 expo-updates 集成到你的 Android 和 iOS 项目中,以便你的应用在发布版本中使用 expo-updates 作为应用的 JavaScript 来源。
🌐 The next step is to integrate expo-updates into your Android and iOS projects so that your app will use expo-updates as the source of your app JavaScript in release builds.
将 expo-updates 与你的 React Native 打包集成
🌐 Integrating expo-updates with your React Native bundling
-
确保你的 Metro 配置继承了 Expo 配置,如下例所示:
metro.config.js// Learn more https://expo.nodejs.cn/guides/customizing-metro const { getDefaultConfig } = require('expo/metro-config'); /** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname); // eslint-disable-line no-undef // Make any custom changes you need for your project by // directly modifying "config" module.exports = config; -
如果你使用自定义入口点,请确保在那里包含 Expo 初始化。这可以确保 Expo 库(包括
expo-updates)都能正确初始化。以下是两个示例:First Custom Entry Point file example// Expo recommends using registerRootComponent(). // It registers the component with the react-native AppRegistry, // and performs all required Expo initialization // (including expo-updates setup) import App from './App'; import { registerRootComponent } from 'expo'; registerRootComponent(App);Second custom entry point file example// If you need to keep an existing entry point that uses AppRegistry directly, // you will need to add a call to Expo's initialization before registering the // app, as shown below. 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
🌐 Integrating expo-updates on Android
以下说明假设你有一个用 Kotlin 编写的应用。你需要更新两个文件:MainApplication.kt 和 MainActivity.kt。
🌐 The following instructions assume you have an app written in Kotlin. You will need to update two files: MainApplication.kt and MainActivity.kt.
主应用更改
🌐 MainApplication changes
打开 android/app/src/main/java/com/<your-app-name>/MainApplication.kt,并按照下面的步骤操作。
- 你的应用类应该实现
ReactApplication。 - 重写
reactHost以使用ExpoReactHostFactory.getDefaultReactHost()。这将使用正确的 expo-updates 集成设置 React 主机。 - 在
onCreate()中,调用loadReactNative()和ApplicationLifecycleDispatcher.onApplicationCreate()来初始化 Expo 模块。
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 // Step 1 class MainApplication : Application(), ReactApplication { // Step 2 override val reactHost: ReactHost by lazy { ExpoReactHostFactory.getDefaultReactHost( context = applicationContext, packageList = PackageList(this).packages.apply { // Packages that cannot be autolinked yet can be added manually here, for example: // add(MyReactNativePackage()) } ) } // Step 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 更改
🌐 MainActivity changes
打开 android/app/src/main/java/com/<your-app-name>/MainActivity.kt,按照下面的步骤操作。
- 你的 React Native 活动应该继承
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 // Step 1 class MainActivity : ReactActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(null) } // Step 2 override fun getMainComponentName(): String = "App" // Step 3 override fun createReactActivityDelegate(): ReactActivityDelegate { return ReactActivityDelegateWrapper( this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, object : DefaultReactActivityDelegate( this, mainComponentName, fabricEnabled ) {}) } }
在 iOS 上集成 expo-updates
🌐 Integrating expo-updates on iOS
以下说明假设你有一个用 Swift 编写的应用,其中一个或多个原生屏幕具有自定义的 UIViewController。我们将添加一个自定义视图控制器来渲染你的 React Native 应用。
🌐 The following instructions assume you have an app written in Swift, with one or more native screens that have custom UIViewControllers. We will add a custom view controller that renders your React Native app.
AppDelegate 变更
🌐 AppDelegate changes
- 修改 AppDelegate.swift 使其扩展
ExpoAppDelegate。 - 如果你还没有这样做,请添加一个公共方法来获取正在运行的
AppDelegate实例,以便你的自定义视图控制器以后可以访问它。 - 添加对
expo-updatesAppController类单例实例的引用,该类管理 iOS 上的更新系统。 - 添加一个新类
CustomReactNativeFactoryDelegate,继承自ExpoReactNativeFactoryDelegate并重写bundleUrl()方法,以在更新系统运行时返回正确的更新包 URL。 didFinishLaunchingWithOptions()方法需要执行两个步骤:- 使用上面创建的
CustomReactNativeFactoryDelegate初始化ExpoReactNativeFactory。这将在稍后用于创建 React Native 根视图。 - 调用
AppController.initializeWithoutStarting()。这会创建控制器实例,但会将其余的更新启动过程推迟到需要时才执行。
- 使用上面创建的
import Expo import EXUpdates import React import ReactAppDependencyProvider import UIKit @UIApplicationMain // Step 1 class AppDelegate: ExpoAppDelegate { var launchOptions: [UIApplication.LaunchOptionsKey: Any]? // Step 2 public static func shared() -> AppDelegate { guard let delegate = UIApplication.shared.delegate as? AppDelegate else { fatalError("Could not get app delegate") } return delegate } // Step 3 var updatesController: (any InternalAppControllerInterface)? // Step 5 private func initializeReactNativeAndUpdates(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { // Step 5.1 self.launchOptions = launchOptions let delegate = CustomReactNativeFactoryDelegate() let factory = ExpoReactNativeFactory(delegate: delegate) delegate.dependencyProvider = RCTAppDependencyProvider() reactNativeFactoryDelegate = delegate reactNativeFactory = factory // Step 5.2 AppController.initializeWithoutStarting() } /** Application launch initializes the custom view controller: all React Native and updates initialization is handled there */ override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { initializeReactNativeAndUpdates(launchOptions) // Create custom view controller, where the React Native view will be created 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) } } // Step 4 class CustomReactNativeFactoryDelegate: ExpoReactNativeFactoryDelegate { let bundledUrl = Bundle.main.url(forResource: "main", withExtension: "jsbundle") override func sourceURL(for bridge: RCTBridge) -> URL? { // needed to return the correct URL for expo-dev-client. bridge.bundleURL ?? bundleURL() } override func bundleURL() -> URL? { if let updatesUrl = AppDelegate.shared().updatesController?.launchAssetUrl() { return updatesUrl } return bundledUrl } }
实现自定义视图控制器
🌐 Implementing a custom view controller
- 视图控制器应实现更新协议
AppControllerDelegate。 - 视图控制器的初始化应该
- 设置应用委托的更新控制器实例,以便其上述的
bundleURL()方法可以正确处理更新。 - 将
AppController代理设置为视图控制器实例 - 启动
AppController
- 设置应用委托的更新控制器实例,以便其上述的
- 最后,视图控制器必须实现
AppControllerDelegate协议中的一个方法appController(_ appController: AppControllerInterface, didStartWithSuccess success: Bool)。该方法将在更新系统完全初始化后被调用,并且最新的更新(或嵌入的包)已准备好渲染。- 使用应用委托创建的
ExpoReactNativeFactory创建 React Native 根视图。传入的应用名称必须与你在上面的 JS 入口点中注册的应用名称相匹配。 - 将此根视图添加到视图控制器中。
- 使用应用委托创建的
import UIKit import EXUpdates import ExpoModulesCore /** Custom view controller that handles React Native and expo-updates initialization */ // Step 1 public class CustomViewController: UIViewController, AppControllerDelegate { let appDelegate = AppDelegate.shared() // Step 2 public convenience init() { self.init(nibName: nil, bundle: nil) self.view.backgroundColor = .clear // Step 2.1 appDelegate.updatesController = AppController.sharedInstance // Step 2.2 AppController.sharedInstance.delegate = self // Step 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") } // Step 3 public func appController( _ appController: AppControllerInterface, didStartWithSuccess success: Bool ) { createView() } private func createView() { // Step 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 ) // Step 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 changes
- 修改 AppDelegate.swift 使其扩展
EXAppDelegateWrapper。 - 如果你还没有这样做,请添加一个公共方法来获取正在运行的
AppDelegate实例,以便你的自定义视图控制器以后可以访问它。 - 添加对
expo-updatesAppController类单例实例的引用,该类管理 iOS 上的更新系统。 - 如果更新系统正在运行,覆盖“bundleUrl()”方法以返回正确的打包包 URL。
didFinishLaunchingWithOptions()方法需要执行两个步骤:- 初始化稍后用于创建 React Native 根视图的根视图工厂。
- 调用
AppController.initializeWithoutStarting()。这会创建控制器实例,但会将其余的更新启动过程推迟到需要时再进行。
import ExpoModulesCore import EXUpdates import React import UIKit @UIApplicationMain // Step 1 class AppDelegate: EXAppDelegateWrapper { let bundledUrl = Bundle.main.url(forResource: "main", withExtension: "jsbundle") var launchOptions: [UIApplication.LaunchOptionsKey: Any]? // Step 2 public static func shared() -> AppDelegate { guard let delegate = UIApplication.shared.delegate as? AppDelegate else { fatalError("Could not get app delegate") } return delegate } // Step 3 var updatesController: (any InternalAppControllerInterface)? // Step 4 override func bundleURL() -> URL? { if let updatesUrl = updatesController?.launchAssetUrl() { return updatesUrl } return bundledUrl } // Step 5 private func initializeReactNativeAndUpdates(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { // Step 5.1 self.launchOptions = launchOptions self.moduleName = "App" self.initialProps = [:] self.rootViewFactory = createRCTRootViewFactory() // Step 5.2 AppController.initializeWithoutStarting() } /** * Application launch initializes the custom view controller; all React Native * and updates initialization is handled there */ override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { initializeReactNativeAndUpdates(launchOptions) // Create custom view controller, where the React Native view will be created self.window = UIWindow(frame: UIScreen.main.bounds) let controller = CustomViewController() controller.view.clipsToBounds = true self.window.rootViewController = controller window.makeKeyAndVisible() return true } }
实现自定义视图控制器
🌐 Implementing a custom view controller
- 视图控制器应实现更新协议
AppControllerDelegate。 - 视图控制器的初始化应该
- 设置应用委托的更新控制器实例,以便其上述的
bundleURL()方法可以正确处理更新。 - 将
AppController代理设置为视图控制器实例 - 启动
AppController
- 设置应用委托的更新控制器实例,以便其上述的
- 最后,视图控制器必须实现
AppControllerDelegate协议中的一个方法appController(_ appController: AppControllerInterface, didStartWithSuccess success: Bool)。该方法将在更新系统完全初始化后被调用,并且最新的更新(或嵌入的包)已准备好渲染。- 使用应用代理创建的根视图工厂来创建 React Native 根视图。传入的应用名称必须与你在上面的 JS 入口点中注册的应用名称相匹配。
- 将此根视图添加到视图控制器中。
import UIKit import EXUpdates import ExpoModulesCore // Step 1 public class CustomViewController: UIViewController, AppControllerDelegate { let appDelegate = AppDelegate.shared() // Step 2 public convenience init() { self.init(nibName: nil, bundle: nil) self.view.backgroundColor = .clear // Step 2.1 appDelegate.updatesController = AppController.sharedInstance // Step 2.2 AppController.sharedInstance.delegate = self // Step 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") } // Step 3 public func appController( _ appController: AppControllerInterface, didStartWithSuccess success: Bool ) { createView() } private func createView() { // Step 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 ) // Step 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) ]) } }
常见问题
🌐 Common questions
将这个添加到我的应用需要多长时间?
假设你使用的是 Expo SDK 支持的最新版本的 React Native,并且你对在原生项目中集成 React Native 感到熟悉,那么你很可能可以在与集成 CodePush 或 Sentry 等工具所需时间相近的时间内集成 EAS Update。
🌐 Assuming you are using the latest version of React Native supported by the Expo SDK, and you are comfortable with the React Native integration in your native projects, then you can likely integrate EAS Update in a similar amount of time as it would take you to integrate with a tool like CodePush or Sentry.
最重要的因素是你的应用使用的 React Native 版本。如果你的应用使用的版本低于 Expo SDK 支持的最新版本(如本指南开头所述),那么你首先需要升级到该版本,而所需时间将很大程度上取决于应用的规模和复杂性,以及团队的技能和经验水平。
🌐 The most important factor is the React Native version that your app uses. If your app uses anything older than the latest supported version by the Expo SDK (as referenced at the top of this guide), then you will want to upgrade to that version first, and the time that will take is heavily dependent on the size and complexity of the app and skill and experience level of the team working on it.
我正在从 CodePush 迁移,我还需要了解什么?
要了解更多信息,请参阅 从 CodePush 迁移 指南。
🌐 To learn more, see Migrating from CodePush guide.