在现有原生应用中使用 EAS 更新
了解如何将 EAS 更新集成到你现有的原生 Android 和 iOS 应用中以启用无线更新。
信息 如果你的项目是一个 全新 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).
先决条件
🌐 Prerequisites
警告 以下说明可能并不适用于所有项目。将 EAS 更新集成到现有项目中的具体方式很大程度上取决于你的应用的具体情况,因此你可能需要根据你的特定设置调整这些说明。如果遇到问题,请在 GitHub 上创建问题或提交拉取请求以建议改进本指南。
你应该有一个已经安装并配置好 React Native 的现有本地项目,以便渲染根视图。如果你还没有,先按照 React Native 文档中的与现有应用集成指南操作,然后完成步骤后再回到这里。
🌐 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 once you have followed the steps.
- 你的应用必须使用最新的 Expo SDK 版本及其支持的 React Native 版本。
- 从你的应用中删除任何其他更新库集成,例如 react-native-code-push,并确保你的应用在你支持的平台上的调试和发布版本中都能成功编译和运行。
- 必须在你的项目中安装并配置对 Expo 模块(通过
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。
安装和基本配置
🌐 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/settings.gradle 以设置禁用自动更新初始化的属性,如下面的示例所示:
🌐 Modify android/settings.gradle 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.
示例
完整的工作示例可在 这个 GitHub 仓库 中找到。
🌐 A complete working example is available at this GitHub repository .
将 expo-updates 与你的 React Native 打包集成
🌐 Integrating expo-updates with your React Native bundling
-
确保你的 Metro 配置继承了 Expo 配置,如下例所示:
metro.config.js// Learn more https://docs.expo.io/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编写的应用,并且有一个或多个原生活动。打开 android/app/src/main/java/com/<your-app-name>/MainActivity.kt,按照下面的步骤作。
- 你的 React Native 活动应该继承
com.facebook.react.ReactActivity。 - 在此活动中,向
onCreate()添加代码以初始化更新系统。初始化不应在主线程中进行(否则会发生死锁和应用无响应)。 - 重写
getMainComponentName()以返回你在上面的 JS 入口点中注册的应用名称。 - 通过重写
createReactActivityDelegate()方法来显示 React Native 视图,如下所示。
package com.yourpackagename import android.content.Context 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 import expo.modules.updates.UpdatesController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch // Step 1 class MainActivity : ReactActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) CoroutineScope(Dispatchers.IO).launch { startUpdatesController(applicationContext) } } // Step 2 private fun startUpdatesController(context: Context) { UpdatesController.initialize(context) // Call the synchronous `launchAssetFile()` function to wait for updates ready UpdatesController.instance.launchAssetFile } // Step 3 override fun getMainComponentName(): String = "App" // Step 4 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.