本文是《React Native x HarmonyOS NEXT 创新能力接入方案》系列第 2 篇。上一篇React Native 跑在 HarmonyOS NEXT 上:RNOH 环境搭建与首个页面运行完成了 RNOH 环境搭建和首屏运行,本篇先把 React Native 调用 ArkTS 原生模块 的工程闭环讲清楚。
源码: https://atomgit.com/huqi/RNHarmonySkuAssistant


1. 本篇要实现什么?

本篇不直接接 TTS、OCR、IAP 这类复杂 Kit,而是先完成一个最小 TurboModule:

React Native 页面点击按钮
    ↓
NativeSkuModule.ts typed spec
    ↓
RNOH Codegen 生成胶水代码
    ↓
ArkTS UITurboModule 实现
    ↓
GeneratedPackage.ets / RNPackagesFactory.ets 注册
    ↓
React Native 页面展示 ArkTS 返回值

最小模块包含两个方法:

sayHello(): Promise<string>
formatSkuName(name: string): Promise<string>

这一步验证的是跨端调用链路,不验证任何真实 HarmonyOS Kit。


2. 为什么先写 TurboModule?

后续能力都绕不开原生侧:

功能 是否需要 HarmonyOS 原生侧
TTS 语音合成 需要
OCR 文字识别 需要
商品图主体分割 需要
小艺智能体拉起 需要
碰一碰分享 需要
智感握姿事件 需要
Form Kit 桌面卡片 需要
Calendar Kit 需要
IAP Kit / Account Kit 需要

React Native 官方 Turbo Native Module 的基本流程是:定义 typed JS spec、配置 Codegen、生成原生接口、用生成接口接入原生代码。RNOH 在这个基础上增加了 OpenHarmony / ArkTS 工程侧的 Package 注册步骤。


3. 验证环境

本文实际验证环境:

项目 当前环境
DevEco Studio 6.1.1 Release
HarmonyOS SDK API 15 (6.1.1)
React Native 0.84.1
RNOH @react-native-oh/react-native-harmony (本地源码)
Node.js 22.22.3
ohpm 6.1.1
hvigor 6.1.1
设备型号 HarmonyOS NEXT 模拟器 (Pura 70)
系统版本 HarmonyOS 6.1.0

注意:RNOH 仓库页面当前可见 0.82 系列活跃信息与 release/tag。历史文章中的固定版本号只能作为我当时写作时用到的,您应以版本说明、release/tag 和实际依赖为准。


4. 推荐项目结构

RNOH 官方文档把 TurboModule 作为可独立打包的三方依赖包示例,例如 RTNCalculator/。业务项目也可以先在同仓库内组织,但要理解"接口声明包"和"App 侧 Harmony 工程注册"的职责差异。

实际项目结构(本文示例):

RNHarmonySkuAssistant/
├── App.tsx                              # React Native 页面入口
├── index.js                             # RN App 注册
├── metro.config.js                      # RNOH resolver + React 单例解析
├── package.json                         # App 的依赖和 codegen 脚本
│
├── src/
│   ├── native/
│   │   └── NativeSkuModule.ts           # JS 侧 typed import 文件
│   └── specs/v2/
│       └── NativeSkuModule.ts           # (与上同) Codegen 读取的 spec
│
├── RTNSkuModule/                        # 独立 TurboModule 包
│   ├── index.ts                         # 包入口,re-export spec
│   ├── package.json                     # 包含 harmony.codegenConfig
│   └── src/specs/v2/
│       └── NativeSkuModule.ts           # Codegen 读取的 spec 声明
│
├── node_modules/
│   └── rtn-sku-module/                  # npm pack + npm i 安装后的包
│
└── harmony/entry/src/main/
    ├── cpp/
    │   ├── CMakeLists.txt               # 需改为 GLOB_RECURSE 包含子目录
    │   ├── PackageProvider.cpp          # 注册 C++ Package
    │   └── generated/                   # Codegen 生成的 C++ 胶水代码
    │       ├── RNOHGeneratedPackage.h
    │       └── rtn_sku_module/RNOH/generated/
    │           ├── BaseRtnSkuModulePackage.h
    │           └── turbo_modules/
    │               ├── RTNSkuModule.cpp
    │               └── RTNSkuModule.h
    └── ets/
        ├── turbomodule/
        │   └── SkuModule.ets            # 手写 ArkTS 实现
        ├── GeneratedPackage.ets          # 手写 Package 注册
        ├── RNPackagesFactory.ets         # 返回自定义 Package 数组
        └── generated/                   # Codegen 生成的 ETS 胶水代码
            ├── index.ets
            ├── ts.ts
            ├── components/ts.ts
            └── turboModules/
                ├── RTNSkuModule.ts
                └── ts.ts

