如何使用集成方式将 Expo 添加到原生应用中
编辑页面
一份使用集成方式将 Expo 和 React Native 添加到现有原生(brownfield)应用中的指南。
For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.
React Native 和 Expo 非常灵活,可以逐步采用,一次一个屏幕(甚至一次一个视图)。你甚至可能会发现,以这种方式使用 Expo 最适合你的特定应用;或者,你也可能会在应用的更多界面中逐渐采用它。无论哪种方式,这种灵活性都使开发者能够立即在原生应用中采用现代的跨平台工具,而不是冒着完全重写的风险。
本指南将带你了解如何将 React Native 视图添加到现有的原生应用中。这里介绍的方法是我们所说的“集成”方式,因为 React Native 和 Expo 会像集成其他任意库一样被集成进来。
另一种流行的技术是我们所说的“隔离”方式,在这种方式下,你的 Expo 应用会被打包成一个库,并被主应用当作黑盒处理。详情请参阅 isolated approach guide。
前置条件
要将 React Native 集成到你现有的应用中,你需要先搭建一个 JavaScript 开发环境。这包括安装用于运行 Expo CLI 的 Node.js,以及用于管理项目 JavaScript 依赖的 Yarn。
- Node.js (LTS): 用于执行 JavaScript 代码和 Expo CLI 的运行时。
- Yarn: 用于安装和管理 JavaScript 依赖的包管理器。
- iOSCocoaPods: iOS 可用的依赖管理系统之一。CocoaPods 是一个 Ruby gem。你可以使用最新版 macOS 自带的 Ruby 来安装 CocoaPods。
可从 设置环境指南 了解更多。
创建 Expo 项目
首先,在现有原生项目的根目录内创建一个 Expo 项目。
- npx create-expo-app@latest my-project --template default@sdk-55此命令会创建一个名为 my-project 的新目录,其中包含你的新 Expo 项目。虽然你可以将项目命名为任何名称,但为了保持一致,本指南使用 my-project。新项目包含一个示例 TypeScript 应用,帮助你快速上手。
设置项目结构
标准的 React Native 项目会将原生代码放在 android 和 ios 目录中。具体如何操作取决于你的项目,但最简单的情况可能只是创建这些目录并将你的项目移过去。例如:
- mkdir my-project/android- mv /path/to/your/android-project my-project/android/- mkdir my-project/ios- mv /path/to/your/ios-project my-project/ios/无法将你的原生项目移动到 android 和 ios 目录?
设置 monorepo
Monorepo,或“单体仓库”,是包含多个应用或包的单一仓库。了解更多。
设置 monorepo 将确保即使在自定义文件夹结构下,Android 和 iOS 脚本也能够调用 Node 库中的命令。要设置 Yarn monorepo,请在项目根目录创建一个 package.json 文件,并添加以下内容:
{ "version": "1.0.0", "private": true, "workspaces": ["my-project"] }
然后运行 yarn install 来安装依赖。这将确保 node_modules 安装在项目根目录,并且原生脚本可以与 React Native 代码交互。请务必将 ["my-project"] 改为你在上一步创建的 Expo 项目名称。
采用 monorepo 方式需要你在 Gradle/CocoaPods 中配置自定义项目根目录。下一节会对此进行说明。
配置你的原生项目
要在 Android 上集成 React Native,你需要通过修改以下文件来配置原生项目:
- Gradle 文件:settings.gradle、顶层 build.gradle、app/build.gradle 和 gradle.properties,用于添加 React Native Gradle Plugin(RNGP)及其他属性。
- AndroidManifest.xml:用于添加必要权限。(了解更多)
- MainActivity:用于加载你的 React Native 应用。
配置 Gradle
1
首先编辑你的 settings.gradle 文件,并添加以下行(可参考 最小模板):
// 为自动链接配置 React Native Gradle Settings 插件 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) // 在这里包含你现有的 Gradle 模块。 // include(":app")
2
然后打开顶层 build.gradle 并包含这一行(如 最小模板 所示):
这可以确保 React Native Gradle 和 Expo 插件在你的项目中可用并被应用。
3
在你应用的 build.gradle 文件中添加以下几行(通常是 app/build.gradle —— 你可以参考 最小模板文件):
4
最后,打开你应用的 gradle.properties 文件,并添加以下几行(可参考 最小模板文件):
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 newArchEnabled=true hermesEnabled=true
配置你的 manifest
1
首先,确保你的 AndroidManifest.xml 中包含 INTERNET 权限:
2
现在,在你的 debug AndroidManifest.xml 中启用 明文流量:
这是让你的应用通过 HTTP 与本地 Metro bundler 通信所必需的。你可以将最小模板中的 AndroidManifest.xml 文件作为参考:main 和 debug
将其与你的代码集成
现在,你需要添加一些原生代码来启动 React Native 运行时,并告诉它渲染你的 React 组件。
更新你的 Application 类
先更新你的 Application 类以初始化 React Native。你可以参考 最小模板 中的 MainApplication.kt:
创建一个 ReactActivity
创建一个新的 Activity,让它继承 ReactActivity 并承载 React Native 代码。这个 activity 将负责启动 React Native 运行时并渲染 React 组件。你可以参考 最小模板中的 MainActivity.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() { /** * 返回从 JavaScript 注册的主组件名称。该名称用于安排 * 组件的渲染。 */ override fun getMainComponentName(): String = "main" /** * 返回 [ReactActivityDelegate] 的实例。我们使用 [DefaultReactActivityDelegate] *,它允许你通过一个布尔标志 [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:
现在你的 activity 已准备好运行一些 JavaScript 代码。
要在 iOS 上集成 React Native,你需要通过修改以下文件来配置原生 iOS 项目:
- Podfile:用于添加 React Native 依赖项。
- Xcode 项目:用于添加打包 JavaScript 代码的构建阶段。
- Info.plist:用于配置 React Native 所需的应用设置。
配置 CocoaPods
如果你的项目没有 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, # 应用根目录的绝对路径。 :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 中。
现在,运行以下命令:
- pod install运行 pod 命令会将 React Native 代码集成到你的应用中,使你的 iOS 文件能够导入 React Native 头文件。
配置你的 Xcode 项目
1
在执行 pod install 命令后,CocoaPods 会创建一个 Xcode 工作区 {Project}.xcworkspace,你需要打开 xcworkspace 项目,而不是传统的 xcodeproj 项目。或者,你也可以使用以下命令打开项目:
- xed my-project/ios在 Xcode 项目导航器中,选择你的项目,然后在 TARGETS 下选择你的应用 target。在 Build Settings 中,使用搜索栏查找 ENABLE_USER_SCRIPT_SANDBOXING。如果它尚未设置,则将其值设为 No。这对于在 React Native 随附的 Hermes 引擎 的 Debug 和 Release 版本之间正确切换是必需的。
2
现在切换到 Build Phases 选项卡,并在 [CP] Embed Pods Frameworks 阶段之前添加一个新的 Run Script Phase。这个脚本将把你的 JavaScript 代码和资源打包到 iOS 应用中。
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 # 项目根目录默认是 ios 目录的上一级 export PROJECT_ROOT="$PROJECT_DIR"/.. if [[ "$CONFIGURATION" = *Debug* ]]; then export SKIP_BUNDLING=1 fi if [[ -z "$ENTRY_FILE" ]]; then # 使用 bundler 的入口解析来设置入口 JS 文件。 export ENTRY_FILE="$("$NODE_BINARY" -e "require('expo/scripts/resolveAppEntry')" "$PROJECT_ROOT" ios absolute | tail -n 1)" fi if [[ -z "$CLI_PATH" ]]; then # 使用 Expo CLI export CLI_PATH="$("$NODE_BINARY" --print "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })")" fi if [[ -z "$BUNDLE_COMMAND" ]]; then # 打包时默认使用的 Expo CLI 命令 export BUNDLE_COMMAND="export:embed" fi # 如果 .xcode.env.updates 存在则加载它,以便在需要时 # 取消设置 SKIP_BUNDLING if [[ -f "$PODS_ROOT/../.xcode.env.updates" ]]; then source "$PODS_ROOT/../.xcode.env.updates" fi # 加载本地更改,以便在需要时 # 覆盖默认值 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'"`
下次当你为 Release 构建应用时,React Native 代码将使用 Expo CLI 进行打包,并嵌入到应用中。
3
编辑你的 Info.plist 文件,并确保添加 UIViewControllerBasedStatusBarAppearance 键,其值为 NO,这对于确保状态栏由 React Native 正确管理是必需的。
将其与你的代码集成
现在,你需要添加一些原生代码来启动 React Native 运行时,并告诉它渲染你的 React 组件。
创建 ReactViewController
创建一个名为 ReactViewController.swift 的新文件,这将作为加载 React Native 视图并将其作为 view 的 ViewController。
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 视图
最后,你可以展示你的 React Native 视图。为此,你需要一个新的 View Controller 来承载一个可加载 JS 内容的视图。你已经有了初始的 ViewController,并且可以让它展示 ReactViewController。实现方式有多种,取决于你的应用。这个例子中,我们假设你有一个按钮会以模态方式展示 React Native。
import UIKit class ViewController: UIViewController { var reactViewController: ReactViewController? override func viewDidLoad() { super.viewDidLoad() // 在加载视图后执行任何额外的设置。 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), ]) } }
测试你的集成
你已经完成了将 React Native 与你的应用集成所需的所有基本步骤。现在,在 React Native 目录中运行以下命令以启动 Metro bundler
- yarn startMetro 会将你的 TypeScript 应用代码构建成一个 bundle,通过其 HTTP 服务器提供该 bundle,并将开发环境中 localhost 上的 bundle 共享到模拟器或设备,从而支持 热重载。现在你可以像往常一样构建并运行你的应用。一旦你在应用中进入由 React 驱动的 Activity,它就应该从开发服务器加载 JavaScript 代码。