如何使用集成方法将 Expo 添加到原生应用

使用集成方法向现有原生(棕地)应用添加 Expo 和 React Native 的指南。


React Native 和 Expo 非常灵活,可以逐步采用,一次一个屏幕(甚至一个视图)。你可能会发现以这种方式使用 Expo 对你的特定应用来说是最合适的,或者你可能最终会在应用的更多部分慢慢采用它。无论哪种方式,这种灵活性都使开发者能够立即在原生应用中采用现代的跨平台工具,而无需冒完全重写的风险。

🌐 React Native and Expo are flexible and can be adopted incrementally, one screen (or even one view) at a time. You might even find that using Expo in this way is the best fit for your particular application, or you may end up slowly adopting it across more surfaces in your app. Either way, this flexibility allows enables developers to adopt modern, cross-platform tools in their native apps immediately instead of risking a complete rewrite.

本指南将指导你如何将 React Native 视图添加到现有的原生应用中。这里介绍的方法被称为“集成”方法,因为 React Native 和 Expo 是以与任何其他库相同的方式集成的。

🌐 This guide will walk you through the steps to add a React Native view into an existing native app. The approach covered here is what we call the "integrated" approach, because React Native and Expo are integrated in the same way that you would any other library.

信息 另一种流行的技术是我们称之为“隔离”方法的方法,在这种方法中,你的 Expo 应用被打包为一个库,并由主要的现有应用作为黑盒处理。详情请参见 隔离方法指南

先决条件

🌐 Prerequisites

要将 React Native 集成到你现有的应用中,你需要搭建一个 JavaScript 开发环境。这包括安装 Node.js 来运行 Expo CLI,以及安装 Yarn 来管理项目的 JavaScript 依赖。

🌐 To integrate React Native into your existing application, you'll need to set up a JavaScript development environment. This includes installing Node.js to run Expo CLI and Yarn to manage the project's JavaScript dependencies.

  • Node.js(LTS):用于执行 JavaScript 代码和 Expo CLI 的运行时环境。
  • Yarn:一个用于安装和管理 JavaScript 依赖的包管理器。
  • iOS
    CocoaPods:iOS 可用的依赖管理系统之一。CocoaPods 是一个 Ruby gem。你可以使用 macOS 最新版本自带的 Ruby 来安装 CocoaPods。

设置环境指南了解更多信息。

🌐 Learn more from the Set up environment guide.

创建 Expo 项目

🌐 Create an Expo project

首先,在现有原生项目的根目录中创建一个 Expo 项目。

🌐 First, create an Expo project inside your existing native project's root directory.

Terminal
npx create-expo-app@latest my-project --template default@sdk-55

此命令会创建一个名为 my-project 的新目录,其中包含你的新 Expo 项目。虽然你可以给项目起任何名字,但本指南为了保持一致性使用 my-project。新项目包含一个示例 TypeScript 应用,以帮助你快速入门。

🌐 This command creates a new directory named my-project that contains your new Expo project. While you can name the project anything, this guide uses my-project for consistency. The new project includes an example TypeScript application to help you get started.

设置你的项目结构

🌐 Set up your project structure

一个标准的 React Native 项目会将原生代码放在 androidios 目录中。具体操作方式取决于你的项目,但可能非常简单,例如只需创建这些目录并将你的项目移到其中。例如:

🌐 A standard React Native project places native code in android and ios directories. The specifics of how to do this depend on your project, but it could be as simple as creating the directories and moving your projects there. For example:

Terminal
mkdir my-project/android
mv /path/to/your/android-project my-project/android/
Terminal
mkdir my-project/ios
mv /path/to/your/ios-project my-project/ios/
无法将本地项目移动到 Android 和 iOS 目录吗?

建立一个单体仓库

🌐 Set up a monorepo

Monorepos,或称“单体仓库”,是包含多个应用或包的单一仓库。了解更多

🌐 Monorepos, or "monolithic repositories", are single repositories containing multiple apps or packages. Learn more.