5. TypeScript spec:Native*.ts 命名

React Native Codegen 需要 typed spec 文件。官方文档强调 spec 文件应使用 Flow 或 TypeScript,并以 Native 前缀命名。RNOH 文档示例也使用 NativeCalculator.ts

实际代码:

// RTNSkuModule/src/specs/v2/NativeSkuModule.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  sayHello(): Promise<string>;
  formatSkuName(name: string): Promise<string>;
}

export default TurboModuleRegistry.getEnforcing<Spec>('RTNSkuModule');

关键点:

1. 文件名建议为 Native<MODULE_NAME>.ts。
2. 必须导出 Spec,并继承 TurboModule。
3. TurboModuleRegistry 中的模块名必须和原生注册名一致(本例为 'RTNSkuModule')。
4. 方法签名要和 ArkTS 实现保持一致。
5. 第一阶段建议只使用 string / number / boolean / object / array 等基础类型。

App 侧 src/native/NativeSkuModule.ts 内容与上面相同,作为 JS 侧的 import 入口。


6. package.json:harmony.codegenConfig

RNOH 文档示例使用 harmony 字段描述 OpenHarmony 侧配置,codegenConfig 是待生成三方库配置数组。

实际代码(RTNSkuModule/package.json):

{
  "name": "rtn-sku-module",
  "version": "0.1.0",
  "description": "RNOH TurboModule 最小实践:Sku 工具模块",
  "main": "index.ts",
  "files": [
    "index.ts",
    "src/*"
  ],
  "harmony": {
    "alias": "rtn_sku_module",
    "codegenConfig": [
      {
        "version": 2,
        "specPaths": ["./src/specs/v2"]
      }
    ]
  }
}

说明:

alias:OpenHarmony 侧模块别名(snake_case)。
version:Codegen 配置版本,目前使用 2。
specPaths:spec 文件所在目录,相对于包根目录。
files:npm pack 时需要把接口、源码打进去。harmony 产物(.har / .tar.gz)在未来发布正式版本时加入。

如果目录中有空文件夹,npm 默认不会打包,RNOH 文档建议使用 .gitkeep 保留。


7. Codegen 执行与产物

RNOH 文档的典型流程是:

# 1. 在 TurboModule 包目录打包
cd RTNSkuModule
npm pack

# 2. 在 App 项目安装本地 tgz
cd ../RNHarmonySkuAssistant
npm i file:../RTNSkuModule/rtn-sku-module-0.1.0.tgz

# 3. 在 App 项目执行 RNOH Codegen
npm run codegen:harmony

App 的 package.json 增加如下脚本:

{
  "scripts": {
    "codegen:harmony": "react-native codegen-harmony --ets-output-path ./harmony/entry/src/main/ets --cpp-output-path ./harmony/entry/src/main/cpp/generated"
  }
}

执行成功后控制台输出:

