React Native 应用适配鸿蒙PC 实战:从白屏到成功运行

背景

随着 HarmonyOS NEXT 的推出,越来越多的开发者希望将现有的 React Native 应用运行在鸿蒙平台上。React Native OpenHarmony(RNOH)项目为这一需求提供了框架支持,但在实际适配过程中,由于版本兼容性、架构差异、依赖缺失等问题,开发者往往会遇到各种阻碍。

本文记录了一个真实的 React Native 项目(AwesomeProject)适配 HarmonyOS 的完整过程,从项目创建到最终成功运行,期间经历了多次白屏问题及深度调试,最终逐一解决。希望通过这篇文章,能帮助更多开发者少走弯路,快速完成 RN 到鸿蒙的适配工作。


环境信息

项目 版本
macOS macOS Sonoma
DevEco Studio 5.0+
React Native 0.82.1
@rnoh/react-native-openharmony 0.82.30
@react-native-oh/react-native-harmony 0.82.30
@react-native-oh/react-native-harmony-cli 0.82.30
Metro 0.83.7
Hermes 随 RN 0.82.1 内置
HarmonyOS SDK 6.1.1(24)
CAPI 架构 RNOH_C_API_ARCH=1

第一步:环境准备

1.1 配置 HDC 工具环境变量

HDC(HarmonyOS Device Connector)是与鸿蒙设备通信的核心工具,需配置环境变量:

# 编辑 ~/.zshrc
vi ~/.zshrc

# 添加以下内容
export PATH="/Applications/DevEco-Studio.app/Contents/sdk/{版本路径}/openharmony/toolchains:$PATH"
HDC_SERVER_PORT=7035
launchctl setenv HDC_SERVER_PORT $HDC_SERVER_PORT
export HDC_SERVER_PORT

使配置生效:

source ~/.zshrc

1.2 配置 CAPI 版本环境变量

RNOH 框架的 Demo 工程默认使用 CAPI 版本,必须设置环境变量:

# 在 ~/.zshrc 中添加
export RNOH_C_API_ARCH=1

验证:

echo $RNOH_C_API_ARCH
# 输出应为:1

⚠️ 重要:此环境变量在构建和部署时必须生效,否则 CAPI 架构不会启用,导致白屏。


第二步:创建 React Native 项目

2.1 初始化 RN 项目

npx @react-native-community/cli init AwesomeProject
cd AwesomeProject

2.2 添加 OpenHarmony 依赖

修改 package.json,在 dependencies 中添加:

{
  "dependencies": {
    "@react-native-oh/react-native-harmony": "^0.82.30",
    "@react-native-oh/react-native-harmony-cli": "^0.82.30",
    "metro": "^0.83.7",
    "react": "19.1.1",
    "react-native": "0.82.1"
  }
}

⚠️ 关键踩坑react-native 版本必须与 @rnoh/react-native-openharmony 版本对齐。初始创建的 RN 项目可能是 0.84.x 版本,但 RNOH 0.82.30 仅兼容 RN 0.82.x,版本不匹配会导致 TurboModule 缺失错误。此外,metro 需要作为直接依赖安装,否则 bundle-harmony 命令可能找不到 Metro。

安装依赖:

npm install

2.3 创建 Metro 配置

创建 metro.config.js

const {mergeConfig, getDefaultConfig} = require('@react-native/metro-config');
const {createHarmonyMetroConfig} = require('@react-native-oh/react-native-harmony/metro.config');

const config = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
};

module.exports = mergeConfig(
  getDefaultConfig(__dirname),
  createHarmonyMetroConfig({
    reactNativeHarmonyPackageName: '@react-native-oh/react-native-harmony',
  }),
  config
);

2.4 简化 App.tsx

默认模板使用了 @react-native/new-app-screenreact-native-safe-area-context,这些组件库目前没有对应的 OHOS 适配版本,会导致运行时崩溃。将其替换为最基本的 RN 组件:

import { Text, View, StyleSheet } from 'react-native';

function App() {
  return (
    <View style={styles.container}>
      <Text>Hello HarmonyOS!</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default App;

2.5 生成 JS Bundle

package.jsonscripts 中添加:

"dev": "react-native bundle-harmony --dev"

执行生成:

npx react-native bundle-harmony --dev false

成功后会在 harmony/entry/src/main/resources/rawfile/ 目录下生成 bundle.harmony.js 文件。


第三步:创建 HarmonyOS 项目

3.1 使用 DevEco Studio 创建项目

在 DevEco Studio 中创建一个新的 HarmonyOS 项目(Myrndemo),选择 Empty Ability 模板,配置如下:

  • Bundle Name: com.nutpi.rndemo
  • Module Name: entry
  • Language: ArkTS

3.2 添加 RNOH 依赖

修改 entry/oh-package.json5

{
  "name": "entry",
  "version": "1.0.0",
  "description": "Please describe the basic information.",
  "main": "",
  "author": "",
  "license": "",
  "dependencies": {
    "@rnoh/react-native-openharmony": "0.82.30",
    "@ppd/ffrt": "1.1.5"
  },
  "devDependencies": {},
  "dynamicDependencies": {}
}

⚠️ 踩坑@ppd/ffrt 是 RNOH C++ 层的运行时依赖,缺少它会导致 C++ 编译链接失败。

3.3 配置 CMakeLists.txt

创建或修改 entry/src/main/cpp/CMakeLists.txt

project(rnapp)
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_SKIP_BUILD_RPATH TRUE)
set(OH_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")
set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}")

set(RNOH_CPP_DIR "${OH_MODULE_DIR}/@rnoh/react-native-openharmony/src/main/cpp")
set(RNOH_GENERATED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/generated")
set(CMAKE_ASM_FLAGS "-Wno-error=unused-command-line-argument -Qunused-arguments")
set(CMAKE_CXX_FLAGS "-fstack-protector-strong -Wl,-z,relro,-z,now,-z,noexecstack -s -fPIE -pie")
add_compile_definitions(WITH_HITRACE_SYSTRACE)
set(WITH_HITRACE_SYSTRACE 1)

add_subdirectory("${RNOH_CPP_DIR}" ./rn)

add_library(rnoh_app SHARED
    "./PackageProvider.cpp"
    "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp"
)

target_link_libraries(rnoh_app PUBLIC rnoh)

3.4 创建 PackageProvider.cpp

创建 entry/src/main/cpp/PackageProvider.cpp

#include "RNOH/PackageProvider.h"

using namespace rnoh;

std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Context ctx) {
    return {};
}

3.5 配置 EntryAbility

修改 entry/src/main/ets/entryability/EntryAbility.ets

import { RNAbility } from '@rnoh/react-native-openharmony';

export default class EntryAbility extends RNAbility {
  getPagePath() {
    return 'pages/Index';
  }
}

注意:EntryAbility 必须继承自 RNAbility 而非 UIAbility,否则 RN 运行时无法初始化。

3.6 创建 RNPackagesFactory

创建 entry/src/main/ets/RNPackagesFactory.ets

import { RNPackageContext, RNPackage } from '@rnoh/react-native-openharmony/ts';
export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
  return [];
}

3.7 配置 Index.ets(核心页面)

修改 entry/src/main/ets/pages/Index.ets

import {
  AnyJSBundleProvider,
  ComponentBuilderContext,
  FileJSBundleProvider,
  MetroJSBundleProvider,
  ResourceJSBundleProvider,
  RNApp,
  RNOHErrorDialog,
  RNOHLogger,
  TraceJSBundleProviderDecorator,
  RNOHCoreContext
} from '@rnoh/react-native-openharmony';
import { createRNPackages } from '../RNPackagesFactory';

@Builder
export function buildCustomRNComponent(ctx: ComponentBuilderContext) {}