设置单一代码库(monorepo)将确保即使有自定义的文件夹结构,Android 和 iOS 脚本也能够调用 Node 库中的命令。要设置 Yarn 单一代码库,请在项目根目录下创建一个 package.json 文件,并添加以下内容:

🌐 Setting up a monorepo will ensure that Android and iOS scripts will be able to invoke commands from Node libraries even with a custom folder structure. To set up a Yarn monorepo, create a package.json file at the root of your project and add the following content:

package.json
{ "version": "1.0.0", "private": true, "workspaces": ["my-project"] }

然后运行 yarn install 来安装依赖。这将确保 node_modules 安装在项目根目录,并且本地脚本能够与 React Native 代码交互。请确保将 ["my-project"] 改为你在上一步创建的 Expo 项目名称。

🌐 Then run yarn install to install the dependencies. This will ensure node_modules are installed at the root of your project, and that native scripts can interact with React Native code. Make sure to change ["my-project"] to the name of the Expo project you created in the previous step.

信息 选择使用单一代码库(monorepo)需要你在 Gradle/CocoaPods 中配置自定义项目根目录。这将在接下来的章节中讲解。

配置你的原生项目

🌐 Configuring your native project

要在 Android 上集成 React Native,你需要通过修改以下文件来配置本地项目:

🌐 To integrate React Native on Android, you need to configure the native project by modifying the following files:

  • Gradle 文件settings.gradle、顶层 build.gradleapp/build.gradle 以及 gradle.properties 用于添加 React Native Gradle 插件(RNGP)和其他属性。
  • AndroidManifest.xml:用于添加必要的权限。(了解更多)
  • MainActivity:用于加载你的 React Native 应用。

配置 Gradle

🌐 Configuring Gradle

1

首先编辑你的 settings.gradle 文件,并添加以下内容(参考 最简模板):

settings.gradle
// Configures the React Native Gradle Settings plugin used for autolinking pluginManagement { def reactNativeGradlePlugin = new File( providers.exec { workingDir(rootDir) commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })") }.standardOutput.asText.get().trim() ).getParentFile().absolutePath includeBuild(reactNativeGradlePlugin) def expoPluginsPath = new File( providers.exec { workingDir(rootDir) commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })") }.standardOutput.asText.get().trim(), "../android/expo-gradle-plugin" ).absolutePath includeBuild(expoPluginsPath) } plugins { id("com.facebook.react.settings") id("expo-autolinking-settings") } extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand) } expoAutolinking.useExpoModules() // rootProject.name = 'HelloWorld' expoAutolinking.useExpoVersionCatalog() includeBuild(expoAutolinking.reactNativeGradlePlugin) // Include your existing Gradle modules here. // include(":app")
使用自定义文件夹结构吗?

如果你使用自定义的文件夹结构,你需要在 settings.gradle 中显式设置你的项目根目录,以便自动链接生效。修改以下几行:

🌐 If you're using a custom folder structure, you need to explicitly set your project root in settings.gradle for autolinking to work. Modify the following lines:

2

然后打开你的顶层 build.gradle 文件,并加入这行代码(如最小模板所建议的):

这确保了 React Native Gradle 和 Expo 插件在你的项目中可用并已应用。

🌐 This makes sure the React Native Gradle and the Expo plugins are available and applied inside your project.

3

在你应用的 build.gradle 文件中添加以下内容(通常是 app/build.gradle —— 你可以参考最小模板文件):

使用自定义文件夹结构吗?

如果你使用自定义文件夹结构,需要在 app/build.gradle 中调整 projectRoot 的值以指向你的 Expo 项目的根目录。修改以下几行:

🌐 If you're using a custom folder structure, you need to adjust the projectRoot value to point to root of your Expo project in app/build.gradle. Modify the following lines:

4

最后,打开你应用的 gradle.properties 文件,并添加以下几行(参考 最简模板文件):

gradle.properties
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 newArchEnabled=true hermesEnabled=true

配置清单