• harmony/entry/src/main/cpp/generated/RNOHGeneratedPackage.h
• harmony/entry/src/main/cpp/generated/rtn_sku_module/RNOH/generated/BaseRtnSkuModulePackage.h
• harmony/entry/src/main/cpp/generated/rtn_sku_module/RNOH/generated/turbo_modules/RTNSkuModule.cpp
• harmony/entry/src/main/cpp/generated/rtn_sku_module/RNOH/generated/turbo_modules/RTNSkuModule.h
• harmony/entry/src/main/cpp/generated/rtn_sku_module/react/renderer/components/rtn_sku_module/ComponentDescriptors.h
• harmony/entry/src/main/cpp/generated/rtn_sku_module/react/renderer/components/rtn_sku_module/Props.cpp
• harmony/entry/src/main/cpp/generated/rtn_sku_module/react/renderer/components/rtn_sku_module/EventEmitters.cpp
• harmony/entry/src/main/ets/generated/components/ts.ts
• harmony/entry/src/main/ets/generated/index.ets
• harmony/entry/src/main/ets/generated/ts.ts
• harmony/entry/src/main/ets/generated/turboModules/RTNSkuModule.ts
• harmony/entry/src/main/ets/generated/turboModules/ts.ts

info Generated 18 file(s)

生成的 ETS 胶水代码核心文件:

// harmony/entry/src/main/ets/generated/turboModules/RTNSkuModule.ts
export namespace RTNSkuModule {
  export const NAME = 'RTNSkuModule' as const

  export interface Spec {
    sayHello(): Promise<string>;
    formatSkuName(name: string): Promise<string>;
  }
}

⚠️血泪史:

Codegen 自动生成的胶水代码可能覆盖本地同名实现。不要把手写业务逻辑放在会被 Codegen 重写的位置。


8. ArkTS 实现:UITurboModule 与 AnyThreadTurboModule

UI 线程版本可以继承 UITurboModule,异步方法返回 Promise

实际代码:

// harmony/entry/src/main/ets/turbomodule/SkuModule.ets
import { UITurboModule, UITurboModuleContext } from '@rnoh/react-native-openharmony';
import type { TM } from '../generated/ts';

export class SkuModule extends UITurboModule implements TM.RTNSkuModule.Spec {
  constructor(ctx: UITurboModuleContext) {
    super(ctx);
  }

  sayHello(): Promise<string> {
    return Promise.resolve('来自 ArkTS:调用成功');
  }

  formatSkuName(name: string): Promise<string> {
    return Promise.resolve(`[SKU] ${name.toUpperCase()}`);
  }
}

关键说明:

  • TM.RTNSkuModule.Spec 来自 Codegen 生成的定义(generated/turboModules/RTNSkuModule.ts
  • TMgenerated/ts.tsexport * as TM from "./turboModules/ts" 的别名

9. GeneratedPackage.ets:把模块暴露给 RNOH

RNOH 文档要求创建继承 RNOHPackage 的 Package,并在其中返回 TurboModule factory。

实际代码:

// harmony/entry/src/main/ets/GeneratedPackage.ets
import {
  RNOHPackage,
  TurboModuleContext,
  UITurboModule,
} from '@rnoh/react-native-openharmony';
import { SkuModule } from './turbomodule/SkuModule';
import type { TM } from './generated/ts';

export default class GeneratedPackage extends RNOHPackage {
  getUITurboModuleFactoryByNameMap(): Map<string, (ctx: TurboModuleContext) => UITurboModule> {
    return new Map<string, (ctx: TurboModuleContext) => UITurboModule>([
      [TM.RTNSkuModule.NAME, (ctx) => new SkuModule(ctx)],
    ]);
  }

  createEagerUITurboModuleByNameMap(ctx: TurboModuleContext): Map<string, UITurboModule> {
    return new Map<string, UITurboModule>();
  }
}

关键点:

  • getUITurboModuleFactoryByNameMap 返回懒加载 factory map —— 只有被 JS 侧请求时才创建实例
  • createEagerUITurboModuleByNameMap 返回空 map,表示不需要提前创建
  • 模块名 TM.RTNSkuModule.NAME 必须与 TurboModuleRegistry.getEnforcing('RTNSkuModule') 中的名字一致

10. RNPackagesFactory.ets:注册 Package

entry/src/main/ets/RNPackagesFactory.ets 中把自定义 Package 加入返回数组。

实际代码:

// harmony/entry/src/main/ets/RNPackagesFactory.ets
import { RNPackageContext, RNPackage } from '@rnoh/react-native-openharmony/ts';
import GeneratedPackage from './GeneratedPackage';

export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
  return [
    new GeneratedPackage(ctx),
  ];
}

