做React Native(RN)的开发者,这两年大概率会越来越多地听到一个话题:如何在RN项目里去开发鸿蒙(HarmonyOS)组件。鸿蒙是一款面向全场景的分布式操作系统,现在已经铺到手机、平板、车机、智能家居多个终端。对做跨端业务的人来说,"RN能不能跑到鸿蒙上""鸿蒙原生组件怎么跟RN桥接"这类问题早就不是未雨绸缪,而是很多项目里实打实要交付的需求。
这篇文章的定位很直接:不堆概念,把鸿蒙开发的基础知识先捋一遍,再重点讲清楚在React Native项目里集成鸿蒙应用的路线,包括原生模块封装、桥接调用、构建调试的完整过程,以及我在实操中踩过的坑。适合正在做跨端适配、或者准备把RN业务延伸到鸿蒙平台的开发者,也适合刚接触鸿蒙、想搞明白"RN和鸿蒙到底怎么结合"的前端同学。
1. 为什么会在React Native项目里碰到鸿蒙组件
1.1 React Native的跨端逻辑与鸿蒙适配的由来
React Native的核心思路,是让JavaScript代码跑在一个原生宿主的JS引擎里,通过桥接机制把渲染指令和原生UI组件关联起来。在传统的Android、iOS项目里,RN已经是一个非常成熟的跨端方案:一套JS业务,两套原生宿主。
鸿蒙出现之后,情况变成"需要第三套宿主"。鸿蒙自身有原生的UI框架和语言生态,RN的JS运行逻辑并没有天然支持鸿蒙。于是社区和官方生态就出现了针对鸿蒙的适配层,本质上跟"RN适配Android"做的事情一样:把JS引擎跑起来、把UI映射到ArkUI组件、把原生能力通过模块方式暴露给JS。这也是为什么你会听到"鸿蒙版RN运行时""鸿蒙适配包"这类名词。
搞清楚这一点很重要,因为很多人在项目里遇到"鸿蒙组件"时,会误以为要重写整条业务链。实际上RN的JS层业务代码大体可以不动,需要动的是宿主接入层,也就是鸿蒙原生侧的那一坨桥接代码。
1.2 什么场景下需要开发鸿蒙组件
我把过去接触到的实际需求归纳成三类,你在项目里遇到的情况大概率逃不出这几类:
第一类,已有的RN应用要覆盖鸿蒙设备。这是最常见的诉求。App已经用RN做了大量业务页面,现在要让它在鸿蒙系统上跑起来。这时候你需要做的是"鸿蒙宿主工程 + RN容器"的组合,让已有的JS页面直接运行在鸿蒙设备上。这类工作里,你可能需要封装一些鸿蒙特有的原生能力,比如分布式文件、跨设备流转,用JavaScript去调用。
第二类,鸿蒙原生应用里要嵌入RN页面。很多团队是"先做鸿蒙原生壳,再逐步把业务页面RN化",或者反过来"在鸿蒙原生应用里复用一套RN页面"。这时候RN的作用是作为动态化方案,页面升级不用走应用发版。
第三类,RN项目需要调用鸿蒙独有的系统能力。比如分布式数据同步、多设备协同认证。RN自带的API没有这些,只能通过原生模块暴露给JS。这时候开发一个"鸿蒙组件"的定义就是:用ArkTS实现的NativeModule,包一层JS接口,让RN侧能正常调用。
1.3 先搞清楚"鸿蒙组件"到底指什么
很多聊天记录里说的"鸿组件(鸿蒙组件)",其实有两种截然不同的含义,一定得先分清楚:
- 一种是"ArkUI组件",也就是用ArkTS写在鸿蒙原生页面里的UI组件,比如一个自定义按钮、一个卡片视图,供鸿蒙原生应用内部复用。
- 另一种是"RN集成场景下的原生模块",承载的是能力封装,比如蓝牙连接、扫码、分布式传输,这种情况是你要开发给JS调用的。
在React Native项目集成的语境下,我们主要讨论的是后者。因为RN本身就提供了自己的UI方案,你不会用ArkUI去写RN页面内部的UI,但你可以让RN去调用ArkTS写的能力模块。如果一上来就把ArkUI组件当成"要在RN里使用的组件",思路容易跑偏。
2. 鸿蒙开发基础:先弄清楚鸿蒙应用长什么样
2.1 应用模型与工程结构
在动RN集成之前,得先看懂鸿蒙原生工程的骨架,否则代码都不知道往哪里放。
鸿蒙现在主推的是Stage模型。在这个模型下,一个应用由多个Ability组成,其中UIAbility负责提供界面,ExtensionAbility负责后台任务或扩展能力。应用入口配置在module.json5文件里,里面声明了包名、Ability列表、权限、入口页面等。工程里的主要目录包括ets(放ArkTS源码)、resources(存放字符串、颜色、图片等资源)、以及构建配置文件build-profile.json5。
这些概念对应到RN集成时很重要:你的RN容器页面本质上要挂载到一个UIAbility上,而这个UIAbility就是RN的宿主页面。如果你对Ability生命周期不了解,后续做页面跳转、退到后台再恢复时,RN页面会容易出现白屏或状态丢失。
2.2 ArkTS语法与ArkUI声明式开发
鸿蒙原生开发的语言是ArkTS,它基于TypeScript扩展而来,加了声明式UI的能力。如果你会写TS和前端,上手门槛不高。一个简单的ArkTS页面大致长这样:
@Entry @Component struct Index { @State message: string = 'Hello HarmonyOS' build() { Column() { Text(this.message) .fontSize(30) .fontWeight(FontWeight.Bold) Button('点击') .onClick(() => { this.message = 'Clicked' }) } .width('100%') .height('100%') } }这里有几个关键概念:@Entry标记页面入口,@Component定义组件,@State声明响应式状态,build描述UI结构。ArkUI框架会自动追踪状态变化并刷新UI,这跟React的useState本质上是同一种思想。理解这一点后,你在做RN桥接时会非常有帮助,因为ArkTS模块的状态回传、UI刷新逻辑跟RN的JS调用栈经常混在一起,两边都掌握才能调顺。
2.3 权限、模块与发布机制
鸿蒙在权限管理上比较严格,所有涉及敏感能力的API都要在module.json5里声明权限。比如访问位置、相机、读取文件等。RN侧开发鸿蒙原生模块时,如果模块内部用了受控API,别忘记在鸿蒙工程的module.json5加上对应权限,否则调用时会直接抛异常。这个坑我后面还会再提。
鸿蒙生态里也有类似"库"的机制:HAR(Harmony Archive)和HSP(Harmony Shared Package)。HAR是静态共享包,编译时打包进应用;HSP是动态共享包,可以按需加载、单独更新。当你把一个鸿蒙原生模块做成可复用的独立组件时,最自然的做法就是封装成HAR包,再被RN宿主工程引用。
3. 在React Native中集成鸿蒙应用的技术路线
3.1 RN跨平台内核与原生宿主的关系
要理解集成鸿蒙的方案,先要回到RN的架构底层。RN应用启动后,JS代码在一个JavaScript引擎里执行,React渲染出的虚拟DOM不会直接变成原生控件,而是通过一个桥接层把操作发送给原生端;原生端根据操作创建、更新对应的真实视图,或者调用原生模块的能力。
这个桥接层是集成鸿蒙的核心阵地。RN的历史版本使用经典NativeModule桥,新版本则逐步过渡到TurboModule/Fabric架构。无论版本如何变化,核心要做的事情是稳定的:
- 让RN的JS引擎能在鸿蒙进程里跑起来
- 让RN的渲染指令能映射到ArkUI组件
- 让RN的JS代码能调用ArkTS原生模块
3.2 集成鸿蒙的常见架构方案
从工程架构看,目前RN集成鸿蒙主要有以下三种形态,适用于不同业务诉求:
| 架构方案 | 主要思路 | 适用场景 | 注意点 |
|---|---|---|---|
| RN作为鸿蒙应用内的页面容器 | 鸿蒙壳工程里创建一个UIAbility承载RN容器 | 已有鸿蒙原生应用,内部嵌入RN页面 | 需要处理好壳与RN的交互 |
| 鸿蒙原生模块作为RN的NativeModule | 用ArkTS编写原生能力,通过桥接暴露给JS | RN业务里调用鸿蒙系统能力 | 模块封装与线程回调是重点 |
| RN作为独立鸿蒙应用运行 | 直接用RN适配层生成鸿蒙应用 | 纯RN团队快速上线鸿蒙版本 | 鸿蒙增量能力需要自研模块 |
这三种不是互斥的,实际项目往往是"鸿蒙壳 + RN容器 + 自研鸿蒙模块"一起用。我重点推荐第二种方案作为团队切入点的原因很简单:它改动范围最小,不需要先把整条RN链路跑通才能验证价值,先把一两个鸿蒙能力封装给JS调用,风险低、见效快。
3.3 建立原生桥接的关键步骤
无论采用哪种架构,桥接鸿蒙原生模块的通用流程都包括这几步:
- 在鸿蒙侧定义一个类,实现RN框架要求暴露的模块接口或继承对应基类,用装饰器或注册表标注模块名。
- 在这个类里实现业务方法,方法内可以调用ArkTS API,结果以Promise或回调方式返回给JS。
- 在鸿蒙宿主工程的模块注册入口处,把定义的模块注册进RN框架。
- 在RN侧通过NativeModules(或TurboModule的方式)拿到模块实例,然后调用方法。
这四步跟你在Android上写一个原生Toast模块的模式几乎一样。区别在于鸿蒙侧的接口规范、线程调度方式、以及数据类型的序列化方式。大家最容易踩坑的地方是"回调线程":鸿蒙很多系统API的回调线程不是RN的JS线程,如果回调里直接操作UI或JS侧变量,轻则闪退,重则死锁。
4. 实操:写一个鸿蒙组件并在RN中调用
4.1 搭建工程骨架
先准备RN工程。假设你已经有一个RN项目,并且安装了React Native CLI。在项目里增加鸿蒙适配层通常有两种方式:一种是使用社区提供的鸿蒙适配脚手架直接生成鸿蒙工程,另一种是在项目目录下手动创建鸿蒙的HarmonyOS工程目录并关联RN依赖。
我的建议是先用官方或社区推荐的脚手架跑通最小路径,避免手写一堆工程配置。工程生成后,你会看到类似下面的目录结构:
projectRoot/ ├── entry/ │ ├── src/main/ │ │ ├── ets/ │ │ │ ├── entryability/ │ │ │ ├── pages/ │ │ │ └── rnmodules/ │ │ ├── resources/ │ │ └── module.json5 │ └── build-profile.json5 ├── node_modules/ └── package.json其中ets/rnmodules目录就是我们要放鸿蒙模块的地方。
4.2 用ArkTS实现一个原生模块
下面我以一个最简单的"获取鸿蒙设备信息"功能为例,演示模块的开发。在鸿蒙侧,新建文件DeviceInfoModule.ets,实现一个可被RN调用的原生模块:
import { TurboModule } from '@rnoh/react-native-openharmony/src/main/ts/RNOHCorePackages/TurboModule' export class DeviceInfoModule extends TurboModule { getDeviceModel(): string { return this.ctx.uiAbilityContext.resourceManager.getDeviceName() } }这只是示意,实际项目中你需要根据RN适配框架的版本,决定基类到底叫什么、是否需要使用特定的装饰器注册。不同版本API可能有差异,但核心是:类里暴露的方法,最终可以被JS同步或异步调用。
再举一个带异步回调的例子,比如获取鸿蒙系统版本号:
export class SystemInfoModule extends TurboModule { getSystemVersion(): Promise<string> { return new Promise<string>((resolve) => { // 假设通过某系统API读取版本号 resolve('HarmonyOS 5.0') }) } }拿到模块类之后,需要在模块注册表里注册,让RN框架能根据模块名找到它。注册逻辑一般在鸿蒙侧RNHost的Package列表里追加新的Package类,类似于Android的ReactPackage。
4.3 在RN端封装调用接口
鸿蒙侧模块注册好后,JS侧调用逻辑非常简单,但我在实际项目中习惯再包一层JS接口,方便统一管理、加错误处理、做类型定义。比如:
import { NativeModules } from 'react-native'; const { DeviceInfoModule, SystemInfoModule } = NativeModules; export async function getDeviceInfo() { try { const model = await DeviceInfoModule.getDeviceModel(); const version = await SystemInfoModule.getSystemVersion(); return { model, version }; } catch (error) { console.error('[harmony] 获取设备信息失败', error); return null; } }封装层的意义不只是好看,更重要的是把"鸿蒙原生模块可能返回异常格式""模块在非鸿蒙端不存在"等问题集中拦截掉。比如你在iOS、Android上运行同一套RN代码,鸿蒙模块是undefined,直接调用会崩溃,但有了封装层,至少能优雅降级。
4.4 运行调试与构建验证
模块写完,接下来就是构建验证。鸿蒙侧的调试跟Android不完全一样,通常流程是:
- 用鸿蒙官方IDE打开生成的鸿蒙工程。
- 连接鸿蒙真机或模拟器。
- 先编译鸿蒙工程,确认ArkTS代码没有语法或依赖问题。
- 启动应用,用RN的日志工具确认JS侧能打印正确返回值。
- 如果JS调用没有任何输出,优先检查模块是否注册成功。
实操中我经常遇到"编译成功但运行时模块找不到"的情况,这时候不是代码语法问题,而是注册表没扫到模块,跟Android上忘记在ReactPackage里加模块的表现一模一样。
5. 常见坑与排查经验
5.1 构建配置与依赖版本
RN集成鸿蒙的第一大坑是版本匹配。React Native的版本、鸿蒙适配框架的版本、鸿蒙SDK的版本,三者必须对齐。我曾经遇到一个项目,RN侧用了一个较新的动态特性,但鸿蒙适配层版本较老,导致组件渲染异常。这种问题的排查效率其实很低,报错信息往往指不到真正的原因。
我的建议是:在项目初期就固定好三端的版本组合,并在工程里用文档记录,升级任何一端的版本都要重新做全链路回归。社区主推的"某个RN版本对应某个鸿蒙适配版本"这种组合表一定要认真对待,不要轻易跨版本随便升。
5.2 线程模型与异步回调
鸿蒙和RN都有各自的线程模型。RN的JS线程负责执行JS逻辑,原生模块方法的调用可能发生在JS线程、UI线程或专门的IO线程。当你拿到鸿蒙系统API的回调时,先搞清楚它到底在哪个线程上。
举个例子:鸿蒙的分布式数据回调可能在子线程返回,如果这个回调里直接去改RN的UI状态,RN侧通常不允许也不安全。正确做法是把数据先序列化成普通JSON,再用RN提供的事件或状态管理机制抛回JS端。说白了就是:原生侧只负责把数据取回来,不要试图"帮JS做UI刷新"。
5.3 页面生命周期同步
RN页面在鸿蒙宿主里跑,会面临两个生命周期体系:RN自己的生命周期和鸿蒙UIAbility的生命周期。如果两者没配合好,会出现很隐蔽的Bug,比如App退到后台再回来,RN页面白屏或事件丢失。
我的排查经验是:在鸿蒙宿主UIAbility的onBackground/onForeground回调里,要主动通知RN容器暂停或恢复JS运行,不要让RN自己猜宿主是否可见。这就跟在Android里onPause/onResume要通知RN一样,只是鸿蒙的回调名称不同,机制是相通的。
5.4 图片、字体与资源路径
RN项目普遍依赖图片资源和自定义字体。在鸿蒙上,资源管理方式跟Android不完全一致。常见问题有两个:一是图片放在assets目录下,但鸿蒙工程读取不到;二是字体文件因为资源路径差异导致样式失效。
解决方案很朴素:把公共资源按照鸿蒙工程要求的路径放一份,或者在RN侧将资源改为网络加载。如果你的RN代码里有大量本地图片,在新平台集成初期,资源适配的工时一定要规划进去。
5.5 内存泄漏与模块释放
RN + 鸿蒙双端运行,内存问题会格外明显。特别是鸿蒙原生模块里如果在子线程持有了RN上下文或UIAbility引用,很容易造成Activity无法释放。我见过一个定位模块,因为回调里持有了宿主Context,导致每次进入页面内存都涨一截。
经验是:所有从RN上下文传入原生模块的引用,都要在使用完后显式置空;能用临时上下文就不要持有全局上下文;事件监听在页面销毁时务必反注册。开发阶段可以配合内存调试工具,盯着"进入页面-退出页面"的内存曲线,如果曲线每次比上一次都高,基本就是泄漏。
5.6 常见问题速查表
| 问题现象 | 可能原因 | 排查建议 |
|---|---|---|
| JS调用模块返回undefined | 原生模块未注册或模块名不一致 | 检查注册表与NativeModules名称 |
| 编译通过但运行崩溃 | 原生模块方法回调线程不正确 | 打印线程ID,确认回调位置 |
| 页面白屏或状态丢失 | UIAbility生命周期未同步到RN | 显式触发RN的暂停/恢复 |
| 资源图片不显示 | 资源路径或格式不兼容 | 改用网络地址或按鸿蒙路径放置 |
| 内存持续上涨 | 上下文被原生模块持有 | 检查回调引用并及时置空 |
写在最后的实操体会
我在实际项目里反复体会最深的一点是:RN集成鸿蒙这件事,最大的学习成本不在React Native,而在鸿蒙这一侧。你要是没有耐心把Stage模型、Ability、ArkTS这些基础啃一遍,哪怕社区脚手架再成熟,遇到问题照样一头雾水。
另一个很有用的经验是:不要一上来就追求"整条RN业务链跑通"。先挑一两个纯鸿蒙能力做原生模块封装,走通"RN JS → 鸿蒙原生模块 → JS"这条最小闭环,再逐步扩大覆盖面。路线越陡,越要先把地基压实。
最后分享一个小技巧:在鸿蒙侧写原生模块时,尽量把返回给JS的数据全部做成纯JSON结构,不要直接传对象引用。RN桥接层的序列化能力跟宿主紧密相关,纯JSON是最稳的传输格式。这能帮你避开一大批锁死、白屏、数据丢失的诡异问题。