const wrappedCustomRNComponentBuilder = wrapBuilder(buildCustomRNComponent)

@Entry
@Component
struct Index {
  @StorageLink('RNOHCoreContext') private rnohCoreContext: RNOHCoreContext | undefined = undefined
  @State shouldShow: boolean = false
  private logger!: RNOHLogger

  aboutToAppear() {
    this.logger = this.rnohCoreContext!.logger.clone("Index")
    const stopTracing = this.logger.clone("aboutToAppear").startTracing();
    this.shouldShow = true
    stopTracing();
  }

  onBackPress(): boolean | undefined {
    this.rnohCoreContext!.dispatchBackPress()
    return true
  }

  build() {
    Column() {
      if (this.rnohCoreContext && this.shouldShow) {
        if (this.rnohCoreContext?.isDebugModeEnabled) {
          RNOHErrorDialog({ ctx: this.rnohCoreContext })
        }
        RNApp({
          rnInstanceConfig: {
            createRNPackages,
            enableNDKTextMeasuring: true,
            enableBackgroundExecutor: false,
            enableCAPIArchitecture: true,
            arkTsComponentNames: []
          },
          initialProps: { "foo": "bar" } as Record<string, string>,
          appKey: "AwesomeProject",
          wrappedCustomRNComponentBuilder: wrappedCustomRNComponentBuilder,
          onSetUp: (rnInstance) => {
            rnInstance.enableFeatureFlag("ENABLE_RN_INSTANCE_CLEAN_UP")
          },
          jsBundleProvider: new TraceJSBundleProviderDecorator(
            new AnyJSBundleProvider([
              new ResourceJSBundleProvider(this.rnohCoreContext.uiAbilityContext.resourceManager, 'bundle.harmony.js'),
              new ResourceJSBundleProvider(this.rnohCoreContext.uiAbilityContext.resourceManager, 'hermes_bundle.hbc'),
              new FileJSBundleProvider('/data/storage/el2/base/files/bundle.harmony.js'),
              new MetroJSBundleProvider(),
            ]),
            this.rnohCoreContext.logger),
        })
      }
    }
    .height('100%')
    .width('100%')
  }
}

关键配置说明:

配置项 说明
enableCAPIArchitecture: true 启用 CAPI 架构,需配合 RNOH_C_API_ARCH=1 环境变量
appKey: "AwesomeProject" 必须与 JS 端 AppRegistry.registerComponent 注册的名称一致
ResourceJSBundleProvider 优先 确保 bundle 从本地资源加载,而非先尝试 Metro 连接

3.8 配置构建选项

确认 entry/build-profile.json5 中包含 C++ 构建配置:

{
  "apiType": "stageMode",
  "buildOption": {
    "resOptions": {
      "copyCodeResource": {
        "enable": false
      }
    },
    "externalNativeOptions": {
      "path": "./src/main/cpp/CMakeLists.txt",
      "arguments": "",
      "cppFlags": ""
    }
  }
}

第四步:拷贝 JS Bundle 并构建

4.1 拷贝 Bundle

将 RN 项目生成的 bundle 拷贝到 HarmonyOS 项目的 rawfile 目录:

cp AwesomeProject/harmony/entry/src/main/resources/rawfile/bundle.harmony.js \
   Myrndemo/entry/src/main/resources/rawfile/bundle.harmony.js

4.2 安装 OH 依赖

在 Myrndemo 根目录执行:

ohpm install

4.3 构建项目

export RNOH_C_API_ARCH=1
cd Myrndemo
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw \
  --mode module -p module=entry@default -p product=default assembleHap --no-daemon

第五步:部署与调试

5.1 安装到设备

hdc install Myrndemo/entry/build/default/outputs/default/entry-default-unsigned.hap

5.2 启动应用

hdc shell aa start -a EntryAbility -b com.nutpi.rndemo

image-20260607090708316

5.3 查看日志

# 清除旧日志
hdc shell hilog -r