如果你还接入了三方 RNOH 组件或 Fabric 组件,也在这里集中注册。模块找不到时,首先检查这里是否返回了对应 Package。


11. worker 线程 TurboModule 的额外条件

只有当模块确实需要跑在 worker 线程时再引入这部分。RNOH 文档中的关键条件包括:

1. ArkTS 模块基类从 UITurboModule 改为 AnyThreadTurboModule。
2. Package 侧使用 getAnyThreadTurboModuleFactoryByNameMap / createEagerAnyThreadTurboModuleByNameMap。
3. EntryAbility.ets 继承 RNAbility。
4. EntryAbility.ets 重载 getRNOHWorkerScriptUrl()。
5. 在 ets/workers/ 下创建 RNOHWorker.ets。
6. worker 中调用 setupRNOHWorker,并传入 thirdPartyPackagesFactory。

示意:

getRNOHWorkerScriptUrl(): string {
  return 'entry/ets/workers/RNOHWorker.ets';
}

主线程与 worker 线程中的 HttpClientcaPathProvider 等配置相互独立。


12. C++ 侧 Package 注册

Codegen v2 会生成 C++ 桥接代码,必须在 C++ 侧和 ArkTS 侧同时注册,否则运行时会出现 Couldn't find Turbo Module on the ArkTs side 错误。

12.1 修改 PackageProvider.cpp

注册 BaseRtnSkuModulePackage,它包含 TurboModuleFactoryDelegate,会在 C++ 侧创建 ArkTSTurboModule 桥接类(包含方法签名元数据),这样才能正确地将 JS 调用路由到 ArkTS 实现。

// harmony/entry/src/main/cpp/PackageProvider.cpp
#include "RNOH/PackageProvider.h"
#include "RNOHPackagesFactory.h"
#include "generated/RNOHGeneratedPackage.h"
#include "generated/rtn_sku_module/RNOH/generated/BaseRtnSkuModulePackage.h"

using namespace rnoh;

std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Context ctx) {
    auto packages = createRNOHPackages(ctx);
    packages.push_back(std::make_shared<RNOHGeneratedPackage>(ctx));
    packages.push_back(std::make_shared<BaseRtnSkuModulePackage>(ctx));
    return packages;
}

12.2 修改 CMakeLists.txt

需要做两处修改:

  1. GLOB 改为 GLOB_RECURSE,因为 v2 codegen 生成的 .cpp 文件在子目录中
  2. 添加 include_directories,因为 BaseRtnSkuModulePackage.h 中的 #include "RNOH/generated/turbo_modules/RTNSkuModule.h" 需要从 generated/rtn_sku_module/ 目录解析
# harmony/entry/src/main/cpp/CMakeLists.txt

# 修改 1:GLOB_RECURSE 以包含子目录的 .cpp 文件
file(GLOB_RECURSE GENERATED_CPP_FILES "${CMAKE_CURRENT_SOURCE_DIR}/generated/*.cpp")

# 修改 2:添加 include path,让 #include "RNOH/generated/turbo_modules/RTNSkuModule.h" 能被正确解析
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/generated/rtn_sku_module)

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

注意:必须使用全局 include_directories 而非 target_include_directories,因为 Codegen 生成的 Header 中用引号包含路径 #include "RNOH/generated/turbo_modules/RTNSkuModule.h",全局 include path 才能确保编译器在各编译单元中正确解析该路径。

12.3 为什么需要 C++ 注册?

C++ TurboModuleFactory 的调用流程:

JS 调用 TurboModuleRegistry.getEnforcing('RTNSkuModule')
    ↓
