前阵子团队接到一个需求:现有 React Native 项目要跑上 HarmonyOS,同时还要把几个原生鸿蒙应用里才有的专用组件,一个数据面板,一个图表卡片,接进 RN 页面里。说实话,只改 JS 根本不够,因为“鸿蒙组件”不是普通 npm 包,它长在 ArkUI 这套生态里。所以这篇文章聊的内容,本质上是三件事的结合:鸿蒙开发的基础认知、RN 与鸿蒙的桥接思路、以及真正把一个鸿蒙组件放进 RN 工程里的实操经验。
如果你已经在做 React Native,但头一回碰鸿蒙,或者反过来,你已经写过 ArkUI 组件,但不知道怎么被 RN 复用,这篇文章应该能帮你少踩很多弯路。它解决的核心问题就一个:如何在 React Native 项目里去开发、封装并调用一个鸿蒙组件,同时处理好整个工程的集成方式。
下面我不按官方文档那种“目录式”讲法来,而是按一个真实项目从调研到跑通的顺序来拆。
1. 先理清需求:你要开发的到底是哪一层鸿蒙组件
1.1 三种典型诉求
“在 RN 中开发鸿蒙组件”这个说法其实很模糊。我见过太多人一开始就找错方向,所以第一步不是写代码,而是先确认项目到底属于下面哪一种。
- 第一种,只想要鸿蒙原生应用里的某个页面能力,比如相机扫描、统一扫码、日历提醒,直接从一个 RN 页面跳到那个鸿蒙页面。这种本质上属于“应用集成”,不是组件开发。
- 第二种,想让现有 RN 应用本身能运行在鸿蒙手机上,也就是把 RN 的渲染引擎、JS 运行环境、原生桥接层全部适配到鸿蒙系统上。这种属于“平台迁移”,工作量主要在底层基础设施。
- 第三种,才是真正的“开发鸿蒙组件”。在鸿蒙工程里用 ArkUI 写一个 UI 组件,再把这个组件以原生视图的形式嵌入到 RN 页面中。RN 侧通过 props 传参数,通过事件回调接收鸿蒙侧的反馈。
第三种通常是最容易被低估的。很多 RN 开发者的第一反应是:鸿蒙组件是不是也能像 npm 包一样,直接用 import 引进来?实际上不行。RN 的 JS 代码运行在 JS 引擎里,鸿蒙组件运行在 ArkUI 的 UI 框架里,两者之间必须经过一层原生桥接,并不是同一个运行时。
1.2 不同方案的技术影响
搞清楚诉求之后,技术选型会差非常多。
如果只是做应用集成,那么 RN 工程里不需要深度适配鸿蒙 UI 体系,只需要在鸿蒙端注册一个页面或 Ability,RN 侧通过深链接或路由把参数传过去即可。优点是 RN 代码几乎不用改,缺点是两个端之间通信体验很割裂,你拿不到鸿蒙页面的局部视图,只能整页跳转。
如果是要让 RN 应用跑在鸿蒙系统上,那就得借助社区和厂商维护的 React Native for OpenHarmony 适配层。这类适配不是把 iOS 或 Android 的实现原样搬过去,而是要让 RN 的渲染管线认 ArkUI 的节点树。
如果是要做组件级集成,你需要同时理解三套东西:RN 侧的原生组件封装规范、鸿蒙侧的 ArkUI 组件声明、以及两者之间的数据映射关系。这三块缺一不可,但每一块其实都没有你想象的那么深,关键是先打通一条最小链路。
提示:最忌一上来就追求“大而全”。建议先把第三种组件级的最小 Demo 跑通,再回头补第一种、第二种也不迟。
2. 鸿蒙开发基础速扫:带“RN 视角”去学,而不是零基础重学
2.1 ArkTS 和 ArkUI 到底是什么
很多 RN 开发者听到鸿蒙开发第一反应是“又要学一门新语言”。严格说,ArkTS 并不是一门全新语言,它是在 TypeScript 基础上做了一套更严格的类型约束和语法规范。比如结构化类型检查更严格、不支持太多动态特性、在编译期就能拦截大量类型错误。这对于 RN 背景的人反而友好,因为你已经在写 TS/JS。
ArkUI 则是一套声明式 UI 框架。它的写法很有 React 的味道:
@Component export struct HelloHOSPanel { @Prop title: string = ''; @State count: number = 0; build() { Column({ space: 8 }) { Text(this.title) .fontSize(20) .fontColor('#182431') Text(`点击次数:${this.count}`) .fontSize(16) Button('点击') .onClick(() => { this.count++; }) } .padding(16) .backgroundColor('#F1F3F5') .borderRadius(12) } }这个代码你基本能直接看懂:@Component声明这是一个 UI 组件,build()决定渲染结构,@State表示内部可变状态,@Prop表示接收外部传入的参数。它和 React 组件的关系很像,但不是同一套运行时。
2.2 状态管理对照
RN 里你用useState、useEffect、useCallback,鸿蒙 ArkUI 里有@State、@Prop、@Link、@Watch。刚开始最容易绕晕的是@Prop和@Link的区别。
简单理解:@Prop是单向传值,父组件传一个值给子组件,子组件自己的修改不会同步回去,除非父组件重新创建子组件;@Link才是双向绑定,子组件改了,父组件对应变量也会变。
这和 React 的“状态提升”不完全一样,它更像是编译期帮你生成了一套响应式依赖关系。所以在做 RN 到鸿蒙组件的数据穿透时,我的建议是:鸿蒙组件内部所需的本地状态尽量用@State,需要响应式同步的属性可以用@Prop接收,但不要在组件内部随便改外部传入的引用型对象,否则排错成本很高。
2.3 Stage 模型和 UIAbility 不必深挖,但要懂概念
如果你只是开发一个组件而不是一个完整鸿蒙应用,确实不需要把 Stage 模型研究透。但当你做“RN 工程里集成鸿蒙应用”时,一定会碰到页面生命周期和 Ability 的概念。
鸿蒙现在的应用模型叫 Stage 模型,一个应用可以包含多个模块,每个模块里有 UIAbility、ExtensionAbility 这类系统调度单元。UIAbility 可以理解成系统能直接拉起的一个“应用窗口单元”,类似 Android 里的 Activity。RN 要跳转到鸿蒙原生页面,通常就是通过 Want 对象去拉起一个 UIAbility。
我在最开始犯过的错误是:把这套东西当成 JS 里的路由来理解。实际上它不是前端路由,而是系统级进程调度层面的能力。所以你在设计集成方案时,需要考虑的不只是页面参数,还有前台后台切换、任务栈、系统对敏感权限的授权时机。
注意:RN 侧的页面路由和鸿蒙侧的 UIAbility 是两套体系,不要尝试用 JS 侧的路由参数直接控制原生页面栈,跨边界的地方尽量只传简单的可序列化数据。
3. RN 侧如何挂接鸿蒙组件:三层结构与通信边界
3.1 选择哪条适配路线
如果你的 RN 应用要跑在鸿蒙上,通常会接触到一个叫 React Native for OpenHarmony 的社区适配层。它把 RN 的 native 层能力移植到了 OpenHarmony/HarmonyOS NEXT 上,让同一个 JS 业务包可以被 Android、iOS、鸿蒙三端复用。
我用下来的感受是:这套适配层已经能跑常见的页面和第三方库,插件生态不如 Android/iOS 丰富,但基本骨架已经有了。选路线时不要自己造轮子,优先看官方或开源社区维护的 example 工程。原因很简单:鸿蒙系统和 RN 版本都在快速迭代,自己从零适配会碰到大量底层 API 变更,尤其渲染、线程调度、事件注入这几块,没有社区踩坑记录很难定位。
3.2 组件集成的三层结构
把一个鸿蒙组件塞进 RN 页面里,按我习惯的拆法可以分成三层。
最外层是 JS 组件层。RN 业务代码只认识一个普通的 React 组件,组件接收 props,并通过回调接收事件。这一层对业务透明。中间是 native bridge 层。它负责把 JS 侧的字符串、数字、方法名映射成鸿蒙侧能识别的数据结构,同时把鸿蒙侧的事件回传转换成 JS 回调。最内层是 ArkUI 原生实现层。这一层才是真正写 UI 和业务逻辑的地方。
很多刚接触的人会在这中间堆很多重复代码,每个属性都手动一个 setter,每个事件都手动发一次。实际项目里不用这么细,只要把“页面级参数”和“高性能交互事件”分开即可。像 title、禁用态这类低频属性,可以在 UI 组件初始化时一次性传入;像手势、滑动、点击坐标这高频事件,必须走事件通道,不能通过属性轮询。
3.3 集成鸿蒙应用时的生命周期衔接
当你在 RN 工程里跳转到一个鸿蒙原生页面时,会面临两个生命周期并存的问题。RN 侧有 React 组件的componentDidMount、卸载逻辑,鸿蒙侧有 UIAbility 的onCreate、onForeground、onBackground、onDestroy。
我第一次做混合跳转时,在 RN 页面销毁后,原生页面还在往前台推,结果整个 app 出现了页面栈错乱。后来统一处理方式:RN 发起跳转前,先把 RN 侧需要保持的状态持久化到本地;原生页面回来后,再通过事件告诉 RN 重新拉取状态。不要指望原生页面弹栈后 RN 页面还保留着原封不动的内存状态,系统可能在后台回收。
4. 实操:在 RN 工程里跑通一个鸿蒙组件
4.1 环境准备与版本搭配
把这部分单独放一章,是因为环境不一致导致的失败占了此类项目的 70% 以上。如果你直接拿老版本 RN 工程套新版鸿蒙 SDK,光编译错误就能劝退一批人。
我当前工程使用的是一套相对保守的组合,也是 RN for OpenHarmony 社区测试较多的版本线:
| 层级 | 我使用的方案 |
|---|---|
| RN 版本 | 0.72 及以上的 release 版本,尽量不追 0.76 刚发布首日 |
| 鸿蒙 IDE | DevEco Studio 官网当前稳定版 |
| 系统 API | API 10 以上,具体看适配层说明 |
| RN 适配层 | react-native-harmony / RNOH 社区发行包 |
| 调试设备 | HarmonyOS NEXT 真机或官方模拟器 |
为什么特意强调“别追最新”?因为 RN 本身升级频率已经很高,加上鸿蒙适配层还有一个编译间隙。真要做生产项目,版本锁定很重要。先跑通,再慢慢升,不等于不升级。
接入流程大致是这样:先申请鸿蒙应用签名,在 DevEco Studio 新建一个空工程确认能装到真机;再把 RNOH 模板工程加载进来;最后才把你的业务 JS 包接进去。我建议第一次做时不要直接操作现有业务仓库,先拉一个最小工程练手。
4.2 在鸿蒙侧创建并封装一个组件
以我们实际做过的数据面板为例。在鸿蒙工程的ets/components目录下新建组件,然后用 ArkUI 实现展示逻辑。注意:不是随便写一个 struct 就算完成了,最终目标是要让这个 struct 能被一个独立入口包装起来,以便 RN bridge 能实例化它。
组件本身很简单,可以先用一个小面板验证链路:
@Component export struct SampleArkUIPanel { @Prop title: string = ''; @State currentValue: number = 0; build() { Column({ space: 12 }) { Text(this.title) .fontSize(18) .fontWeight(FontWeight.Bold) Row({ space: 16 }) { Text(`值:${this.currentValue}`) .fontSize(16) Button('增加') .onClick(() => { this.currentValue++; }) Button('重置') .onClick(() => { this.currentValue = 0; }) } } .width('100%') .padding(16) .backgroundColor('#F1F3F5') .borderRadius(16) } }这里我先不展开具体 bridge 全量代码,因为不同适配版本生成的模板差异很大。核心流程是:在鸿蒙侧实现一个原生组件控制器,把组件名称注册成类似SampleArkUIPanelView的名字;RN 侧用同名引用去索取这个原生视图。
4.3 RN 侧封装组件并传递数据
RN 侧的工作可以拆成三步。
第一步,建立原生组件引用。经典写法就是 requireNativeComponent,新架构下对应 codegen 生成的方式,概念没有本质变化:
import { requireNativeComponent } from 'react-native'; const SampleArkUIPanelView = requireNativeComponent('SampleArkUIPanelView');第二步,在 JS 里封装一个业务组件:
export function SampleArkUIPanel({ title, onChangeValue }) { return ( <SampleArkUIPanelView title={title} onValueChange={(event) => { onChangeValue?.(event.nativeEvent.value); }} /> ); }第三步,在页面里像普通 RN 组件一样使用。让我印象最深的一点是:不要试图把一个大的 JS 对象直接塞给原生组件,鸿蒙桥接层对复杂对象的处理不像 WebView postMessage 那样自由。传对象最好先 JSON.stringify 成字符串,事件回传时再解析,能规避大量兼容问题。
4.4 构建与真机验证
工程配置完成后,第一次联调建议跑一个流程顺序:先启动 RN packager,再安装鸿蒙应用。有些版本的 RNOH 在 debug 模式下依赖 JS Bundle 从电脑动态加载,如果先启动应用再启动 Metro,鸿蒙侧会因为拿不到 bundle 而白屏。
真机验证时要注意数据线连接。HarmonyOS 设备默认开启 USB 调试后才能和 DevEco Studio 交互。连上后先看开发工具的设备列表里有没有识别到,再跑构建。构建成功不等于组件真的渲染对了,还要打开 DevEco 的 HiLog 或设备日志,确认组件生命周期是否执行。
我通常会在鸿蒙组件侧把 onPageShow、build 完成日志打出来,RN 侧也在 componentDidMount 里打一条日志,通过两边日志的先后顺序快速定位问题出在 JS 层还是原生层。
5. 常见问题与排查技巧实录
5.1 RN 启动白屏
白屏问题是 RN 鸿蒙化过程中返修率最高的问题,没有之一。最常见的现象是:应用能启动、原生 logo 能显示,但整个 RN 页面一片空白。
排查思路不要乱,按顺序来。先确认 Metro 是否在运行,再看电脑和真机是否同一个局域网,最后看 bundle URL 配置是否正确。还有一个老坑是 Metro 端口被占用。直接改端口不是最佳方案,我习惯查一下是哪个进程占用了 8081,而不是每次都换一个端口,否则 RN 页面里配置的 bundle 地址也要跟着变。
如果是真机调试,注意鸿蒙侧的端口映射命令。Android 上通常用 adb reverse,鸿蒙侧适配环境里的调试桥也提供了类似能力。执行完映射后,再在设备上冷启动应用,白屏大概率会消失。
5.2 调试桥连不上,以及无线调试问题
开发工具连不上设备时,先别急着重装驱动,要先区分是普通 USB 充电模式还是开发者模式。鸿蒙手机需要用开发者选项里允许 HDB 调试相关开关,每个版本入口名称略微不同,但都在“开发者选项”里。网上很多旧教程会写命令行叫 hdb,实际新版本工具链里名字可能已经改成 hdc,以你电脑上 DevEco Studio 自带工具目录里的文件名为准。
无线调试我实践下来比较稳的做法是:电脑和设备连同一个路由器,先 USB 连一次获取设备标识和网络地址,然后用hdc tconn 设备IP:端口建立连接。连接后不建议立刻拔掉 USB,先跑一次看日志是否正常,确认无线通道可用后再脱离数据线。
5.3 自定义鸿蒙组件不显示
如果 Metro 正常、RN 页面也渲染了其他 JS 内容,但自定义鸿蒙组件那块区域是空的,问题大概率出在组件名映射上。
RN 侧 requireNativeComponent 的字符串必须和鸿蒙侧注册的原生组件名完全一致,大小写都不能错。很多组件名带 View 后缀,鸿蒙侧也一定要一致。还有一个原因是组件没有宽高。原生组件如果自身没有内容撑起尺寸,RN 布局引擎把它当成一个宽高为 0 的节点,肯定看不见。解决方法是先在鸿蒙组件 build 里设置明确的 width 和 height,或者给外层容器一个layoutWeight,确认能显示后再逐步放开。
另外,鸿蒙侧的@Prop接收外部属性时,如果 RN 第一次渲染给的值是 undefined,而原生侧没有设置默认值,组件可能直接跳过更新。这一点比 Android/iOS 更脆弱,因为 ArkUI 的响应式更新系统对 undefined 处理很谨慎。RN 侧封装组件时,每个原生命名属性最好给一个默认值。
5.4 通信数据不一致或事件乱序
RN 和鸿蒙之间的事件通常是异步回调。最常见的问题是快速连续点击按钮时,鸿蒙侧把结果一直往 JS 发,JS 侧收到事件后立刻更新 state,然后又把新值回传,导致你看到 UI 在几个值之间来回跳。
我在这个项目上采取的办法是:高频事件统一走“事件通道”,只告诉 JS“数据变了,请主动查询最新值”,低频属性再走普通 props。这样虽然多一次查询,但避免了回环。尤其在刷帧类的交互事件上,这个思路几乎是必须的,否则哪怕只延迟几百毫秒,用户都会明显感觉卡顿。
5.5 一个可复用的工程检查清单
最后我把现场排查时常用的清单列出来。每次集成出问题,先照着走一圈,能省掉大量重复提问。
| 检查项 | 说明 |
|---|---|
| 包管理器依赖 | RN 适配层版本是否和 RN 主版本匹配 |
| Metro 是否启动 | debug 模式下有没有正常监听 8081 端口 |
| 鸿蒙 IDE 是否识别设备 | 确认不是在充电模式 |
| bundle 地址 | 是否指向电脑局域网 IP,不是 localhost |
| 原生组件名 | 两边名称大小写是否完全一致 |
| 组件宽高 | 是否被 RN 布局压缩成 0 |
| 日志 | 鸿蒙侧是否执行到组件 build 生命周期 |
| 数据类型 | 是否传了复杂对象导致桥接序列化失败 |
这个清单我在团队内部整理过几轮。新来的同事第一次做鸿蒙组件集成,只要不是项目级结构性错误,照着顺序排查基本都能自己定位。
说回我自己的体会。这个项目最让我受益的一步,不是把组件跑通的那一刻,而是在动手前仔细确认了“集成鸿蒙应用”和“开发鸿蒙组件”是两条路线。如果你本来只想在 RN 里嵌套鸿蒙组件,却被“集成鸿蒙应用”这个词带偏,去研究整个 UIAbility 生命周期,就会多走很大一段路。先把链路切成小段,每段都有一个能验证的节点,后面再填肉时会快很多。