# 查看 RNOH 相关日志
hdc shell hilog -x | grep "#RNOH"

踩坑记录:从白屏到成功运行的完整排障过程

问题一:首次白屏 — 无任何 RNOH 日志输出

现象:应用启动后白屏,hdc hilog 中没有任何 #RNOH 日志。

原因MetroJSBundleProvider 在 JS Bundle Provider 列表中排在第一位。当设备没有连接 Metro 开发服务器时,Metro 连接失败导致整个 Bundle 加载流程中断,后续的 ResourceJSBundleProvider 也未被执行。

修复:调整 Index.etsAnyJSBundleProvider 的顺序,将 ResourceJSBundleProvider 放在第一位:

// 修复前(错误顺序)
new AnyJSBundleProvider([
  new MetroJSBundleProvider(),              // ❌ 优先尝试 Metro,离线必失败
  new ResourceJSBundleProvider(...),        // 永远不会执行到
])

// 修复后(正确顺序)
new AnyJSBundleProvider([
  new ResourceJSBundleProvider(..., 'bundle.harmony.js'),  // ✅ 优先从本地加载
  new ResourceJSBundleProvider(..., 'hermes_bundle.hbc'),  // 备选 HBC 格式
  new FileJSBundleProvider('/data/storage/el2/base/files/bundle.harmony.js'),
  new MetroJSBundleProvider(),              // 最后尝试 Metro(开发调试用)
])

验证:重新构建部署后,日志中出现 RNOH 初始化的 ASCII Art 标志和 TurboModule 创建日志。


问题二:RNOH 初始化后报 TurboModule 缺失

现象:日志中出现以下错误:

E #RNOH_CPP: Couldn't provide turbo module "NativePerformanceCxx"
E #RNOH_CPP: Couldn't provide turbo module "RNCSafeAreaContext"

原因:RN 0.84.1 引入了 NativePerformanceCxx TurboModule,但 RNOH 0.82.30 尚未实现该模块。同时,@react-native/new-app-screen 依赖了 react-native-safe-area-context,该库没有 OHOS 适配。

修复:分两步解决:

  1. 降级 React Native 版本:将 react-native 从 0.84.1 降级到 0.82.1,与 RNOH 0.82.30 对齐:
// package.json
{
  "dependencies": {
    "react-native": "0.82.1",
    "react": "19.1.1"
  }
}
  1. 移除不兼容的组件库:将 App.tsx 从使用 @react-native/new-app-screenreact-native-safe-area-context 的模板代码,替换为纯 react-native 基础组件(View + Text)。

验证:重新生成 bundle 后,RNCSafeAreaContext 错误消失。NativePerformanceCxx 的警告仍然存在但为非致命错误,不影响渲染。


问题三:C++ 编译缺少 @ppd/ffrt 依赖

现象:构建时报链接错误,找不到 ffrt 相关符号。

原因:RNOH 的 C++ 层依赖 @ppd/ffrt(Functional Flow Runtime),但 oh-package.json5 中未声明该依赖。

修复:在 entry/oh-package.json5dependencies 中添加:

{
  "dependencies": {
    "@rnoh/react-native-openharmony": "0.82.30",
    "@ppd/ffrt": "1.1.5"
  }
}

问题四:持续白屏 — RNOH 已初始化但无 UI 渲染

现象:RNOH 日志显示初始化成功,Running "AwesomeProject" 出现,但屏幕仍然白屏,没有 shadow tree 创建日志。

原因:此问题由多个因素叠加导致:

  1. App.tsx 使用了不兼容组件@react-native/new-app-screen 内部使用了 OHOS 不支持的组件(如 SafeAreaView),导致 JS 执行时虽然 AppRegistry.registerComponent 成功注册,但渲染阶段组件树构建失败。

  2. RNOH_C_API_ARCH 环境变量未设置:CAPI 架构需要在构建时设置此环境变量,否则 C++ 层的 CAPI 渲染管线不会正确初始化。