C++ TurboModuleFactory::create() 被调用
    ↓
C++ TurboModuleFactoryDelegate → 检查所有 C++ Package 的 delegate
    ↓                              ↓
找到 RTNSkuModule 的 ArkTSTurboModule   没找到
↓                                     ↓
C++ 桥接正常工作                         检查 ArkTS TurboModuleProvider
                                         ↓
                                      ArkTS 有该模块 → 报错要求注册 C++ 侧
                                      ArkTS 没有     → 模块不存在的标准错误

因此 C++ 和 ArkTS 两侧的注册都是必需的,缺一不可。


13. React Native 页面调用

页面侧只依赖 typed spec 暴露出的接口:

import React, { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import RTNSkuModule from './src/native/NativeSkuModule';

export default function App() {
  const [message, setMessage] = useState('尚未调用');

  const callNative = async () => {
    try {
      const hello = await RTNSkuModule.sayHello();
      const sku = await RTNSkuModule.formatSkuName('Stainless Steel Bottle');
      setMessage(`${hello}\n${sku}`);
    } catch (error) {
      setMessage(`调用失败:${String(error)}`);
    }
  };

  return (
    <View style={styles.container}>
      <View style={styles.card}>
        <Text style={styles.title}>RNOH TurboModule 最小实践</Text>
        <Text style={styles.message}>{message}</Text>
        <Pressable style={styles.button} onPress={callNative}>
          <Text style={styles.buttonText}>调用 ArkTS TurboModule</Text>
        </Pressable>
      </View>
    </View>
  );
}

14. 运行与验证

14.1 检查清单

[x] 1. Metro / bundle 已生成(npm run harmony)
[x] 2. TurboModule 包已被 App 安装(node_modules/ 中存在 rtn-sku-module)
[x] 3. codegen-harmony 执行成功(18 个文件生成)
[x] 4. harmony/entry/src/main/ets/generated/ 目录存在对应产物
[x] 5. harmony/entry/src/main/ets/turbomodule/SkuModule.ets 继承了 UITurboModule
[x] 6. harmony/entry/src/main/ets/GeneratedPackage.ets 正确映射了模块名
[x] 7. harmony/entry/src/main/ets/RNPackagesFactory.ets 返回了自定义 Package
[x] 8. harmony/entry/src/main/cpp/PackageProvider.cpp 注册了 BaseRtnSkuModulePackage
[x] 9. harmony/entry/src/main/cpp/CMakeLists.txt 使用 GLOB_RECURSE
[x] 10. hvigor 重新编译并安装 HAP
[x] 11. 页面点击按钮后能看到 ArkTS 返回值

14.2 预期运行结果

点击"调用 ArkTS TurboModule"按钮后,页面显示:

来自 ArkTS:调用成功
[SKU] STAINLESS STEEL BOTTLE

实际验证截图:

点击按钮后页面展示 ArkTS TurboModule 返回值

14.3 实际执行命令

本次验证使用命令行完成构建、安装、启动与点击:

npm run codegen:harmony
npm run harmony

cd harmony
/Applications/DevEco-Studio.app/Contents/tools/node/bin/node \
  /Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw.js \
  --mode module \
  -p module=entry@default \
  -p product=default \
  -p requiredDeviceType=phone \
  assembleHap

cd ..
hdc install -r harmony/entry/build/default/outputs/default/entry-default-unsigned.hap
hdc shell aa start -a EntryAbility -b host.huqi.sku_assistant
hdc shell uitest uiInput click 660 1578

验证结果:

npm run lint                  PASS
npm test -- --runInBand       PASS
hvigor assembleHap            BUILD SUCCESSFUL
hdc install                   install bundle successfully
aa start                      start ability successfully
uitest uiInput click          No Error

15. 常见问题

15.1 TurboModule 找不到

TurboModuleRegistry.getEnforcing(...): '<module>' could not be found

优先检查:

1. JS spec 中的模块名(TurboModuleRegistry.getEnforcing('X'))。
2. Codegen 生成的 NAME 常量(generated/turboModules/RTNSkuModule.ts)。
3. GeneratedPackage.ets factory map 中的 key 是否匹配。
4. RNPackagesFactory.ets 是否注册了 GeneratedPackage。
5. C++ PackageProvider.cpp 是否注册了 BaseRtnSkuModulePackage。
6. CMakeLists.txt 是否使用 GLOB_RECURSE(否则 RTNSkuModule.cpp 不会被编译)。
7. HarmonyOS 工程是否重新编译安装(JS 可热更新,ArkTS/C++ 需要重新构建)。

15.2 Render Error:useState of null

本次验证中曾遇到页面启动后直接出现 LogBox:

Render Error
Cannot read property 'useState' of null

截图如下:

双 React 实例导致 useState dispatcher 为空

这个错误不是 TurboModule 注册失败,而是典型的双 React 实例问题。当前项目同时使用本地 RNOH 源码依赖和 App 自身 node_modules,如果 Metro 把 App 代码里的 react 和 RN renderer 里的 react 解析成两份实例,Hook dispatcher 就会变成 null,页面在第一行 useState 处崩溃。

修复方式是在 metro.config.js 中保留 RNOH 自带 resolver,同时把 react / react/* 固定解析到项目根目录:

const mergedConfig = mergeConfig(getDefaultConfig(__dirname), harmonyConfig, config);
const harmonyResolveRequest = mergedConfig.resolver.resolveRequest;

mergedConfig.resolver.resolveRequest = (context, moduleName, platform) => {
  if (moduleName === 'react' || moduleName.startsWith('react/')) {
    return {
      type: 'sourceFile',
      filePath: require.resolve(moduleName, {paths: [projectRoot]}),
    };
  }

  if (moduleName === 'react-native-safe-area-context') {
    return {
      type: 'sourceFile',
      filePath: safeAreaContextShimPath,
    };
  }

  return harmonyResolveRequest
    ? harmonyResolveRequest(context, moduleName, platform)
    : context.resolveRequest(context, moduleName, platform);
};

注意不要直接覆盖 RNOH 自带 resolver。这里只固定 React 单例,其余模块继续交给 RNOH resolver,否则可能破坏 Harmony 平台包重定向。

修改后需要停掉旧 Metro 进程并重新生成 bundle / 安装 HAP。否则模拟器可能仍然加载 localhost:8081 上的旧 bundle:

lsof -nP -iTCP:8081 -sTCP:LISTEN
kill <PID>
npm run harmony
cd harmony && /Applications/DevEco-Studio.app/Contents/tools/node/bin/node \
  /Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw.js \
  --mode module \
  -p module=entry@default \
  -p product=default \
  -p requiredDeviceType=phone \
  assembleHap

15.3 Codegen 后手写代码丢失

不要把业务实现写进会被 Codegen 生成覆盖的文件。手写逻辑应放在独立 ArkTS 模块中(如 turbomodule/SkuModule.ets),由 Package factory 引用。

15.4 方法签名不一致

先缩小到基础类型和一个异步方法,确认链路跑通后再引入复杂对象、数组、回调或事件。

JS 侧 Spec、Codegen 生成的 TM.RTNSkuModule.Spec、手写 SkuModule 类三者的方法签名必须严格一致。

15.5 修改 ArkTS 后没有生效

React Native JS 可以热更新,但 ArkTS 原生代码通常需要重新构建、安装应用。DevEco Studio 中点击"重新运行"(Run > Run ‘entry’)而非仅刷新 JS。

15.6 C++ 编译报错:undefined reference

如果报 undefined reference to rnoh::RTNSkuModule::RTNSkuModule(...),说明 RTNSkuModule.cpp 没有被编译。检查 CMakeLists.txtfile(GLOB_RECURSE ...) 是否正确,并确认 generated/rtn_sku_module/RNOH/generated/turbo_modules/RTNSkuModule.cpp 文件存在。

Logo

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

更多推荐