我在做跨平台方案选型时,最常被问到的一个问题就是:React Native 这套东西,到底能不能跟鸿蒙 OS 玩到一起去?以前大家普遍觉得 RN 就是 iOS 和 Android 的事,跟鸿蒙生态没什么关系。但项目一深挖,你会发现鸿蒙组件、鸿蒙应用集成、分布式能力调用这些需求早就被摆到桌面上来了。这篇文章就是基于我个人在 React Native 工程里集成鸿蒙组件的实际踩坑经历,写清楚鸿蒙开发你需要补哪些基础,以及 RN 和鸿蒙到底怎么在工程层面真正打通。适合那些手头有 RN 项目、又希望把能力扩展到鸿蒙设备上的开发者,也适合刚接触鸿蒙、想搞清楚它和跨平台框架关系的新手。
1. 项目概述:React Native 和鸿蒙组件之间到底是什么关系
1.1 这个需求到底要解决什么
先说结论:所谓“在 React Native 中开发鸿蒙组件”,本质上有两种完全不同的诉求。第一种是想把现有的 RN 应用直接跑到鸿蒙设备上,那你要解决的是“RN 运行时怎么在鸿蒙系统里跑起来”的问题。第二种是想在你现有的 RN 工程里,调用鸿蒙系统特有的原生能力——比如分布式文件、跨设备流转、原子化服务这类 Android 和 iOS 上没有的东西——那你要解决的是“怎么把鸿蒙原生组件桥接给 RN”的问题。
这两个方向听起来相似,实际工程路径差别很大。我见过的项目里,大部分人的真实需求其实是第二种:团队里已经有一个成熟的 RN 客户端,不想因为鸿蒙单独维护一套原生代码,但又必须让应用能使用鸿蒙的特色能力。这种场景下,你把鸿蒙原生功能封装成一个可以被 RN 侧 JavaScript 直接调用的组件或模块,是最务实的做法。
而第一种需求,也就是让 RN 应用完整运行在鸿蒙上,则会涉及到更多底层适配。当前社区里已经有基于 OpenHarmony 的 React Native 适配方案,工程结构、打包脚本、JS 引擎加载方式都跟 Android 版本不一样。我建议你在动手前先想清楚:你到底是要“鸿蒙支持 RN”,还是要“RN 调用鸿蒙”。想清楚这个,后面所有的技术选型才有判断依据。
1.2 鸿蒙开发基础知识的最小集合
不管走哪个方向,鸿蒙开发的一些底层概念你是绕不开的。不需要你变成鸿蒙专家,但至少这几个名词得门儿清。
ArkTS 是鸿蒙的声明式开发语言。它是 TypeScript 的超集,语法上跟 TS 高度相似,但多了很多 ArkUI 的场景化 API。你在写鸿蒙原生组件的时候,界面部分基本都是 ArkTS + ArkUI 声明式语法。
ArkUI 是鸿蒙的 UI 开发框架,类似 Flutter 的 widget 树或者 SwiftUI 的 view 描述。它用@Component和@Builder这类装饰器来组织组件层级,页面逻辑用@State、@Prop、@Link管理状态。你如果写过 Vue 或 React,上手 ArkUI 会非常快。
接下来是 Ability。鸿蒙的应用不是按 Activity 或 ViewController 来组织的,而是按 Ability 来组织。一个 UIAbility 基本上就是一个带界面的入口,可以理解成 Android 的 Activity。RN 要跑在鸿蒙上,本质上就是要在一个 UIAbility 里挂载一个 RN 视图。
然后是一个非常重要的概念:HAP。鸿蒙的应用安装包格式叫.hap,大部分情况下一个 HAP 就是一个可以独立安装部署的应用单元。RN 项目集成鸿蒙组件之后,最终交付的产物就是一个 HAP 包,跑在鸿蒙设备上。
最后一个你必须理解的概念是分布式能力。鸿蒙 OS 的看家本领不是单设备上的 UI 效率,而是它从系统底层就支持的跨设备能力——设备发现、跨端流转、数据共享。这一块在 Android 和 iOS 上都需要借助云端或第三方 SDK 才能实现,在鸿蒙上是系统级的 API。RN 要调用鸿蒙分布式能力,最直接的方式就是把分布式 API 封装成原生模块,暴露给 JavaScript。
2. 关键技术选型:三条打通的路径,哪条适合你
2.1 路径 A:用 RNOH 框架让 React Native 应用跑在鸿蒙上
RNOH 是社区推动的 React Native on OpenHarmony 适配方案。它的目标是把 RN 的运行时、渲染管线、原生组件映射全部移植到鸿蒙系统上。
这条路径下,RN 的 JS 代码和 React 组件树基本不需要改。你的 RN 项目会通过脚手架生成一个鸿蒙工程目录,里面包含 ArkUI 写的原生宿主页面。RN 的 JS Bundle 会被加载到 Hermes 引擎里执行,UI 渲染通过 RN 的 Fabric 渲染器映射到 ArkUI 的组件上。
我做选型时比较关注的是:RNOH 对 RN 版本的跟进速度。RN 的版本迭代很快,RNOH 往往需要一定时间才能同步适配。如果你项目里的 RN 版本比较新,或者用了很多第三方原生模块,能不能顺利跑通取决于 RNOH 社区的适配进度。
这条路径适合的目标是:不想维护两套业务代码,想让 RN 项目直接覆盖鸿蒙设备。但你要做好心理准备,React Native 里那些依赖 Android/iOS 原生模块的第三方库,在鸿蒙上大概率是不能直接用的,需要你逐步替换或自己写鸿蒙适配。
2.2 路径 B:把鸿蒙原生组件封装成 RN 的 Native Module
这是我最推荐的方案,也是我实际用在生产环境里的方案。原理非常简单:RN 本身就有成熟的桥接机制,JavaScript 可以通过NativeModules调用原生代码。在鸿蒙这边,你只需要用 ArkTS 写一个原生模块,通过 RNOH 的TurboModule注册机制挂到 RN 运行时上,JS 侧就能像调用普通 JS 方法一样调用鸿蒙 API 了。
举个例子。你想让 RN 页面触发一个鸿蒙的跨设备流转,Android 和 iOS 都没有这个 API,只有鸿蒙有。你就在鸿蒙工程里写一个DistributeModule,里面调用鸿蒙的分布式能力 API,然后注册成 RN 的原生模块。RN 侧只需这样:
import { NativeModules } from 'react-native'; const { DistributeModule } = NativeModules; await DistributeModule.startDistributedShare('com.example.target.device');这种封装方式的优点是隔离清晰。你的 RN 业务代码不用关心鸿蒙原生实现是什么样子,只需要定义一个统一的接口。鸿蒙团队和 RN 团队可以并行开发,各自测试,最后通过桥接协议对接。
适合的目标是:现有 RN 工程已经稳定运行,你只想逐步引入鸿蒙原生能力,而且不希望把整个 RN 运行时的适配风险引入到核心业务里。
2.3 路径 C:在鸿蒙原生应用里嵌入 RN 视图
这个方案的方向是反过来的。它不是为了把 RN 应用搬到鸿蒙,而是你本身在开发一个鸿蒙原生应用,但某个页面或模块想用 RN 来写——通常是团队里前端工程师多、原生工程师少,或者某个页面迭代频率极高,用 RN 可以快速发版。
实现上,你需要在鸿蒙工程的 UIAbility 里创建一个容器,把 RN 的ReactRootView挂载进去。RN 的 JS Bundle 可以本地加载,也可以远程下发。这个方案的工程复杂度比路径 B 高一些,因为你要同时维护鸿蒙原生工程和 RN 工程,还要处理两个运行时之间的生命周期同步。
我一般不建议把整个鸿蒙应用都用 RN 来写。更合理的方式是:鸿蒙原生负责框架层和系统能力调用,RN 负责那些频繁变化的业务页面。两边通过事件总线通信。这样既保留了鸿蒙的原生体验,又利用了 RN 的开发和发版效率。
2.4 三条路径怎么选
我把三条路径的适用场景整理成一张表,方便你自己对号入座。
| 路径 | 适用场景 | 工程改造成本 | 风险点 |
|---|---|---|---|
| 路径 A(RNOH) | 新项目启动,希望一套 RN 代码覆盖所有平台 | 中等,需要适配第三方库 | RN 版本升级受限于 RNOH 适配进度 |
| 路径 B(Native Module) | 已有 RN 工程,需要调用鸿蒙独有能力 | 较低,桥接层清晰 | 需要熟悉鸿蒙 API 封装 |
| 路径 C(嵌入 RN 视图) | 鸿蒙原生应用中的部分页面用 RN 开发 | 较高,双工程维护 | 生命周期和内存管理复杂 |
我自己的项目选的是路径 B。原因是业务侧 RN 已经沉淀了大量代码,不可能因为鸿蒙重写。而鸿蒙那侧的分布式能力只有原生能做到,所以最好的方式是两边留一个干净的接口。
3. 环境准备与工程搭建实操
3.1 工具链准备:这一步卡住了很多人
先说我踩过的第一个坑:版本不匹配。鸿蒙开发工具链和 RN 工具链各自独立迭代,你在安装的时候一定要看清楚对应关系。我这里给一个我实际验证过的组合参考:
- Node.js:建议 18 LTS 以上,RN 的脚手架和 Metro 打包都依赖它
- JDK:建议 17 版本。DevEco Studio 自带的 JDK 可能跟 RN 的 Gradle 配置冲突,最好在系统环境变量里统一指定
- DevEco Studio:建议使用官网最新的稳定版,鸿蒙 SDK 也一并下载
- React Native CLI:如果你的 RN 工程是旧版本,升级前先看一下 RNOH 插件支持的 RN 版本区间
工具链安装完以后,还要确认鸿蒙 SDK 的command-line-tools能不能在终端里正常执行。很多集成脚本会调用这些工具做签名和打包,如果环境变量没配好,后面构建的时候会报一些莫名其妙的错误。
3.2 创建 React Native 工程和鸿蒙工程
这个阶段我建议你先初始化一个全新的 RN 工程做验证,别一上来就拿生产项目试。流程是:先用 RN 官方脚手架创建出基础工程,然后用 RNOH 提供的脚手架插件在同一个工程目录下生成鸿蒙侧工程。
以我当时的操作步骤为例:
npx @react-native-community/cli@latest init HarmonyRNProject cd HarmonyRNProject npx rnoh initrnoh init会生成一个harmony目录,里面是完整的鸿蒙工程结构,包括entry模块、module.json5、ArkTS 源码目录等。生成完成后,用 DevEco Studio 打开这个harmony目录,IDE 会自动同步鸿蒙工程配置。
这里有个容易踩的坑:rnoh init生成的鸿蒙工程默认依赖的 SDK 版本可能跟本机不一致。打开工程后先看build-profile.json5里的compileSdkVersion和targetSdkVersion,改成你本机 DevEco Studio 实际安装的版本。不改的话,构建直接报错。
3.3 配置鸿蒙工程,把 RN 运行时接入系统入口
RNOH 生成的鸿蒙工程里,入口是一个 UIAbility,它负责创建并加载 RN 的视图。你要检查的核心文件是entry/src/main/ets/entryability/EntryAbility.kt——不对,鸿蒙用的是 ArkTS,所以是.ets。这个文件里通常会创建一个ReactHost实例,然后通过它加载 RN 的 Bundle。
你需要在module.json5里确认权限配置。RN 应用跑起来以后可能访问网络、读写本地文件,这些权限都需要在鸿蒙的配置文件里申请。跟 Android 的AndroidManifest类似,鸿蒙的权限在module.json5的requestPermissions字段里声明。我踩过的坑是:只加了网络权限,结果 Metro 开发服务器能连上,但真机上应用内请求资源一直失败,后来才发现是权限没写全。
3.4 编写第一个鸿蒙原生组件:以桥接分布式能力为例
现在到重点了。假设我们要写一个鸿蒙原生模块,暴露两个方法:一个是获取当前设备的设备 ID,另一个是触发跨设备流转。
先在鸿蒙工程的ets目录下创建一个原生模块类:
import { TurboModule } from 'react-native-oh-turbo'; export default class DeviceInfoModule extends TurboModule { getDeviceId(): string { // 调用鸿蒙系统 API 获取设备唯一标识 return deviceInfo.getDeviceId(); } startDistributedShare(targetDeviceId: string): Promise<void> { return new Promise<void>((resolve, reject) => { // 在这里调用鸿蒙分布式流转 API // 成功后 resolve(),失败则 reject() }); } }然后在模块注册文件里把这个类注册成 RN 可识别的原生模块:
import { TurboModuleRegistry } from 'react-native-oh-turbo'; TurboModuleRegistry.registerModule('DeviceInfoModule', DeviceInfoModule);RN 侧调用就非常简单了:
import { NativeModules } from 'react-native'; const DeviceInfo = NativeModules.DeviceInfoModule; const deviceId = DeviceInfo.getDeviceId(); await DeviceInfo.startDistributedShare(selectedDeviceId);这里面有个非常重要的细节:鸿蒙的分布式能力 API 大部分是异步回调或 Promise 风格的,你在封装 Native Module 的时候一定要把异步逻辑处理好。RN 侧拿到的应该是一个返回 Promise 的接口方法,而不是一个塞回调函数进去的 C 风格 API。这样封装出来的模块对 RN 业务层面最友好。
3.5 编译 HAP 包并在模拟器或真机上调试
模块写完之后,就可以在 DevEco Studio 里执行构建了。构建产物是一个.hap包,可以直接安装到鸿蒙模拟器或真机。
调试阶段建议先用模拟器跑通 JS 到原生模块的基础调用链路,再上真机测试分布式相关功能。因为分布式能力在模拟器上往往会有很多限制,比如没法真实模拟多设备发现。你需要在 DevEco Studio 的远程模拟器里手动添加多个虚拟设备来测试流转流程,但效果还是不如两台真机来得直接。
真机调试调试分布式能力,注意两台设备要登录同一个账号,并且在设置里开启多设备协同。否则设备发现阶段就会卡住,而不是你的代码有问题。
4. 核心实现细节:桥接原理与组件映射
4.1 RN 与鸿蒙的桥接机制到底是怎么工作的
很多朋友问过我:RN 和鸿蒙之间的通信,跟 RN 和 Android/iOS 之间有什么本质区别?其实核心机制是一脉相承的,RN 把 JavaScript 侧的调用序列化以后,通过桥接层分发到原生模块,原生模块执行完再把结果回传。
鸿蒙这边不同的一点在于,RNOH 走的路线更接近新一代的 TurboModule 架构,而不是老的 NativeModule 架构。也就是说,它是通过 JSI(JavaScript Interface)直接建立 JS 和原生侧的调用关系,省去了老架构里 JSON 序列化和异步消息队列的损耗。
这意味着你在封装鸿蒙模块的时候,要注意方法签名必须符合 JSI 的约束。比如只支持可序列化的参数类型,复杂对象要转成字典或字符串,不能像早期 RN 一样传一个跨语言的实例引用。这条我从实践里得来的经验能帮你少走不少弯路。
4.2 原生组件是如何映射到自动化 UI 渲染的
RN 的 UI 组件在鸿蒙上不能直接渲染,需要经过一层映射。RN 的View在渲染时,UI Manager 会向原生侧请求对应的原生视图组件。在 Android 上是ReactViewGroup,在 iOS 上是RCTView,而在鸿蒙上,RNOH 提供了一套映射关系,把 RN 的View、Text、ScrollView这些基础组件映射到 ArkUI 的对应组件上。
这不是简单的一对一翻译。ArkUI 的组件模型和 RN 的组件模型在布局、事件、样式属性方面都各有特点。RNOH 要做的事情是把 RN 的 Yoga 布局结果转换成 ArkUI 的布局参数,把 RN 的合成事件转换成 ArkUI 的 gesture 事件。
所以如果你要写一个自定义的鸿蒙原生组件(比如把某个 ArkUI 特有的复杂组件暴露给 RN 使用),你得同时处理两层:一层是 ArkUI 组件的 UI 展示逻辑,另一层是它跟 RN 的事件和布局对接逻辑。
4.3 鸿蒙独有能力的暴露思路:以分布式数据为例
鸿蒙最强的就是分布式能力,这也是它有别于 Android 和 iOS 的最大差异点。你要在 RN 里用上分布式数据管理,思路是把它封装成 RN 的原生模块。
鸿蒙的分布式数据服务允许不同设备上的应用读写同一个数据副本,数据会通过系统底层同步。你在封装的时候,可以考虑暴露这几个接口:初始化一个分布式数据库、写入键值对、监听远端数据变化。RN 侧拿到的是一个统一的数据访问层,但背后实际使用的是鸿蒙特有的分布式能力。
这里有一个产品层面的建议:不要试图把鸿蒙所有的分布式能力都一口气暴露给 RN,那样会让桥接层变得非常臃肿。先挑业务上最需要的两三个能力做模块化封装,跑通整个链路,再逐步扩展。我见过一个团队一次性封装了十几个分布式接口,结果半年过去了,实际被业务调用的只有两个。
5. 常见问题与排查经验
5.1 构建失败:绝大多数是版本和配置问题
我先说一个最典型的报错:Gradle 或构建系统提示 SDK 版本找不到。这一步在鸿蒙 SDK 更新之后尤其常见。RNOH 生成的工程里默认的compileSdkVersion可能指向某个 IDE 自带的 SDK 版本,而你本机装的是另一个。
排查的顺序我建议这样走:先看 DevEco Studio 里 SDK Manager 里的安装版本,再去看build-profile.json5和local.properties里的 SDK 路径配置,最后确认环境变量里的命令行工具路径没有被某个旧的缓存覆盖。
另一个我去踩过的高频问题是:JDK 版本冲突。RN 工程本身依赖的 Gradle 和鸿蒙工程的 Gradle 对 JDK 版本要求不同,如果你的系统默认 JDK 是最新版,很可能在某个环节报“Unsupported class file major version”。解决办法是给鸿蒙工程单独指定一个 JDK 版本,不改全局环境。
5.2 桥接调用失败:JS 侧拿不到模块
TypeError: null is not an object (evaluating 'NativeModules.DeviceInfoModule')这个报错出现的时候,首先别急着去查 JS 代码。大概率是原生模块没有正确注册。排查路径是:先确认原生模块类有没有在注册文件里被导入,然后确认模块注册的时机是不是在 RN 运行时启动之前。
我处理过一个极端情况:模块注册代码写在了鸿蒙的某个页面生命周期方法里,RN 视图加载需要依赖这个模块,结果模块还没有注册,JS 侧就执行了调用。解决办法是把注册逻辑提到 Ability 的onCreate阶段,保证它先于 JS Bundle 执行。
5.3 真机调试的签名和开发证书问题
很多人在模拟器上跑得好好的,一上真机就闪退或装不上。这时候先别怀疑代码,大概率是签名和证书的配置问题。鸿蒙应用上真机运行需要配置签名证书,DevEco Studio 支持自动签名,但如果你用了自定义证书,记得检查 profile 文件的描述符是否跟应用包名匹配。
真机调试分布式功能时还有一个容易被忽略的点:两台设备必须处于同一个局域网,且都开启了多设备协同。我见过有人排查了很久代码,最后发现是一台设备没有开启协同开关。
5.4 性能优化:渲染卡顿与 JS 线程瓶颈
最后说性能问题。RN 在鸿蒙上的性能瓶颈,跟它在其他平台上的性能瓶颈很像:JS 线程和 UI 线程之间的通信频率过高会导致卡顿。
如果你发现某个页面滚动掉帧严重,优先检查是不是在滚动手势里频繁调用了原生模块。我的一般优化思路是:用节流或批处理把这些高频调用合并成一次原生通信,而不是让每个事件都走一次桥接。另外,尽量减少跨线程的 JSON 序列化,尤其是大数据量传递的时候。
6. 一点实操体会
这篇文章写到这里,基本上把 RN 和鸿蒙组件集成这件事从前到后都过了一遍。我在实操中觉得最有价值的一个经验是:别一开始就追求一套方案管到底。HF鸿蒙生态和 RN 生态都在快速演进,你今天搞定的适配方案,可能半年后就被官方的新能力或者新框架取代。比较稳妥的做法是,把两种生态之间的边界当成一个可以随时替换的适配层来设计,只要两边接口稳定,底层方案换代不会影响你的业务代码。
最后想单独建议的是:如果你手头的团队里既有 RN 开发者又有鸿蒙开发者,一定要让两边尽早开始结对开发,不要各写各的然后留到最后联调。RN 和鸿蒙这对组合的知识跨度非常大,越早让对方理解各自的边界,后面集成阶段就越顺利。我在这个项目里最深刻的体会就是,技术本身并不难,难的是让两个不同技术背景的人在同一套接口上达成共识。把这个共识定好,你的跨平台鸿蒙之路就已经成功了七八成。