修复

  1. 简化 App.tsx 为仅使用 View + Text 的最小组件
  2. ~/.zshrc 中添加 export RNOH_C_API_ARCH=1source
  3. 确保构建命令在设置了 RNOH_C_API_ARCH=1 的环境中执行

问题五:Metro 依赖缺失导致 bundle 命令失败

现象:执行 npx react-native bundle-harmony 时报错,提示找不到 Metro。

原因metro 作为 @react-native-community/cli 的间接依赖,在某些版本中不会被自动安装为直接依赖,而 bundle-harmony 命令需要直接引用 Metro。

修复:在 package.jsondependencies 中显式添加 metro

{
  "dependencies": {
    "metro": "^0.83.7"
  }
}

最终成功的日志标志

当所有问题解决后,完整的成功日志如下:

I #RNOH_ARK: ██████╗ ███╗   ██╗ ██████╗ ██╗  ██╗
I #RNOH_ARK: ██╔══██╗████╗  ██║██╔═══██╗██║  ██║
I #RNOH_ARK: ██████╔╝██╔██╗ ██║██║   ██║███████║
I #RNOH_ARK: ██╔══██╗██║╚██╗██║██║   ██║██╔══██║
I #RNOH_ARK: ██║  ██║██║ ╚████║╚██████╔╝██║  ██║
I #RNOH_ARK: ╚═╝  ╚═╝╚═╝  ╚═══╝ ╚═════╝ ╚═╝  ╚═╝
I #RNOH_CPP: RNOHAppNapiBridge.cpp:131> onInit (LOG_VERBOSITY_LEVEL=0)
I #RNOH_CPP: Using HermesInstance
I #RNOH_CPP: RNInstanceInternal::start
D #RNOH_ARK: RNInstance::runJSBundle  START
D #RNOH_ARK: RNInstance::runJSBundle  STOP
I #RNOH_JS: Running "AwesomeProject"
I #RNOH_CPP: Creating Turbo Module: DeviceInfo
I #RNOH_CPP: Creating Turbo Module: BlobModule
I #RNOH_CPP: Creating Turbo Module: DeviceEventManager
I #RNOH_CPP: ArkUISurface.cpp:74> onMessageReceived: STATUS_BAR_HEIGHT
I #RNOH_CPP: ArkUISurface.cpp:74> onMessageReceived: CONFIGURATION_UPDATE
I #RNOH_CPP: ArkUISurface.cpp:74> onMessageReceived: WINDOW_SIZE_CHANGE

关键成功标志:

  • ✅ RNOH ASCII Art 横幅出现
  • Using HermesInstance — Hermes 引擎启动
  • Running "AwesomeProject" — JS Bundle 执行成功,AppRegistry 找到注册的组件
  • ArkUISurface 收到 STATUS_BAR_HEIGHT / CONFIGURATION_UPDATE / WINDOW_SIZE_CHANGE — UI 渲染管线工作正常

可忽略的非致命警告

以下警告在日志中出现但不会影响应用运行:

警告 说明
Failed to get customDensity: {} 自定义密度未配置,使用默认值即可
Shake to open Dev Menu is disabled 缺少加速度计权限,不影响功能
Failed to get maxFontScale 最大字体缩放获取失败,使用默认值
Turbo Module 'NativePerformanceCxx' not found RN 0.82.x 的性能模块,RNOH 未实现,降级处理即可
Turbo Module 'SoundManager' not found 音效管理器,RNOH 未实现,不影响核心功能
Turbo Module 'FrameRateLogger' not found 帧率日志,RNOH 未实现,不影响核心功能

完整项目结构