🌐 Configuring your manifest

1

首先,确保你在 AndroidManifest.xml 中具有 INTERNET 权限:

2

现在在你的 调试 AndroidManifest.xml 中,启用 明文流量

这是你的应用通过 HTTP 与本地 Metro bundler 通信所必需的。你可以参考极简模板中的 AndroidManifest.xml 文件:maindebug

🌐 This is necessary for your app to communicate with your local Metro bundler via HTTP. You can use the AndroidManifest.xml files from the bare minimum template as a reference: main and debug

与你的代码集成

🌐 Integrating with your code

现在,你需要添加一些本地代码来启动 React Native 运行时,并告诉它渲染你的 React 组件。

🌐 Now, you need to add some native code to start the React Native runtime and tell it to render your React components.

更新你的 Application

🌐 Updating your Application class

首先更新你的 Application 类以初始化 React Native。你可以参考 bare minimum template 中的 MainApplication.kt

🌐 Start by updating your Application class to initialize React Native. You can use MainApplication.kt from the bare minimum template as a reference:

创建 ReactActivity

🌐 Creating a ReactActivity

创建一个新的 Activity,它将继承 ReactActivity 并托管 React Native 代码。这个活动将负责启动 React Native 运行时并渲染 React 组件。你可以使用最低限度模板文件中的 MainActivity.kt作为参考:

🌐 Create a new Activity that will extend ReactActivity and host the React Native code. This activity will be responsible for starting the React Native runtime and rendering the React component. You can use the MainActivity.kt from bare minimum template file as a reference:

MyReactActivity.kt
// package <your-package-here> import android.os.Build 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 class MyReactActivity : ReactActivity() { /** * Returns the name of the main component registered from JavaScript. This is used to schedule * rendering of the component. */ override fun getMainComponentName(): String = "main" /** * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] */ override fun createReactActivityDelegate(): ReactActivityDelegate { return ReactActivityDelegateWrapper( this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, object : DefaultReactActivityDelegate( this, mainComponentName, fabricEnabled ){}) } }

将新的 Activity 添加到你的 AndroidManifest.xml 文件中,确保将 MyReactActivity 的主题设置为 Theme.AppCompat.Light.NoActionBar(或任何非 ActionBar 主题),以避免你的应用在 React Native 屏幕上显示 ActionBar

🌐 Add the new Activity to your AndroidManifest.xml file, make sure to set the theme of MyReactActivity to Theme.AppCompat.Light.NoActionBar (or to any non-ActionBar theme) to avoid your application rendering an ActionBar on top of the React Native screen:

现在你的活动已准备好运行一些 JavaScript 代码。

🌐 Now your activity is ready to run some JavaScript code.

要在 iOS 上集成 React Native,你需要通过修改以下文件来配置原生 iOS 项目:

🌐 To integrate React Native on iOS, you need to configure the native iOS project by modifying the following files:

  • Podfile:用于添加 React Native 依赖。
  • Xcode 项目:添加一个构建阶段以打包 JavaScript 代码。
  • Info.plist:配置 React Native 所需的应用设置。

配置 CocoaPods

🌐 Configuring CocoaPods

如果你的项目没有 Podfile,你可以参考 最小模板 来创建一个:

🌐 If your project does not have a Podfile, you can create one using the bare minimum template as a reference:

Podfile
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods") require 'json' platform :ios, '15.1' install! 'cocoapods', :deterministic_uuids => false prepare_react_native_project! target 'HelloWorld' do use_expo_modules! config_command = [ 'npx', 'expo-modules-autolinking', 'react-native-config', '--json', '--platform', 'ios' ] config = use_native_modules!(config_command) use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] use_react_native!( :path => config[:reactNativePath], :hermes_enabled => true, # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/..", :privacy_file_aggregation_enabled => true, ) post_install do |installer| react_native_post_install( installer, config[:reactNativePath], :mac_catalyst_enabled => false, ) end end

如果你的项目已经有一个 Podfile,你需要手动将 React Native 的依赖合并到现有的 Podfile 中。

