1. 项目背景与思路拆解
先聊聊我为什么会对这个方向感兴趣。身边不少做移动端的人,手里已经有一套用 React Native 写好的跨端业务,现在突然要做鸿蒙版本的适配,摆在面前的无非两条路:要么重新用原生的 ArkTS 把整块业务再写一遍,要么想办法把鸿蒙的能力塞进现有的 React Native 工程里。前者成本太高,团队要养两套技术栈,后者听起来取巧,但真的落地时会发现,坑比预想的多。
我最初接触鸿蒙组件时,第一反应也是到处翻文档,看看有没有官方现成的桥接方案。结果发现资料非常分散,论坛上大多是零碎的经验帖,没有一个能直接照着做的完整流程。后来我干脆自己把整个链路跑了一遍,才理清楚 RN 与鸿蒙之间的那层关系。这篇博文不打算讲那些教科书式的概念,就基于我实际操作过的项目来拆解,怎么从零开始搭出可用的桥接,哪些环节最容易出问题,以及我对这套方案的取舍判断。
先说清楚它到底解决了什么问题。你的 React Native 项目里如果需要调用鸿蒙特有的能力,比如分布式流转、原子化服务,或者其他一些只存在于鸿蒙系统里的 API,直接用 JS 调原生肯定不行,这时候就需要一个中间层把两边的调用方式打通。集成完成后,你的 RN 页面可以正常加载,同时又能触发鸿蒙组件去完成那些原生任务,业务逻辑依然留在 JS 侧,不用重写。
适合谁看?我建议三类人可以重点读一下:一类是已经在做 React Native 开发,忽然被排期要求支持鸿蒙的应用的;另一类是鸿蒙原生开发想了解怎么跟 RN 工程共存的;还有一类纯粹是技术选型纠结中的人,看完这篇你应该能判断出该不该走组件集成这条路。
有一点我需要先说明白,这里的“鸿”指的就是鸿蒙 HarmonyOS,以下统称鸿蒙。实际开发中涉及的权限、API 版本、构建工具,都会随着系统更新而变化,但底层的桥接思路不会大变,你看到的是我在当前环境下的实测结果。
1.1 核心痛点:为什么非要桥接,而不是重写
先做个简单的对比。假设你有一个中等规模的电商应用,RN 里大概有 30 个页面,涉及购物车、支付、消息推送这些模块。如果纯用鸿蒙 ArkTS 重写,一个熟练的原生开发至少要两个月,而且后续每次 RN 侧更新需求,都得同步改一遍鸿蒙代码,工作量直接翻倍。
但走桥接方案,你只需要把鸿蒙特有的那部分逻辑抽成组件,其余页面仍然留在 RN 里。这意味着团队里 RN 的开发节奏不会被打破,只有涉及鸿蒙能力的页面才需要额外维护一份原生实现。当然,代价是桥接层本身有学习成本,而且你得时刻关注 RN 与鸿蒙两边的版本兼容问题。
我一开始的想法是,如果鸿蒙有官方的 RN 支持库,那就直接用官方方案,省得自己造轮子。后来我发现,官方确实提供了一个用于开发调试的鸿蒙环境适配库,但它的定位更偏向验证基本功能,真正要接入复杂的业务组件,你还得自己写不少东西。所以我最终的选择是:官方库负责基础桥接,自研桥接模块负责业务组件。
注意:这不意味着你可以跳过鸿蒙原生基础直接开工,桥接层里你照样要写 Ability、写 ArkTS 组件,只是最终这些能力是从 RN 侧触发的。
1.2 方案选型:桥接 vs 嵌入式原生页面
选型的过程里,我认真比过两种路径。第一种是“嵌入式原生页面”,也就是在 RN 里开一个 WebView 或者原生容器去加载鸿蒙页面,优点是实现快,不需要深度桥接,但缺点也明显:页面之间切换不流畅,数据共享麻烦,而且鸿蒙的原生能力依然要额外开放接口才能被 JS 调用。
第二种是“桥接组件”,也就是把鸿蒙组件封装成 RN 能识别的原生模块,JS 侧可以像调用普通组件一样去渲染、传参、接收事件。我倾向这种,因为它真正做到了一套逻辑多处复用,而且后续鸿蒙系统升级时,你只要更新桥接层即可,业务页面无感知。
当然,第二种开发周期更长,要处理生命周期同步、线程模型、事件回调这些细节。但长远看,维护成本更低,这也是我最终采用的方案。
2. 鸿蒙开发基础速览
说实话,如果你之前没接触过鸿蒙原生,直接跳到桥接层会非常痛苦。我在刚开始的时候就是因为不了解 Ability 的概念,导致构建出来的组件始终无法正确挂载到 RN 页面上。所以这里花一点篇幅,把桥接工程里最常用到的几个鸿蒙知识点梳理出来,用比较通俗的方式讲。
2.1 必须理解的 ArkTS 与 ArkUI
ArkTS 是鸿蒙应用开发的主要语言,基于 TypeScript 做了扩展,语法上跟 TS 很像,但加入了状态管理、组件化的一些约束。虽然它长得像 JS,但运行机制和 RN 的 JS 引擎是两套东西,不能直接混用。
ArkUI 则是声明式 UI 框架,类似 Flutter 的 Widget 或 RN 的组件,写法上也是函数式组件加状态驱动。对 RN 开发者来说,这层上手其实不难,难的是你要区分哪些能力是 ArkUI 专属的,哪些能力需要调用鸿蒙 API 才能实现。
我在桥接开发里大量用到的 ArkTS 特性包括:Entry装饰器声明入口 Ability、State和Prop管理组件状态、build()方法定义界面结构。这些理解到位了,你写桥接组件时才有把握。
// 一个最简单的 ArkTS 组件示例 @Entry @Component struct Index { @State message: string = '从鸿蒙侧返回的消息' build() { Row() { Text(this.message) .fontSize(20) .fontWeight(FontWeight.Bold) } } }你可以看到,它的组件结构其实跟 React 很相似,只是状态修饰符换了名字。这层相似性能帮你少学很多东西。
2.2 Ability 是什么,和 Activity 有什么区别
鸿蒙应用的基本单位是 Ability,你可以把它理解成 Android 里的 Activity 或者 iOS 里的 ViewController。一个鸿蒙应用可以有一个或多个 Ability,每个能力负责一个独立的功能页面。
在 RN 集成场景里,我们通常不需要创建太多 Ability,只需要一个入口 Ability,它负责承载 RN 的运行环境。桥接组件本质上是挂在这个 Ability 下,通过一套通信机制与 JS 侧交互。
我在第一次写桥接时犯过一个错,试图在一个项目里创建多个 Ability 来分别承载不同页面,结果生命周期管理变得非常混乱。后来才想明白,RN 本身就是一个大的原生视图,你只要把它嵌入到一个主 Ability 里就够了。
3. React Native 集成鸿蒙组件的完整流程
这部分是核心,我会从环境准备讲到最终运行,每个步骤都尽量详细,同时把参数怎么选、为什么这么选说清楚。
3.1 环境准备:你需要装哪些东西
先列一份我在实测环境里使用的工具版本,你可以当作参考,但不是硬性标准。
| 工具 | 版本 | 说明 |
|---|---|---|
| Node.js | 18.17.0 或更高 | RN 工具链依赖 Node 环境 |
| React Native | 0.72 及以上 | 0.72 的架构对鸿蒙适配更友好 |
| DevEco Studio | 3.1 Release | 鸿蒙官方 IDE,用于构建原生部分 |
| HarmonyOS SDK | API 9 或以上 | 我测试时用的是 API 10,稳定性和兼容性都较好 |
| 鸿蒙官方 RN 适配库 | 0.72.5 版本 | 提供最基础的桥接能力 |
如果你已经用 DevEco Studio 创建过鸿蒙工程,那环境其实已经具备了大半。需要注意的是,RN 项目和鸿蒙工程要放在同一个根目录下,方便后续联动,我的建议结构如下:
my-rn-project/ ├── android/ # 原有 Android 工程(可留) ├── ios/ # 原有 iOS 工程(可留) ├── harmony/ # 鸿蒙工程 │ ├── entry/ │ │ ├── src/main/ │ │ │ ├── ets/ │ │ │ │ ├── MainAbility/ │ │ │ │ │ ├── MainAbility.ts │ │ │ │ └── pages/ │ │ │ │ └── Index.ets │ │ │ └── module.json5 │ └── build-profile.json5 ├── index.js # RN 入口 ├── package.json这样划分的优点是构建时能通过脚本同时触发 RN 与鸿蒙的打包流程,减少手工切换的麻烦。
3.2 初始化 RN 工程并接入鸿蒙适配库
我通常用命令行直接初始化一个 RN 工程,名字就按自己业务来,注意不要用中文字符。
npx @react-native-community/cli init HarmonyDemo --version 0.72.5 cd HarmonyDemo然后安装鸿蒙官方提供的适配库,这一步很关键,它相当于在 RN 和鸿蒙之间建立起了通信管道的基础工具。
npm install @react-native-oh/react-native-harmony --save安装完成后,你需要进到 harmony 目录下做一次初始化配置。官方适配库会在工程里生成必要的桥接代码,但不会替你完成所有组件的封装。我这里碰到过一个问题:如果安装顺序搞反了,比如先配鸿蒙工程再安装插件,常常会找不到对应的原生模块定义。
3.3 配置鸿蒙工程与 DevEco 编译参数
打开 harmony 工程的build-profile.json5,你会看到类似这样的内容:
{ "app": { "signingConfigs": [], "products": [ { "name": "default", "signingConfig": "default", "compileSdkVersion": 10, "compatibleSdkVersion": 9, "runtimeOS": "HarmonyOS" } ] } }重点看compileSdkVersion和compatibleSdkVersion。我之前图省事,直接沿用 API 9 的配置,结果发现 RN 适配库的部分原生接口在旧 SDK 上找不到定义,编译直接挂掉。你可以根据实际需求把compileSdkVersion调成 10 或者更新的版本,但compatibleSdkVersion建议保持较低,这样老设备也能跑。
另外,你需要在module.json5里声明网络权限和必要的服务能力。如果你开发的应用要访问远程 JS Bundle,必须加上:
{ "module": { "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ] } }3.4 编写桥接模块并挂载到 RN
接下来是重头戏。假设你要开发一个鸿蒙原生日期选择组件,你想在 RN 页面里直接调用它。首先在鸿蒙侧创建一个DatePickerAbility.ts,这个 Ability 负责创建原生视图,并暴露给 RN。
import { Component, State } from '@ohos.arkui' import { RNPackageContext, NeedMakeComponent, AsyncCallback } from '@react-native-oh/react-native-harmony' @Entry @Component struct DatePickerComponent { @State currentDate: string = '2024-01-01' build() { Column() { Text(this.currentDate) Button('选择日期') .onClick(() => { // 这里可以从 JS 侧读取或返回数据 }) } } }然后你需要实现一个包装类,用来告诉 RN 这个组件的名字、属性和回调。
在 JS 侧,你写一个普通的 React 组件,通过requireNativeComponent或者TurboModule的方式引用原生模块。
import { requireNativeComponent } from 'react-native'; const NativeDatePicker = requireNativeComponent('DatePickerView'); export default function DatePicker(props) { return <NativeDatePicker currentDate={props.date} onChange={(e) => props.onDateChange(e.nativeEvent.date)} />; }这里,requireNativeComponent后面的字符串必须和鸿蒙侧注册的组件名完全一致,大小写都不能错。
3.5 构建并运行到鸿蒙设备
一切写完后,你需要先构建鸿蒙工程。打开 DevEco Studio,点击 Sync 和 Build,如果编译没问题,会生成一个 HAP 包。运行 RN 逻辑时,有两种方式加载 JS Bundle:一种是本地打包成 Bundle 文件,另一种是通过 Metro 开发服务器实时加载。我建议调试阶段用后者,能极大提升开发效率。
启动 Metro:
npx react-native start然后你在鸿蒙设备上打开你刚安装的 App,它会自动去连接开发机上的 Metro 服务。有一点要注意,设备和开发机必须在同一个局域网内,否则握手会失败。我之前踩过坑,以为是代码问题,结果查了半天发现是 IP 被防火墙挡了。
4. 核心实现与关键细节解析
这一节不再讲流程,而是深挖桥接后的技术细节。很多问题只在组件真正跑起来之后才会暴露,比如生命周期不同步、线程切换、数据格式转换。
4.1 生命周期同步:RN 页面与鸿蒙组件如何对齐
RN 里的组件有useEffect、componentDidMount这一套生命周期的概念,鸿蒙侧也有aboutToAppear、aboutToDisappear这套。问题是两边并不自动同步。你在 JS 侧卸载一个组件时,鸿蒙侧的原生视图不一定立刻销毁,这就可能造成内存泄漏或者事件堆积。
我试过的办法是,在 JS 侧调用onUnmount时,显式通知鸿蒙侧销毁对应实例。鸿蒙组件里用一个dispose()方法接收这个通知,主动去清理定时器、监听器。
aboutToDisappear() { // 清理资源 this.clearTimer(); }写到这里我想提醒一句,不要依赖 RN 框架帮你自动清理,毕竟桥接层不是它亲儿子,手动管理最踏实。
4.2 线程模型与 UI 更新
RN 的 JS 线程和 UI 线程是分离的,鸿蒙侧的 UI 更新只能在主线程执行。你在 JS 里发起一个异步任务,比如从 HTTP 接口拉取数据,拿到数据后再去更新鸿蒙组件,就需要确保数据传递后,鸿蒙侧的setState是在主线程触发的。
官方适配库封装了大部分异步回调,但我还是建议你把复杂的原生逻辑放到鸿蒙侧单独的线程里,处理好后再回到主线程更新 UI。示例如下:
import worker from '@ohos.worker'; const workerInstance = new worker.Thread('entry/src/main/ets/workers/DataWorker.ts'); workerInstance.postMessage({ action: 'fetchData' }); workerInstance.onmessage = (event) => { // 回到主线程更新状态 this.updateData(event.data); }4.3 数据传递格式:JS 与 ArkTS 的类型映射
桥接过程中最容易出问题的就是数据类型。JS 侧null和undefined在传输到 ArkTS 侧时,可能会变成空字符串或者变为0,各种奇怪的表现都有。我在一次传 JSON 串时,因为里头有个字段是null,结果 ArkTS 侧拿到后直接抛异常,排查了很久才发现是类型映射问题。
我的建议是:所有要传到鸿蒙侧的数据,一律在 JS 侧先JSON.stringify成字符串再传,在鸿蒙侧再JSON.parse回来,这样能用最简单的办法绕开各种边界情况。之所以不用对象直接传,是因为对象序列化时容易触发深拷贝的隐性 bug,而字符串是通用语言。
5. 实战中发现的问题与排查技巧
这里记录我实际测试中遇到的几个典型问题,按严重程度降序排列,最后附一张速查表。
5.1 构建失败:native module找不到
原因通常是注册名不一致。RN 侧调用的模块名如果带NativeDatePicker,那鸿蒙侧注册时必须完全匹配。但我发现有些版本要求大写开头,有些要求小写,不同适配库版本有差异。解决方法是打开鸿蒙侧的桥接代码,直接搜索registerComponent或者TurboModule的注册语句,查清楚确切的名称。
5.2 设备上页面白屏,但 Metro 日志正常
这是最常遇到的坑。Metro 显示的日志没问题,JS 也加载完成,但页面上就是白屏。这时候我第一个去查鸿蒙侧的日志,往往能看到 ArkTS 抛出的布局异常。比如我遇到过的Row组件没有设置高度,导致子组件渲染溢出,被系统吃掉但没报错。解决方法是给鸿蒙组件加一层明确的尺寸约束。
5.3 React Native 升级后桥接失效
RN 从 0.71 升到 0.72 之后,我发现旧版适配库的桥接失效,页面直接崩溃。原因是 RN 新架构引入了 Fabric,原生组件渲染方式和之前完全不同。这时候你只能升级鸿蒙适配库到支持新架构的版本,没有别的捷径。这也提醒你,生产环境不要盲目追新 RN 版本。
5.4 问题排查速查表
| 症状 | 可能原因 | 排查方向 | 解决建议 |
|---|---|---|---|
| 构建找不到模块 | 组件名不一致或导入路径错误 | 检查鸿蒙侧注册名与 RN 侧 require 的名字 | 统一命名,复制粘贴避免手敲 |
| 页面白屏 | ArkTS 布局异常或尺寸约束缺失 | 打开鸿蒙侧日志查看 ArkUI 报错 | 给根节点明确尺寸,测试时用固定宽高 |
| 数据传过去变成乱码 | JS 对象直接传导致序列化失败 | 打印鸿蒙侧收到的原始内容 | 统一使用 JSON 字符串传递方案 |
| 组件事件回调不触发 | 事件名未注册或大小写不一致 | 查看鸿蒙侧.onClick事件是否绑定到正确按钮类型 | 用官方适配库的AsyncCallback机制完成事件回调 |
| 运行后 CPU 占用过高 | JS 线程或原生线程循环未释放 | 在鸿蒙侧打点,检查aboutToDisappear是否执行 | 手动释放监听器和定时器 |
6. 桥接之后的维护与优化思路
组件能跑通只是第一步,真正头疼的是后续。一个被集成进来的鸿蒙组件,会跟着整套应用的版本迭代一起更新,稍有疏忽就容易出问题。所以在收尾之前,我想把自己后来沉淀下来的一些维护经验摆出来。
6.1 动态加载与按需初始化
如果你的应用里用到了很多鸿蒙组件,一次性全部初始化会拉慢启动速度。我后来给每个桥接组件加了一个按需加载的逻辑,只有当 JS 侧真正请求使用某个组件时,才去实例化对应的 Ability。这有点类似前端里的动态import,做起来不算复杂,收益却很直观。
鸿蒙侧的能力可以通过一个统一的管理类注册,通过startAbility去唤起特定组件。在 JS 侧我用requireNativeComponent配合懒加载标记,确保首屏不渲染更多原生模块。
6.2 数据本地缓存与同步
在集成鸿蒙组件时,经常遇到的一个矛盾是:JS 侧的本地存储和鸿蒙侧的本地存储是两套体系。如果你希望原生组件能读取到 JS 侧写入的数据,就不能各存各的。我采用的方案是,把需要共享的数据统一存放在鸿蒙侧,通过桥接接口供 JS 侧读写。这样做法避免了两边不一致的问题。
// JS 侧调用鸿蒙侧的存储接口 NativeStorage.setItem('userToken', 'abc123'); // 鸿蒙侧通过桥接接收 ...6.3 性能监控与调试工具选择
既然嵌入了原生组件,调试时就不能只盯着 Chrome DevTools。我长期用的方法是,鸿蒙侧打日志走 DevEco 的 HiLog,RN 侧打日志走 Metro,两边日志时间对上,就能定位延误出在哪一环。遇到 UI 卡顿,我会先在鸿蒙侧看是不是有掉帧告警,再看跨线程调用频次是否过高。
=== 写在最后 ===
最后再说一个我自己的体会。开发和调试鸿蒙桥接组件,和平时写纯 RN 模块的心态完全不同,因为你要同时处理两套生态,任何一个变量对不上都可能导致连锁故障。我吃过最大的亏,是无脑把一套配置照搬到新版 SDK,结果跑了几天才知道某些接口已经废弃。踩过几次坑之后,我的习惯是:每次升级前先通读一遍适配库的 Release Notes,再动手,绝不跳过。
如果你也打算在 React Native 项目里集成鸿蒙组件,别急着写业务,先用一个最小的 Demo 跑通完整链路,形成肌肉记忆,再往里面填实际逻辑。记住,初期把桥接层做扎实,才是后面省时间的正确姿势。