reactnative/
├── AwesomeProject/                    # React Native 项目
│   ├── App.tsx                        # 简化后的入口组件
│   ├── package.json                   # 含 RNOH 依赖
│   ├── metro.config.js                # HarmonyOS Metro 配置
│   └── harmony/
│       └── entry/src/main/resources/rawfile/
│           └── bundle.harmony.js      # 生成的 JS Bundle
│
└── Myrndemo/                          # HarmonyOS 项目
    ├── AppScope/app.json5             # 应用配置(bundleName: com.nutpi.rndemo)
    ├── build-profile.json5            # 构建配置
    ├── oh-package.json5               # 项目级依赖
    ├── entry/
    │   ├── build-profile.json5        # 模块构建配置(含 CMake)
    │   ├── oh-package.json5           # 模块依赖(RNOH + ffrt)
    │   └── src/main/
    │       ├── cpp/
    │       │   ├── CMakeLists.txt      # C++ 构建配置
    │       │   └── PackageProvider.cpp # C++ Package 提供者
    │       ├── ets/
    │       │   ├── entryability/
    │       │   │   └── EntryAbility.ets # 继承 RNAbility
    │       │   ├── RNPackagesFactory.ets # RN Package 工厂
    │       │   └── pages/
    │       │       └── Index.ets       # 主页面(RNApp + RNSurface)
    │       ├── module.json5            # 模块配置
    │       └── resources/rawfile/
    │           └── bundle.harmony.js   # JS Bundle(从 RN 项目拷贝)
    └── oh_modules/                    # OH 依赖安装目录

关键经验总结

1. 版本对齐是第一优先级

React Native、@rnoh/react-native-openharmony@react-native-oh/react-native-harmony 三者的版本必须严格对齐。RNOH 的版本号对应了它所支持的 React Native 版本。例如 RNOH 0.82.30 对应 RN 0.82.x。如果 RN 版本过高,会出现 TurboModule 不兼容;版本过低,则可能出现 API 缺失。

2. JS Bundle Provider 顺序至关重要

AnyJSBundleProvider 按列表顺序尝试加载 bundle,第一个成功的 provider 会终止后续尝试。在离线部署场景中,ResourceJSBundleProvider 必须排在 MetroJSBundleProvider 之前,否则 Metro 连接超时会阻塞整个加载流程。

3. CAPI 架构需要双重启用

CAPI 架构需要在两个地方同时启用:

  • Index.etsrnInstanceConfig.enableCAPIArchitecture: true
  • 构建时环境变量 RNOH_C_API_ARCH=1

缺少任何一个都可能导致渲染管线异常。

4. 不兼容的第三方库是白屏的常见原因

在 OHOS 适配初期,应尽量简化 RN 端代码,只使用 react-native 核心组件(View、Text、ScrollView、Image 等)。对于使用了第三方库的模板代码(如 @react-native/new-app-screenreact-native-safe-area-context),需要确认其是否有对应的 OHOS 适配版本。没有适配版本的库会导致 JS 端渲染失败,表现为白屏。

5. 善用 hdc hilog 排查问题

hdc shell hilog -x | grep "#RNOH" 是最重要的调试手段。根据日志中的不同标记可以判断问题层级:

标记 含义
#RNOH_ARK ArkTS 层日志
#RNOH_CPP C++ 层日志
#RNOH_JS JavaScript 层日志

如果完全没有 #RNOH 日志,说明 RN 运行时根本没有初始化;如果有初始化日志但没有 Running "xxx" ,说明 JS Bundle 加载或执行失败;如果有 Running "xxx" 但白屏,说明组件渲染阶段出错。


下一步

在成功跑通基础 Demo 后,可以逐步:

  1. 添加更多 RN 核心组件:Image、ScrollView、TextInput、FlatList 等
  2. 适配第三方库:确认所需库是否有 OHOS 版本,参考 RNOH 生态兼容列表
  3. 接入 Metro 热更新:开发阶段连接 Metro 服务器实现热重载
  4. 性能优化:使用 Hermes Bytecode(HBC)格式 bundle 提升加载速度
  5. 发布构建:配置签名和混淆规则,生成 Release 版本

参考资料

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