🌐 If your project already has a Podfile, you'll need to manually merge the React Native dependencies into your existing Podfile.

使用自定义文件夹结构吗?

如果你使用自定义文件夹结构,需要在 Podfile 中显式设置项目根目录,以使自动链接生效。请修改 Podfile 中的以下几行:

🌐 If you're using a custom folder structure, you need to explicitly set your project root in Podfile for autolinking to work. Modify the following lines in your Podfile:

现在,运行以下命令:

🌐 Now, run the following command:

Terminal
pod install

运行 pod 命令将把 React Native 代码整合到你的应用中,从而允许你的 iOS 文件导入 React Native 头文件。

🌐 Running the pod command will integrate the React Native code into your app, allowing your iOS files to import the React Native headers.

配置你的 Xcode 项目

🌐 Configuring your Xcode project

1

在执行 pod install 命令后,CocoaPods 会创建一个 Xcode 工作区 {Project}.xcworkspace,你需要打开 xcworkspace 项目,而不是传统的 xcodeproj 项目。或者,你可以使用以下命令来打开该项目:

Terminal
xed my-project/ios

在 Xcode 项目导航中,选择你的项目,然后在 TARGETS 下选择你的应用目标。在 Build Settings 中,使用搜索栏搜索 ENABLE_USER_SCRIPT_SANDBOXING。如果尚未设置,请将其值设置为 No。这是在使用 React Native 提供的 Hermes 引擎 时,正确切换 Debug 和 Release 版本所必需的。

🌐 In the Xcode project navigator, select your project and then select your app target under TARGETS. In Build Settings, using the search bar, search for ENABLE_USER_SCRIPT_SANDBOXING. If it is not already, set its value to No. This is needed to properly switch between the Debug and Release versions of the Hermes engine that is shipped with React Native.

2

现在切换到 Build Phases 选项卡,并在 [CP] Embed Pods Frameworks 阶段之前添加一个新的 Run Script Phase。此脚本将把你的 JavaScript 代码和资源打包到 iOS 应用中。

🌐 Now switch to the Build Phases tab and add a new Run Script Phase before the [CP] Embed Pods Frameworks phase. This script will bundle your JavaScript code and assets into the iOS application.

