如何将 Expo 添加到现有的原生(棕地)应用
编辑
添加 Expo 和 React Native 到现有原生应用以及添加第一个视图组件的指南。
React Native 和 Expo 是灵活的,可以逐步采用,一次一个屏幕(甚至一个视图)。您甚至可能会发现,以这种方式使用 Expo 是最适合您特定应用程序的,或者您最终可能会在应用程序的更多表面上慢慢采用它。无论哪种方式,这种灵活性使开发者能够立即在他们的原生应用中采用现代跨平台工具,而不是冒着完全重写的风险。
本指南将引导您完成将 React Native 视图添加到现有原生应用中的步骤。这里介绍的方法被称为“集成”方法,因为 React Native 和 Expo 的集成方式与您使用的其他库相同。
另一种流行的技术是我们所称的“孤立”方法,在这种方法中,您的 Expo 应用被打包为一个库,并被主现有应用视为一个黑箱。孤立方法的指南将很快提供。现在,回到在您的现有原生应用中使用“集成”方法。
前提条件
要将 React Native 集成到您的现有应用中,您需要设置一个 JavaScript 开发环境。这包括安装 Node.js 来运行 Expo CLI 和 Yarn 来管理项目的 JavaScript 依赖关系。
- Node.js (LTS): 执行 JavaScript 代码和 Expo CLI 的运行时。
- Yarn: 用于安装和管理 JavaScript 依赖关系的包管理器。
- iOSCocoaPods: iOS 可用的依赖管理系统之一。CocoaPods 是一个 Ruby gem。您可以使用随最新版本的 macOS 附带的 Ruby 版本安装 CocoaPods。
Learn more from the 设置环境指南。
创建一个 Expo 项目
首先,在您现有的原生项目的根目录中创建一个 Expo 项目。
- npx create-expo-app my-project此命令创建一个名为 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 目录?
设置单体库
单体库,或称为“单一代码库”,是包含多个应用程序或包的单一代码库。了解更多。
设置单体库将确保 Android 和 iOS 脚本能够在自定义文件夹结构下调用 Node 库中的命令。要设置 Yarn 单体库,请在项目根目录创建一个 package.json 文件,并添加以下内容:
{ "version": "1.0.0", "private": true, "workspaces": ["my-project"] }
然后运行 yarn install 来安装依赖关系。这将确保 node_modules 被安装在项目的根目录,并且原生脚本可以与 React Native 代码交互。请确保将 ["my-project"] 更改为您在前一步创建的 Expo 项目的名称。
选择单体库需要您在 Gradle/CocoaPods 中配置自定义项目根目录。这将在接下来的部分中介绍。
配置您的原生项目
要在 Android 上集成 React Native,您需要修改以下文件来配置原生项目:
- Gradle 文件: settings.gradle,顶层 build.gradle,app/build.gradle 和 gradle.properties 来添加 React Native Gradle 插件(RNGP)和其他属性。
- AndroidManifest.xml: 添加必要的权限。 (了解更多)
- MainActivity: 加载您的 React Native 应用程序。
配置 Gradle
1
首先,编辑您的 settings.gradle 文件并添加以下行(使用 bare minimum template 作为参考):
// 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")
2
然后打开您的顶层 build.gradle 并添加此行(根据 bare minimum template 建议):
这会确保 React Native Gradle 和 Expo 插件在您的项目中可用并已应用。
3
在您应用的 build.gradle 文件中添加以下行(通常是 app/build.gradle — 您可以使用 bare minimum template file as reference):
4
最后,打开您应用的 gradle.properties 文件,并添加以下行(使用 bare minimum template file as reference):
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 newArchEnabled=true hermesEnabled=true
配置您的清单
1
首先,确保在您的 AndroidManifest.xml 中拥有 INTERNET 权限:
2
现在在您的 debug AndroidManifest.xml 中,启用 清晰文本流量:
这是让您的应用通过 HTTP 与本地 Metro bundler 通信所必需的。您可以使用 bare minimum 模板中的 AndroidManifest.xml 文件作为参考:main 和 debug
与您的代码集成
现在,您需要添加一些原生代码以启动 React Native 运行时,并告诉它渲染您的 React 组件。
更新您的 Application 类
首先更新您的 Application 类以初始化 React Native。您可以使用来自 bare minimum template 的 MainApplication.kt 作为参考:
创建一个 ReactActivity
创建一个新的 Activity,它将扩展 ReactActivity 并托管 React Native 代码。此活动将负责启动 React Native 运行时并渲染 React 组件。您可以使用 bare minimum template file from 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:
现在您的活动已准备好运行一些 JavaScript 代码。
要在 iOS 上集成 React Native,您需要通过修改以下文件来配置原生 iOS 项目:
- Podfile: 添加 React Native 依赖项。
- Xcode project: 添加用于捆绑 JavaScript 代码的构建阶段。
- Info.plist: 配置 React Native 所需的应用设置。
配置 CocoaPods
如果您的项目没有 Podfile,您可以使用 bare minimum template 作为参考创建一个:
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 下选择您的应用目标。在 Build Settings 中,使用搜索栏搜索 ENABLE_USER_SCRIPT_SANDBOXING。如果尚未设置其值,请将其设置为 No。这是为了确保能够在 Hermes
引擎 的调试和发布版本之间切换。
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 # 根据捆绑器的入口解析设置入口 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'"`
下次当您为发布构建应用程序时,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 视图。为此,您需要一个新的视图控制器,可以在其中加载 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 应用程序代码构建为一个包,通过其 HTTP 服务器提供,并从您的开发环境的 localhost 共享包到模拟器或设备,从而实现 热重载。现在您可以像往常一样构建和运行您的应用程序。一旦您到达应用中的 React 驱动活动,它应该从开发服务器加载 JavaScript 代码。