1. 为什么要在React Native里开发鸿蒙组件
1.1 鸿蒙生态的现状与RNOH的定位
这几年做跨端开发的同行应该都有同样的感受:HarmonyOS NEXT全面去安卓化之后,原本"一套RN代码跑Android和iOS"的舒服日子被打破了。鸿蒙不再兼容APK,意味着所有基于React Native的存量App,想要进入鸿蒙生态,都得重新找路。
这时候"React Native for OpenHarmony"(以下简称RNOH)就成了绕不开的话题。它不是一个简单的SDK,而是把React Native的核心渲染链路完整迁移到鸿蒙OS上的一套适配方案。简单说,你的JS逻辑、React组件树、状态管理这些都不用动,而是把原先映射到Android View或iOS UIView的那一层,替换成鸿蒙的ArkUI组件。
但这里有个关键点很多人没意识到:RNOH并不是"装个包就能跑",它需要你在鸿蒙工程里做一些原生侧的适配。尤其是当你需要调用鸿蒙特有的能力时——比如分布式文件、系统设置、统一扫描等等——就必须自己去写鸿蒙组件,再通过RN的桥接机制暴露给JS侧。
这篇文章要讲的,就是这条路上最核心的一段:如何在React Native项目中开发鸿蒙组件。我会从架构原理讲到实际操作,最后把我踩过的坑也一并列出来,给后面接手的人省点时间。
1.2 技术选型:不是只有一条路
在正式动手之前,得先搞清楚市面上到底有哪几条路可以走。我梳理了一下,目前想在鸿蒙上跑RN业务,大概有三个方向。
第一条是直接上RNOH官方方案,也就是@react-native-oh/react-native-harmony这套仓库。它由OpenHarmony社区维护,适配了React Native 0.72到0.75等版本,覆盖了绝大部分核心组件和API。优点是社区活跃、文档全、遇到问题有人答,缺点是你得用它的模板工程再改造,老项目迁移有一定成本。
第二条是自研桥接层。如果你的RN业务本身很简单,只是想把UI渲染到鸿蒙上,同时又要深度调用鸿蒙系统能力,可以考虑自己用N-API或者ArkTS写一套轻量桥。但说实话,除非团队里有精通鸿蒙底层的人,否则我不推荐这么做,维护成本会把你拖垮。
第三条是放弃RN,直接上ArkTS重写。这在某些场景下确实是更优解,尤其是华为对ArkUI的性能调优支持明显更到位。但这不适合大部分团队,因为业务逻辑复用率会大幅下降。
我个人的判断是:如果你是中小团队、RN代码量已经积累到一定程度,RNOH是当前性价比最高的选择。而"开发鸿蒙组件"这个事,恰恰是RNOH方案里最需要补课的部分。接下来我会从开发环境讲起,一步步带你跑通。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发前的认知准备:理解鸿蒙与RN的架构差异
2.1 HarmonyOS NEXT对RN生态的影响
先花点时间理解鸿蒙的系统分层,这决定了你写原生组件的方式。
HarmonyOS NEXT基于OpenHarmony发展而来,应用层使用ArkTS语言,UI框架是ArkUI声明式范式。它与Android最大的区别在于,没有Java/Kotlin虚拟机,所有应用都运行在方舟运行时之上,原生代码通过N-API或者C-API与系统交互。
这对RN意味着什么?原来在Android上,RN通过ReactViewGroup、AppCompatEditText这类系统控件渲染UI;在鸿蒙上,RNOH把这一层替换成了FrameNode和ArkUI的组件节点。渲染树、事件分发、布局计算,全部走ArkUI的管线。
还有一个容易忽略的点:鸿蒙的Ability相当于Android的Activity,但生命周期模型不太一样。RNOH在鸿蒙里跑起来,本质上是把RN的RootView挂载到一个Ability的加载流程里,由RNOHCoreContext统一管理。
理解了这一层,后面排查问题会轻松很多。很多新手上来就写代码,遇到白屏就懵,其实大部分时候不是代码问题,而是对鸿蒙的组件挂载方式没有概念。
2.2 RNOH的底层运行机制
RNOH的运行机制可以简化成三层来看。
第一层是JS层,也就是你熟悉的React业务代码。它运行在鸿蒙的JS运行时上,RNOH默认使用QuickJS(也可以切Hermes),通过napi与底层通信。
第二层是C++层,RNOH把React Native的Core模块——比如Fabric渲染器、TurboModule管理器、ShadowNode协议——用C++重新实现了一遍,通过OpenHarmony的NDK接口编译成.so库。这里面最核心的是ComponentDescriptor和ComponentManager,它们负责把React元素树转换成ArkUI能识别的节点树。
第三层是ArkTS层。它接收C++层传递过来的指令,去操作真正的ArkUI组件。比如你写了一个<Text>,链路是这样的:JS里创建Text元素,Fabric生成ShadowNode,C++层把它转成ArkUITextNode的操作指令,最后ArkTS层执行Text组件的渲染。
开发鸿蒙组件,本质上就是在这三层之间搭一座新的桥:让RN能识别一个自定义的组件名,让C++层能创建对应的节点,让ArkTS层能渲染出真正的鸿蒙原生UI。记住这个链路,后面写代码的时候你会不断想起它。
3. 开发环境搭建与工程初始化
3.1 工具链准备
RNOH开发环境和普通RN开发有一些区别。你需要准备的基础工具有这些:
- Node.js 18以上,建议直接用LTS版本,版本太老会导致构建脚本报错
- DevEco Studio 5.0及以上,这里是鸿蒙工程的IDE,对应API 12以上的HarmonyOS SDK
- ohpm,鸿蒙的包管理器,类似npm,DevEco Studio会自带,但建议确认下环境变量是否配好
- React Native CLI,创建RN工程用的
有个细节要注意:RNOH对React Native版本有明确要求,你不能拿最新的RN 0.76直接跟RNOH搭配。我目前用的是RN 0.72.19 + RNOH 0.72.32的搭配,跑起来很稳定。你可以在RNOH的官方仓库里查到版本对应表,照着选就行。
还有个小技巧:react-native安装时不要用latest标签,直接指定版本号,避免 unintended upgrade。
3.2 初始化一个RN for HarmonyOS工程
RNOH的工程结构和纯RN不一样,它多了一个harmony目录,里面是鸿蒙原生工程。初始化有两种方式。
第一种,直接用RNOH提供的模板:
bash复制npx @react-native-oh/react-native-harmony@latest init MyRNApp --version 0.72.19
这个命令会帮你创建标准的RN工程,并自动添加鸿蒙侧的工程文件。装完依赖后,你会看到一个结构大致如下的目录:
code复制MyRNApp/
├── android/
├── ios/
├── harmony/
│ ├── entry/
│ │ ├── src/main/
│ │ │ ├── ets/
│ │ │ │ ├── entryability/
│ │ │ │ └── pages/
│ │ ├── oh-package.json5
│ │ └── build-profile.json5
├── node_modules/
├── package.json
└── index.js
第二种,如果你有现成的RN工程,可以手动引入RNOH。这一步稍微繁琐,我建议直接参考RNOH官方仓库里的Upgrade Guidance文档,把harmony目录拷过来,然后执行ohpm install装依赖。
初始化完成后,先在DevEco Studio里打开harmony目录,等同步完成,连接鸿蒙真机或者模拟器,直接build entry模块。如果顺利,你会在手机上看到一个跑着RN页面的App。
这里我特别提醒一句:RNOH构建依赖鸿蒙SDK的版本和ohpm依赖的版本严格匹配,一旦报错,优先检查oh-package.json5里各种@react-native-oh/react-native-harmony相关包的版本,别随便升依赖。
4. 手写第一个自定义鸿蒙组件
4.1 在ArkTS侧创建原生组件
现在到正题了。假设你的业务需要在RN里展示一个鸿蒙原生的Swiper轮播图,或者一个调用了系统能力的ScanView扫码组件,你该怎么把它封装成RN组件?我用一个最简单的自定义组件来做演示。
先在鸿蒙工程的ets目录下新建一个目录components,然后创建CustomTextView.ets文件。ArkTS组件本质上是一个用@Component装饰器声明的struct,和一个普通的ArkUI页面组件没有区别。
ts复制@Component
export struct CustomTextView {
@Prop message: string = 'default';
@State private textSize: number = 16;
private onTextClick: () => void = () => {};
build() {
Column() {
Text(this.message)
.fontSize(this.textSize)
.fontColor(Color.Black)
.textAlign(TextAlign.Center)
}
.width('100%')
.height(100)
.backgroundColor(Color.Gray)
.onClick(() => {
this.onTextClick();
})
}
}
这个组件很简单,就是一个灰色底、带文字的卡片,点击时回调给上层。
但是,光有ArkUI组件还不够。RNOH要求每个原生组件必须注册到一个ComponentManager里,告诉RNOH这个组件叫什么名字、接收哪些属性、如何创建实例。所以还需要创建对应的Manager类。
ts复制import { ComponentManager, RNComponentContext } from '@react-native-oh/react-native-harmony';
export class CustomTextComponentManager extends ComponentManager<CustomTextView> {
constructor(context: RNComponentContext) {
super(context);
}
override get name(): string {
return 'CustomTextView';
}
override createComponentInstance(reactTag: number): CustomTextView {
const component = new CustomTextView();
component.onTextClick = () => {
this.emitComponentEvent(reactTag, 'onCustomTextClick', { message: component.message });
};
return component;
}
override getProps(component: CustomTextView, props: Record<string, any>): void {
super.getProps(component, props);
if (props.message !== undefined) {
component.message = props.message as string;
}
if (props.textSize !== undefined) {
component.textSize = props.textSize as number;
}
}
}
getProps这个方法很关键,RN传递过来的属性,会通过这个方法同步到ArkTS组件实例上。比如RN侧写<CustomTextView message="hello" />,这里的message就会在getProps里被赋值到组件实例。
注册Manager有两种方式,一种是在EntryAbility的onCreate里调用componentManagerFactory的注册方法,另一种是创建一个自定义的RNPackage,在包里注册。我习惯用后一种,因为可以批量管理多个自定义组件。
ts复制import { RNPackage, TurboModuleRegistry } from '@react-native-oh/react-native-harmony';
export class MyCustomPackage extends RNPackage {
createRNPackage(context: RNComponentContext): any {
return {
componentManagers: [new CustomTextComponentManager(context)],
turboModules: []
};
}
}
然后在EntryAbility里,把MyCustomPackage加进RNOH的初始配置里。这一步在RNOH文档里叫addRNPackage。
4.2 在RN侧封装并调用
原生侧注册完成后,RN侧就可以用了。通常我会创建一个封装组件,把原生组件和JS逻辑隔离开。
jsx复制// CustomTextView.js
import { requireNativeComponent, Platform } from 'react-native';
const CustomTextViewNative = requireNativeComponent('CustomTextView');
const CustomTextView = (props) => {
const { onCustomTextClick, ...restProps } = props;
const handleClick = (event) => {
if (onCustomTextClick) {
const { message } = event.nativeEvent;
onCustomTextClick(message);
}
};
return (
<CustomTextViewNative
{...restProps}
onClick={handleClick}
/>
);
};
export default CustomTextView;
这里有几个关键点。
第一,requireNativeComponent的第一个参数必须和ArkTS侧Manager的name完全一致,大小写都不能错,否则运行时直接报"component name not found"。
第二,事件名在跨层传递时有个隐式规则。ArkUT侧我用emitComponentEvent(reactTag, 'onCustomTextClick', ...)发射事件,JS侧监听的是onClick。实际上RNOH会把ArkTS里的事件名自动映射成on{name}的格式,所以JS侧绑定的回调要和on后面的部分匹配。这个映射关系很容易搞混,我的经验是:在getProps之前,先用super.getProps把父类的绑定逻辑跑一遍,再处理自定义事件,否则可能出现属性延迟更新。
第三,nativeEvent里能拿到的内容,来自ArkTS侧emitComponentEvent的第三个参数,它是一个普通对象。如果你想传递更复杂的结构化数据,建议在ArkTS侧先做一层序列化,避免在JS侧收到对象后字段丢失。
到这里,一个最小的鸿蒙组件就打通了。整个链路是:RN JS层创建组件 -> Fabric生成ShadowNode -> C++层通知ComponentManager -> ArkTS层创建CustomTextView实例并绑定属性 -> 渲染到ArkUI -> 用户点击 -> 事件回传JS层。
5. 桥接的进阶:从组件到能力调用
5.1 自定义HarmonyOS组件的生命周期
组件能显示只是第一步,真正复杂的是生命周期管理。你可能会遇到这类问题:RN页面销毁了,但鸿蒙侧的后台任务还在跑;或者页面切到后台再回来,组件状态丢了。
RNOH给每个原生组件定义了生命周期钩子,你可以在ComponentManager里覆写这些方法:
createComponentInstance:创建组件实例getProps:同步属性onDropViewInstance:组件销毁前的清理onHostPause:宿主Ability进入后台onHostResume:宿主Ability回到前台onHostDestroy:宿主Ability销毁
举个例子,如果你的鸿蒙组件里注册了系统广播监听器,最稳妥的做法是:
ts复制override onDropViewInstance(component: CustomTextView): void {
// 释放资源、解绑监听器
component.unregisterListener();
}
你别小看这个环节,我记得有一次就是因为没做销毁清理,页面上反复进出后,内存持续上涨,最后直接OOM崩溃。而且这种问题在纯RN开发里根本不会遇到,因为Android侧有系统帮你兜底一部分。
5.2 属性传递与事件回调
属性传递的坑,主要集中在类型匹配和更新时机上。
RN侧的props类型只有number、string、boolean、object、array这些,而ArkTS侧属性类型更丰富。为了避免隐式转换出问题,我建议在getProps里做显式转换,尤其是数字类型:
ts复制if (typeof props.textSize === 'number') {
component.textSize = props.textSize;
}
如果不做类型判断,JS侧传过来一个字符串"16",ArkTS侧可能就显示不正常了。
事件回调的另一个坑是:频繁触发的事件(比如滚动、拖拽)会在JS和ArkTS之间产生大量跨语言调用,性能会变得很差。我的做法是在ArkTS侧做节流再发射,或者在JS侧做逻辑处理,不要让事件风暴直接打到React的setState上。
如果你需要调用鸿蒙的系统能力,比如读取系统设置、调起系统扫码页面,那就得走TurboModule。TurboModule和自定义组件的区别在于,它不涉及渲染,只是提供能力接口。写法是创建一个继承TurboModule的类,实现SyncTurboModule接口,然后在JS侧通过TurboModuleRegistry.get获取。这个链路相对清晰,RN老玩家应该很熟悉,只是底层从JNI换成了napi。
6. 常见问题与排查技巧实录
6.1 启动白屏问题排查
"react native 启动白屏"几乎是我被问得最多的问题。每次听到这个,我的第一反应都是:你确定你的JSBundle加载完了吗?
RNOH项目启动白屏,原因大致有四类。
第一类是Metro服务连接失败。开发模式下,RN需要连上Metro拉取bundle。如果手机和电脑不在同一个网段,或者Metro端口被占用,就会出现白屏。排查方法是看DevEco Studio的Logcat里有没有报"Unable to load script"之类的错误。
第二类是JSBundle加载慢。HarmonyOS真机首次启动RN应用时,需要解压并加载bundle,这个过程在低端机器上可能会持续3到5秒。如果加载完成前rootView已经显示,就是白屏。解决思路是做一个Splash占位页,等RNOH的onLoadBundle完成后再切到RN页面。
第三类是RNOH版本不匹配。这个问题最常见,比如RN版本是0.72,但RNOH的npm包是0.73的,会出现各种诡异问题。你可以用这个命令自查:
bash复制npm ls @react-native-oh/react-native-harmony
看看安装的版本是不是和react-native版本匹配。
第四类是entry模块里的EntryAbility配置问题。RNOH要求主Ability继承RNOHAbility,如果你写成了普通的UIAbility,虽然页面能起来,但RN的根视图挂不上去,表现出来的也是白屏。检查一下你的EntryAbility.ets里有没有正确调用super.create(contentStorage, context, rnInstance)。
6.2 构建失败与SDK版本不匹配
DevEco Studio构建时报错的场景,我总结成一个速查表:
| 报错特征 | 常见原因 | 处理办法 |
|---|---|---|
| 无法解析依赖 | ohpm没配好或镜像源问题 | 执行ohpm config set registry换国内源 |
| 编译报错找不到符号 | SDK版本与RNOH版本不匹配 | 对照RNOH官网的版本映射表调整SDK API Level |
| ArkTS编译报错类型不匹配 | 旧版ArkTS语法与新SDK不兼容 | 升级DevEco Studio到推荐版本 |
| build后apk/hap安装失败 | 签名证书缺失 | 在DevEco Studio里配置自动签名 |
还有一个比较容易忽视的问题是Node版本。RNOH在构建时要跑一堆脚本,如果你的Node版本太新(比如20以上),有些老版本的脚本会报错。我的建议是直接用项目根目录.nvmrc里指定的版本,或者统一用Node 18。
6.3 自定义组件不显示或崩溃
如果你照着前面的步骤写了自定义组件,但RN页面上根本没有渲染出来,先别急着查代码。依次检查这三处:
一是组件注册名是否一致。Manager里的name属性和JS侧requireNativeComponent()的第一个参数,必须完全一致。注意RNOH内部会做一次驼峰转换,比如custom_text_view和CustomTextView是有区别的,我用的是纯驼峰,两边保持一致最省事。
二是MyCustomPackage是否成功加入RNOH实例。如果你在EntryAbility里没有调用addRNPackage,组件Manager就不会被加载。你可以在Logcat里搜"CustomTextView",看有没有注册成功的日志,没有的话就是这一步漏了。
三是ArkTS编译有没有真正通过。DevEco Studio有时候不会实时刷新编译状态,我遇到过改完代码后运行还是旧版本的情况。稳妥的做法是Build -> Clean Project,再重新run。
7. 实操心得:值得关注的几个坑
7.1 开发体验的差异与适应
在RNOH上做开发,和纯RN开发最大的不同是:你要同时维护JS和ArkTS两套代码,调试时也要在两个IDE之间来回切。
我个人习惯是:Metro跑在VS Code里改JS,DevEco Studio跑鸿蒙工程,两边各开一个控制台。改JS代码时,按一下Metro的r键就能热更新;改ArkTS代码时,必须重新build整个entry模块。这个切换的节奏感,新上手的人需要一点时间适应。
还有一点,RNOH的热更新能力比Android原生弱。Fast Refresh在某些场景下会失效,比如你改了原生组件Manager的代码,Metro刷新后JS侧可能拿不到最新的原生模块。遇到这种情况,不要硬刷,直接把App杀掉重进。
7.2 一个典型的调试流程
最后分享一个我实际采用的调试流程,供参考。
改动一个鸿蒙组件的属性后,建议按这个顺序排查:
- 先在ArkTS侧写好组件,确保单在ArkUI页面里能正常显示
- 再注册Manager,确认Logcat里有注册成功的输出
- 在RN侧用最简单的
<CustomTextView message="test" />渲染,看属性是否传到了ArkTS侧(可以在getProps里加一行log) - 确认渲染OK后,再绑定事件回调
- 最后才加入复杂业务逻辑
每层都验证通过,再往上一层走,出问题时就能快速锁定是在哪一层断了。你直接全写完再测,一旦报错,排查成本会翻好几倍。
另外推荐大家订阅RNOH官方仓库的release页面,这个项目迭代速度很快,每隔几周就会发布新版本,很多坑在更新日志里都有说明。我自己就在升级到0.72.25之后解决了一个长时间困惑的字体渲染问题。
做RNOH开发,心态上要有一个准备:它不是现成的轮子,而是一个需要你理解底层逻辑才能用得顺手的框架。但只要把这套机制打通了,后面在鸿蒙生态里做适配,思路会清晰得多。希望这篇文章能帮你少走一些弯路。