Build React Native code and image
if [[ -f "$PODS_ROOT/../.xcode.env" ]]; then source "$PODS_ROOT/../.xcode.env" fi if [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then source "$PODS_ROOT/../.xcode.env.local" fi # The project root by default is one level up from the ios directory export PROJECT_ROOT="$PROJECT_DIR"/.. if [[ "$CONFIGURATION" = *Debug* ]]; then export SKIP_BUNDLING=1 fi if [[ -z "$ENTRY_FILE" ]]; then # Set the entry JS file using the bundler's entry resolution. export ENTRY_FILE="$("$NODE_BINARY" -e "require('expo/scripts/resolveAppEntry')" "$PROJECT_ROOT" ios absolute | tail -n 1)" fi if [[ -z "$CLI_PATH" ]]; then # Use Expo CLI export CLI_PATH="$("$NODE_BINARY" --print "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })")" fi if [[ -z "$BUNDLE_COMMAND" ]]; then # Default Expo CLI command for bundling export BUNDLE_COMMAND="export:embed" fi # Source .xcode.env.updates if it exists to allow # SKIP_BUNDLING to be unset if needed if [[ -f "$PODS_ROOT/../.xcode.env.updates" ]]; then source "$PODS_ROOT/../.xcode.env.updates" fi # Source local changes to allow overrides # if needed if [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then source "$PODS_ROOT/../.xcode.env.local" fi `"$NODE_BINARY" --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'"`

下次,当你为发布构建你的应用时,React Native 代码将使用 Expo CLI 打包并嵌入到应用中。

🌐 Next time, when you build your app for Release, the React Native code will be bundled using Expo CLI and embedded into the app.

3

编辑你的Info.plist文件,并确保添加UIViewControllerBasedStatusBarAppearance键,值为NO,这是确保状态栏能被React Native正确管理所必需的。

与你的代码集成

🌐 Integrating with your code

现在,你需要添加一些本地代码来启动 React Native 运行时,并告诉它渲染你的 React 组件。

🌐 Now, you need to add some native code to start the React Native runtime and tell it to render your React components.

创建 ReactViewController

🌐 Create the ReactViewController

创建一个名为 ReactViewController.swift 的新文件,这将是作为其 view 加载 React Native 视图的 ViewController

🌐 Create a new file called ReactViewController.swift, this will be the ViewController that loads a React Native view as its view.

ReactViewController.swift
import UIKit import React import React_RCTAppDelegate import ReactAppDependencyProvider class ReactNativeViewController: UIViewController { var reactNativeFactory: RCTReactNativeFactory? var reactNativeFactoryDelegate: RCTReactNativeFactoryDelegate? override func viewDidLoad() { super.viewDidLoad() reactNativeFactoryDelegate = ReactNativeDelegate() reactNativeFactoryDelegate!.dependencyProvider = RCTAppDependencyProvider() reactNativeFactory = RCTReactNativeFactory(delegate: reactNativeFactoryDelegate!) view = reactNativeFactory!.rootViewFactory.view(withModuleName: "HelloWorld") } } class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { override func sourceURL(for bridge: RCTBridge) -> URL? { self.bundleURL() } override func bundleURL() -> URL? { #if DEBUG RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry") #else Bundle.main.url(forResource: "main", withExtension: "jsbundle") #endif } }

在 rootViewController 中渲染 React Native 视图

🌐 Presenting a React Native view in a rootViewController

最后,你可以展示你的 React Native 视图。为此,你需要一个新的视图控制器来承载一个可以加载 JS 内容的视图。你已经有了初始的 ViewController,并且可以让它展示 ReactViewController。有多种方法可以实现这一点,具体取决于你的应用。对于这个示例,假设你有一个按钮可以以模态方式展示 React Native。

🌐 Finally, you can present your React Native view. To do so, you need a new View Controller that can host a view in which we can load the JS content. You already have the initial ViewController, and you can make it present the ReactViewController. There are several ways to do so, depending on your app. For this example, let's assume that you have a button that presents React Native modally.

ViewController.swift
import UIKit class ViewController: UIViewController { var reactViewController: ReactViewController? override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. self.view.backgroundColor = .systemBackground let button = UIButton() button.setTitle("Open React Native", for: .normal) button.setTitleColor(.systemBlue, for: .normal) button.setTitleColor(.blue, for: .highlighted) button.addAction(UIAction { [weak self] _ in guard let self else { return } if reactViewController == nil { reactViewController = ReactViewController() } present(reactViewController!, animated: true) }, for: .touchUpInside) self.view.addSubview(button) button.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ button.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), button.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), button.centerYAnchor.constraint(equalTo: self.view.centerYAnchor), ]) } }

测试你的集成

🌐 Test your integration

你已经完成了将 React Native 集成到你的应用中的所有基本步骤。现在在 React Native 目录中运行以下命令以启动 Metro bundler

🌐 You have completed all the basic steps to integrate React Native with your application. Now run the following command in the React Native directory to start the Metro bundler

Terminal
yarn start

Metro 将你的 TypeScript 应用代码构建成一个打包包,通过它的 HTTP 服务器提供服务,并将打包包从你的开发环境中的 localhost 分享到模拟器或设备,从而实现热重载。现在你可以像平常一样构建和运行你的应用。一旦你在应用中进入由 React 驱动的 Activity,它应该会从开发服务器加载 JavaScript 代码。

🌐 Metro builds your TypeScript application code into a bundle, serves it through its HTTP server, and shares the bundle from localhost on your developer environment to a simulator or device, allowing for hot reloading. Now you can build and run your app as normal. Once you reach your React-powered Activity inside the app, it should load the JavaScript code from the development server.