RNOH TurboModule 最小实践:从 Native*.ts 到 ArkTS Package 注册
文章摘要(149字): 本文是《React Native x HarmonyOS NEXT 创新能力接入方案》系列第二篇,聚焦实现React Native调用ArkTS原生模块的最小TurboModule闭环流程。通过定义sayHello和formatSkuName两个方法,验证跨端调用链路,为后续TTS、OCR等复杂Kit接入奠定基础。文章详细拆解了TypeScript spec规范、Codeg
本文是《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)TM是generated/ts.ts中export * 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 线程中的 HttpClient、caPathProvider 等配置相互独立。
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
需要做两处修改:
GLOB改为GLOB_RECURSE,因为 v2 codegen 生成的.cpp文件在子目录中- 添加
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
实际验证截图:

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
截图如下:

这个错误不是 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.txt 的 file(GLOB_RECURSE ...) 是否正确,并确认 generated/rtn_sku_module/RNOH/generated/turbo_modules/RTNSkuModule.cpp 文件存在。
更多推荐



所有评论(0)