1. “鸿组件”到底是什么:先把 RN 开发者最陌生的三层基础补齐
先说标题里那个“鸿组件”。这不是官方术语,我刚看到时也愣了一下,结合上下文才反应过来——它指的就是鸿蒙(HarmonyOS)组件。如果你第一眼把它读成“红组件”或“鸿蒙组件”,不奇怪,这个说法在社区里并不通行,我按原意用下去。
作为长期做 React Native 的开发者,我对“组件”这个词的第一反应是“可复用的 UI 封装”:一个自定义 View、一个业务模块、一套 props 接上就能用的黑盒。但放到鸿蒙场景里,“鸿组件”至少要跨三个层面去理解:操作系统层、应用框架层、跨端桥接层。这三个层面如果没理顺,后面所有集成工作都会变成对着文档猜谜。
1.1 鸿蒙 OS 与“分布式”到底是怎么个分布法
从官方定义看,鸿蒙 OS 是华为开发的一款面向全场景的分布式操作系统。这里的关键词不是“操作系统”,而是“分布式”。我最初以为“分布式”是营销话术,真正开始接鸿蒙原生能力后才明白,它体现在三个具体能力上:
- 分布式软总线:多台设备之间可以不依赖公网 Wi-Fi,直接在近场自动发现、组网、通信。开发者拿到的是类似本地 Socket 的调用体验,底层已经帮你处理了设备发现。
- 分布式数据管理:把多台设备上的数据库、文件、键值存储统一成一个逻辑视图。你在一台设备写入的数据,另一台设备可以在满足条件下读到。
- 分布式任务调度:一台设备上的应用可以跨端拉起另一台设备上的 Ability。比如手机上的视频任务,可以无缝流转到平板上继续播。
对 RN 开发者来说,这意味着:如果你只是在鸿蒙上重新跑一遍自己的 JS 页面,那和做 Android/iOS 适配没有本质区别;但如果你想发挥鸿蒙的特色,就必须通过它提供的接口,去拿“跨设备协同”的能力。这部分能力在 Android 和 iOS 上根本没有对等物,必须走鸿蒙原生侧。
另外要记住一个现实:鸿蒙和 Android 的关系在不同版本上差别很大。老版本可能还包含 AOSP 兼容层,但新版本已经彻底去掉了 Android 兼容。这不是简单的“换了层皮的 Android”,API 体系、编译链、包结构都不一样。如果你按照 Android 上“包一层 WebView 跑 RN 页面”的思路去做鸿蒙集成,会遇到比想象中多得多的坑。
1.2 鸿蒙应用开发的最小知识集
我原本以为做 RN 集成可以“绕过”鸿蒙原生开发,直接把 JS 逻辑搬过去。实践证明绕不过去,至少需要掌握下面这些最小知识:
- 开发工具是 DevEco Studio:它基于 IntelliJ IDEA,界面和 Android Studio 很像,但工程结构、构建命令、签名体系都是另一套。工程文件后缀是
.ets、.ts、.json5,这一点别搞混。 - 主力语言是 ArkTS:它在 TypeScript 基础上做了一套声明式 UI 扩展,语法上有点像 SwiftUI + Flutter 的结合体。RN 和 JS 开发者上手其实不算难,难的是遇到 ArkTS 对严格类型和动态类型的限制时,处处觉得被束缚。
- UI 框架叫 ArkUI:它用
@Component、@Entry、@State这类装饰器描述页面和组件状态。如果你用过 Flutter 的 Widget 或 Vue 的单文件组件,理解起来很快。 - 应用模型是 Stage 模型:应用由 Ability(类似 Android 的 Activity)组成,一个应用可以有一个或多个 Ability,模块通过 HAP/HAR/HSP 形式组织。RN 的页面最终必须落在一个 Ability 上才能显示。
这些基础内容不需要你变成鸿蒙专家,但至少要能读懂鸿蒙原生示例代码,知道去哪里声明权限、去哪里配置模块路由、怎么导出一个原生模块给外部调用。否则你连“对方给的鸿蒙 SDK 该怎么接进 RN 工程”都判断不了。
1.3 为什么现有 RN 代码不能直接跑在鸿蒙上
很多人一开始会问:RN 不是跨平台吗?把 Android 的 so 库换一下不就行了?答案是:RN 的跨平台依赖的是一套原生运行时和框架适配层,而鸿蒙不在官方支持的平台列表里。
RN 的底层结构是这样:
- JS 层:通过 Metro 打包成 JavaScript Bundle。
- C++ 层:执行 JS 的 Hermes 引擎或 JSC,以及 Fabric 渲染器、TurboModule 等核心逻辑。
- 平台层:把 C++ 层渲染指令转换成平台原生的 View/组件,并提供原生模块给 JS 调用。
Android 能跑 RN,是因为官方为 Android 写了 Fabric 的 Android 实现,和一套基于 Java/Kotlin 的原生模块桥。iOS 同理。鸿蒙想要跑 RN,需要有人把“平台层”实现一遍,还包括生命周期怎么对接、触摸事件怎么分发、网络库怎么替换等一大堆底层适配。这不是社区十天半个月能肝完的,也不是简单地把android/目录复制一份改个名就行的。
所以如果你听到“RN 支持鸿蒙了”这句话,要先搞清楚它说的是哪种支持。目前最主流的是“RNOH(React Native OpenHarmony)”这个社区方案,它把 RN 的框架层移植到了鸿蒙的 C++ 运行时上,JS 侧 API 基本不变。这样你的 RN 页面确实可以跑在鸿蒙设备上,但原生模块、三方原生 SDK 不一定都能用,很多 Android 上的原生依赖需要重新适配。
2. 在 RN 项目里集成鸿蒙应用的几条路线:从最省事到最彻底
先别急着写代码。这个场景和“从零开发一个应用”不一样,你手上大概率已经有一个 RN 项目,现在要把它跟鸿蒙产线对接。我实践下来,靠谱的路线只有三条,适合的情况完全不同。
2.1 路线一:用 RNOH 让 React Native 直接跑在鸿蒙上
这是“最省事”的路线,也是 RN 官方技术生态在鸿蒙上最自然的一种落地方式。简单说,RNOH 把 React Native 的核心 C++ 层移植到了鸿蒙系统上,你不需要改页面代码,JS 侧照常写。
大致要做的步骤:
- 准备一个基于鸿蒙的 RN 工程模板(社区仓库里有现成的)。
- 把原有的
jsbundle通过打包脚本输出出来。 - 在鸿蒙工程里加载这段 bundle,并且把需要的原生模块按需适配。
- 用 DevEco Studio 构建出 HAP 包,再安装到鸿蒙设备上。
这条路适合业务逻辑以 JS 为主、原生依赖不多、已经厌倦双端重复开发的团队。它的问题是:RN 的版本必须跟 RNOH 维护的版本对齐,不能随便升级;而且如果你的 RN 项目里用了很多自定义原生模块,那些模块在鸿蒙上大概率要重新配对或找替代,不能直接继承 Android 端的原生实现。
从实际体验来看,RNOH 能跑通“页面渲染 + 基础 API(fetch、Storage、DeviceInfo)”这些常规业务,但遇到推送、蓝牙、定位等硬件相关的模块,就要自己动手去写鸿蒙侧适配。这也直接决定了你要不要考虑路线二。
2.2 路线二:通过自定义原生模块嵌入鸿蒙组件
如果你的需求不是“把整个 RN 应用搬到鸿蒙上”,而是“在 RN 的应用壳子里,插入一个鸿蒙原生组件”,那路线二更合适。
这个思路其实和 Android/iOS 上的“自定义原生 View 嵌入 RN”完全一样:
- 在鸿蒙侧实现一个组件或模块,用 ArkTS 或 C++ 写成原生的实现。
- 通过 RN 的 TurboModule 或自定义原生组件机制,暴露给 JS 调用。
- 在 RN 的 JS 侧通过
requireNativeComponent或TurboModuleRegistry.get拿到这个组件,按普通 RN 组件使用。
这条路适合的场景包括:把一个已经写好的鸿蒙播放器、扫码 SDK、地图组件、分布式能力封装接入 RN 工程;把鸿蒙特有的跨端能力(比如分布式数据读写)封装成 JS 友好的接口提供给 RN 业务层调用。
路线二最大的好处是局部改造,不需要把整个 RN 应用“鸿蒙化”。但代价是你必须深入掌握鸿蒙原生开发和 RN 的原生桥接机制。接下来的第 3 章我会给出一份可操作的最小实现,你照着走一遍就能体会到这套流程的真实路况。
2.3 路线三:把鸿蒙应用“包装”成 RN 混合页面
这条路线其实不算是“在 RN 中开发鸿组件”,而是反过来:鸿蒙应用作为宿主,RN 页面作为其中的一个模块加载。策略上更像“混合应用”。
我见过不少从原生 App 向鸿蒙迁移的团队用这种方式:在鸿蒙主工程里创建多个 Ability,其中一个 Ability 加载 RN 页面,另外的 Ability 直接使用 ArkUI 原生页面。业务上需要快速迭代的部分(比如活动页、电商详情页)用 RN 做,需要深度体验系统能力的部分(比如相机、会议协同)用鸿蒙原生做。
这条路适合大型应用做渐进式迁移,团队在很长一段时间内可以维持鸿蒙开发和 RN 开发两套班底。缺点是技术栈割裂,两边的工程师必须互相理解和配合,否则容易陷入“什么都想双写,什么都没写好”的境地。
为了帮你在开工前快速判断,我整理了一份对比:
| 路线 | 核心动作 | 适合场景 | 主要成本 | 鸿蒙原生能力访问程度 |
|---|---|---|---|---|
| RNOH 移植 | 把 RN 框架迁到鸿蒙上,JS 代码复用 | 纯 JS 业务、跨端复用的团队 | 版本对齐、原生模块适配 | 需要额外桥接 |
| 自定义原生模块 | 在 RN 工程里嵌入鸿蒙原生组件 | 需要复用已有鸿蒙 SDK,或发挥鸿蒙特色能力 | 原生开发能力强、桥接维护 | 直接可调 |
| 混合应用 | RN 和鸿蒙页面共存 | 大型应用渐进式迁移 | 团队配置、工程复杂度 | 可直接调 |
我的建议是:如果团队已经有 RN 业务,先走路线一打通链路,出一个能跑的 demo;然后再按业务模块逐步替换成路线二的方案,把真正依赖鸿蒙能力的部分做成自定义组件。绝对不要在刚起步时就选“全量鸿蒙化 + 全量 RN 化”,那会让你陷入双份维护的泥潭。
3. 手动封装一个鸿蒙原生组件到 RN:一次完整的最小实现
这一章我带你把路线二完整跑一遍。先泼一盆冷水:这不是三五十行代码能讲清楚的事,原生桥接的工程细节非常多,我这里会给你一个“能跑的最小闭环”,剩下的细节要靠你在真实项目里踩完再填。
3.1 准备工程:鸿蒙侧模块与 RN 侧工程各自初始化
先准备鸿蒙侧工程:
- 打开 DevEco Studio,创建一个Empty Ability工程,包名和 RN 工程保持一致或映射好。
- 在工程里添加一个 Module(比如叫
harmony_component),这个 Module 会由 RN 的NativeModules调用。 - 确认
build-profile.json5里的签名配置,注册你自己的调试证书。没有真机时,也可以用签名后的 HAP 包做本地验证。
RN 侧的准备相对简单:
- 把鸿蒙工程目录放到 RN 项目根目录,例如
harmony/子目录。 - 安装 RNOH 提供的桥接依赖包(具体包名以当前 RN 版本对应为准,不要拿旧教程的版本往新工程里塞)。
- 配置 Metro 的
harmony平台字段,让打包器能生成鸿蒙可加载的资源路径。
这里有个我最初踩过的坑:鸿蒙工程和 RN 工程不要各自单独初始化一套 Node 依赖。RNOH 方案里,鸿蒙工程会依赖 RN 的react-native源码包,版本必须锁定一致,否则会出现方法签名对不上、运行期闪退的问题。
3.2 编写鸿蒙侧自定义组件(ArkTS + NAPI 桥)
我们先做一个最简单的组件:一个显示“Hello from HarmonyOS”的原生视图。后续你想接分布式能力、跨设备文件读写,逻辑都差不多。
先看 ArkTS 侧的自定义组件原型:
// harmony_component/src/main/ets/components/HelloComponent.ets @Component export struct HelloComponent { @Prop message: string = 'Hello from HarmonyOS' build() { Column({ space: 10 }) { Text(this.message) .fontSize(20) .fontWeight(FontWeight.Bold) Text('This is a native HarmonyOS component rendered inside React Native') .fontSize(12) .fontColor('#666666') } .padding(20) .backgroundColor('#f5f5f5') .borderRadius(8) } }接着是暴露给 RN 的桥接入口。RNOH 里比较常见的做法是写一个基于 TurboModule 的原生模块,把组件实例抛给 RN 侧。我举个更贴近原生模块导出的例子,假设我们要暴露一个返回设备信息的方法:
// harmony_component/src/main/ets/RNBridge.ts import { TurboModule } from '@rnoh/react-native-openharmony' export const NativeHelloModule = TurboModule .getEnforcing<TurboModule>('RNHelloModule') // 这里通过原生方法把自定义组件的渲染参数抛给 ArkUI export function createHelloMessage(name: string): string { return `Hello, ${name}! You are running on HarmonyOS.` }别指望上面这段代码直接复制就能跑。真实工程里你至少要处理三件事:
- 在
module.json5里声明模块被外部引用需要的权限和依赖。 - 如果是 C++ 接口,还要配置
CMakeLists.txt和 NAPI 注册表。 - 如果是 ArkTS 接口,你需要在框架里显式注册这个模块,让 RN 的 TurboModuleManager 能找到它。
这三项属于“你说它繁琐吧,其实不难;你说它简单吧,错过一个就白屏”的典型情况。我建议你在建工程时先从 RNOH 的官方示例工程复制一份ets目录,然后在它的基础上改,不要从空白工程开始慢慢摸索“原生模块该怎么注册”。
3.3 在 RN 端注册组件并调用
RN 侧的调用代码比较轻:
// App.tsx import React, { useEffect, useState } from 'react' import { View, Text, Button, DevSettings } from 'react-native' import { NativeModules } from 'react-native' const { RNHelloModule } = NativeModules export default function App() { const [nativeMessage, setNativeMessage] = useState('') useEffect(() => { const message = RNHelloModule?.createHelloMessage?.('RN Dev') setNativeMessage(message ?? 'module not available') }, []) return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text style={{ fontSize: 16 }}>{nativeMessage}</Text> <Button title="Reload Native Component" onPress={() => DevSettings.reload()} /> </View> ) }到这里,你其实已经体验到了“鸿组件”接入的最核心链路:
- 鸿蒙侧写一个原生模块/组件。
- 通过桥接层注册。
- JS 侧像调用普通方法/普通组件一样使用它。
剩下的事情,比如渲染自定义视图,需要再走一层requireNativeComponent,把组件和名称绑定一遍。这一步网上资料很多,但大多遗漏了“鸿蒙侧必须把组件包成一个独立 ViewManager”的细节。如果你发现requireNativeComponent一直报“未知组件”,先回鸿蒙工程确认 ViewManager 是否已经注册到RNOH的组件管理器里,这个排查顺序比在 JS 侧改代码有效率得多。
4. 调试环节最容易卡住的几个点:白屏、链接失败、资源路径
我见过太多人卡在不是思路问题上,而是调试环境问题上。这一章我把高频问题拆开讲,尤其是“启动白屏”和“没有模拟器/真机怎么调”,这是搜索热词里出现频率最高的两类。
4.1 启动白屏的排查顺序
“React Native 启动白屏”放到鸿蒙场景里,比 Android/iOS 更让人头大,因为问题可能出在四层里的任何一层。我按自己的排查顺序给你一份清单:
- Metro 服务是否在跑,以及 JS Bundle 是否打出来了。鸿蒙端如果用的是离线 bundle 模式,Metro 没跑起来,页面就是白屏。检查 Metro 日志,确认请求到本地 8081 端口后返回的不是 404。
- bundle 路径是否匹配。鸿蒙工程里加载的 bundle 路径和 Metro 输出资源路径必须一致。最常见的问题是大小写不一致,Windows 和 Mac 的大小写规则不同,导致鸿蒙侧加载失败。
- 原生模块是否注册成功。如果 RN 侧调用的原生模块在鸿蒙侧没有被注册,不会直接抛一条“Module not found”,而是变成静默的白屏或 undefined。你可以在鸿蒙侧日志里搜
TurboModule或RNHelloModule相关输出。 - C++ 运行时是否崩溃。RNOH 是 C++ 层移植,如果鸿蒙工程引用的 NAPI 接口和 RNOH 版本不匹配,可能出现初始化即崩。这个崩不一定会打日志到 DevEco 控制台,需要看 crash 日志。
很多人一白屏就开始怀疑是不是自己写的鸿组件代码有问题,结果查半天发现只是 Metro 没启动。我的建议是:先跑通 RNOH 自带的示例工程,再往里面加自己的鸿组件。这样至少可以把“RNOH 基础链路有没有问题”排除掉。
4.2 没有虚拟机和手机时能怎么办
先斩钉截铁给结论:可以做一部分调试,但没有真机,你没法完整验证鸿组件运行效果。
鸿蒙开发不像 Android 有灵活的模拟器体系,但你可以用以下方式做“无设备调试”:
- 使用 DevEco Studio 的 Previewer。它可以在 IDE 里预览 ArkUI 页面效果,适合验证 UI 布局和组件样式。但它只能预览
@Entry页面,无法完整运行 TurboModule 或真实的 Native Event,所以它只能帮你确认鸿组件有没有“长相对”,不能确认“能跑”。 - 使用命令行构建 HAP 包。不用等 IDE 界面启动,直接执行
hvigorw任务构建 HAP。构建成功至少说明编译链没问题,ArkTS 语法和资源引用没问题。 - 使用鸿蒙官方云调试/远程真机。有条件的话可以申请远程真机资源,它能帮你验证系统 API 和分布式能力,比自己没设备硬猜强得多。
我在没有真机的那段时间,策略是:用 Previewer 检查 UI,用命令行检查编译,用 RNOH 单测跑纯 JS 逻辑。但等真机到了之后,第一次跑就发现之前“必定没问题”的自定义组件渲染失败。原因是 Previewer 不会执行 NAPI 调用,很多原生方法在预览环境里直接不存在。所以如果你的目标是调通一个鸿蒙原生组件,确实绕不开真机。
4.3 调通后仍然要注意的“真机感知”问题
即使真机调试跑通,也别开心太早。下面这几个“真机感知”问题我只在实际设备上遇到过,Previewer 和命令行完全发现不了:
- 权限弹窗:部分系统能力(比如定位、分布式数据)第一次调用时,需要一次性授权弹窗。如果你的测试账号没有手动点掉权限,第二次启动就可能白屏或功能失效。
- 屏幕适配:鸿蒙不同设备的折叠屏、平板、手机布局差异很大。有些自定义组件在手机竖屏上完美,在折叠屏内屏上直接显示溢出。
- 系统版本差异:API 版本不同,部分接口可用范围也不同。比如你用的能力只在 API 12 及以上,在 API 11 设备上编译能过,运行期调用就挂。
我在一次真实接入中遇到过一个典型的例子:鸿蒙侧的分布式任务调度接口在 Previewer 里根本不执行,但编译和安装都正常。直到真机上跑,才发现设备没有加入同一个“超级终端”环境,一直报找不到目标设备。这种报错不算代码 bug,但极度容易误导人,排查了两天。
5. 从“能跑”到“稳”:我在实际项目里积累的经验与建议
跑通 demo 只是第一步。真正要把“RN + 鸿组件”组合用到生产环境,需要的不只是技术能力,还有工程规范和团队协作策略。这一章既是经验总结,也是我给后来者的一些务实建议。
5.1 用脚本串联构建流程,别依赖 IDE 按钮
DevEco Studio 的工程构建默认走 IDE 界面,但生产环境里你不可能每次发版都让人去点按钮。我建议在项目根目录维护一套构建脚本,把下面这些动作串联起来:
- 执行 Metro 打包,生成鸿蒙可加载的 JS Bundle。
- 调用
hvigorw构建 HAP。 - 使用
hdc命令安装到指定设备。 - 自动启动应用并抓取日志。
脚本化的好处不只是减少重复劳动,更重要的是它能固化“构建顺序”。RN 和鸿蒙两侧的资源依赖是有先后的,如果先构建鸿蒙工程再打 JS Bundle,你会遇到“页面加载到了但组件找不到”的灵异问题。把顺序写死在脚本里,能避免团队新人乱操作。
5.2 性能与包体积:能少桥接就少桥接
RN 与鸿蒙的每次桥接调用,都有跨语言开销。如果你在鸿组件里一次性查完数据、打包成 JSON,再通过一次桥接抛给 JS 侧,性能会好很多。怕就怕业务层“用哪调哪”,一个页面反复调十几次鸿蒙原生方法,跳帧和卡顿就来了。
包体积方面,鸿蒙工程引用多少原生 SDK、桥接层带多少依赖,最终都会体现在 HAP 体积上。如果你的目标是非纯鸿蒙应用,建议把不常用的分布式能力做成“动态加载”而不是“随主包一锅端”。这一点和 Android/iOS 上控制包体积的思路一致,但鸿蒙的模块机制更细,规划不好容易踩“模块间循环依赖”的问题。
5.3 和 uni-app、Flutter 等跨端方案对比时,别只看宣传语
在搜索热词里,很多人拿“uniapp 开发微信小程序 vs Android/iOS/鸿蒙”和“React Native 鸿蒙”对比。我的真实感受是:不要只看“某某厂商宣布支持鸿蒙”的新闻稿,一定要看它支持到什么程度。
- RN 对鸿蒙的核心优势是:如果你本来就有 RN 代码库,迁移成本最低;JS 生态庞大,社区组件多。
- uni-app 的卖点是“一次编码,多端编译”,它确实也覆盖鸿蒙,但它的运行机制多了一层编译转换,遇到鸿蒙原生硬件能力的细节时,调试起来比 RN 更难透出问题的本质。
- Flutter 同样有鸿蒙支持,渲染性能和动画表现在跨端方案里偏上,但它和 RN 一样要面对“原生插件缺失”的问题,而且 Dart 生态相比 JS 更需要自己造轮子。
所以我不太建议因为你看到某个框架的鸿蒙支持“看上去很美”就全盘迁移。先把你自己当前项目里最核心的三五个页面列出来,在几个框架都跑一遍,对比原生能力、包体积、启动时间,这个数据比任何第三方评测都可靠。
最后再分享一个我个人经验里很重要的细节:在鸿蒙开发中,建立“快速看见效果”的正反馈节奏比什么都重要。我见过不少团队卡在中途,不是技术问题,而是前期迟迟搭不出最小集成 demo,士气被消耗没了。不妨先用一个最简单的 HelloWorld 打通整条链路,哪怕只是显示一行文案,也能让你在后续面对复杂鸿组件时信心提升不少。