ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

React Native鸿蒙适配实战:RNOH桥接与自定义组件开发指南

React Native鸿蒙适配实战:RNOH桥接与自定义组件开发指南 去年年底我们团队接到一个挺头疼的需求公司在推的超级App必须上鸿蒙生态但整套业务代码都是React Native写的重新用ArkTS从头撸一遍工期至少翻三倍。当时鸿蒙生态正好处在一个微妙的节点——新的系统版本不再兼容安卓APK市面上跑的RN框架基本全是基于旧架构的兼容方案几乎没有能直接在纯血鸿蒙上跑起来的路子。我们调研了一圈最后把宝押在了react-native-harmony这套由OpenHarmony SIG维护的适配方案上磕磕绊绊踩了整整一个多月坑总算是把RN工程完整跑上了鸿蒙真机还把几个核心业务模块做成了自定义鸿蒙组件桥接给RN调用。这篇文章就是基于这一轮实战写的核心会聊清楚三件事RN和鸿蒙之间到底是怎么打通关系的、写一个鸿蒙原生组件并塞进RN工程的具体流程是什么、以及你在真机上会遇到哪些典型的白屏问题和排查方法。适合两类人看一是手头有RN存量代码、正在被鸿蒙适配逼疯的移动端开发二是想搞懂“跨端框架怎么接原生系统”这套底层逻辑的鸿蒙初学者。看完之后你应该能照着文章里的步骤两三天内把自己的RN项目跑上鸿蒙设备。1. 先把概念理清HarmonyOS、ArkTS、RNOH三者是什么关系1.1 HarmonyOS的两种存在形式决定了你的技术选型聊鸿蒙开发之前必须先把这个系统的“两张脸”搞清楚因为很多RN适配方案翻车都是因为在一开始就搞混了运行环境。早期鸿蒙系统其实走的是双框架路线底层保留了一部分AOSP安卓开源项目的兼容层可以直接安装和运行安卓APK。在那种环境下React Native通过安卓的编译目标就能跑起来压根不需要额外适配很多“假鸿蒙适配”就是这么实现的——把RN的安卓包塞进鸿蒙设备能用是能用但走的还是安卓虚拟机那一套性能损耗和系统能力割裂的问题始终存在。而2024年底全面铺开的新版本则彻底砍掉了AOSP兼容层系统从底层到UI层全部换成了自研技术栈——底层是鸿蒙内核UI层是ArkUI声明式框架开发语言是ArkTSTypeScript的超集应用打包格式也变成了HAP/HSP。这意味着安卓APK直接装不上去了RN项目想要在新版鸿蒙上运行就必须走原生的鸿蒙适配链路。这条链路就是react-native-harmony社区里通常简称RNOHReact Native OpenHarmony。1.2 react-native-harmony到底是个什么东西RNOH本质上是一套将React Native运行时移植到鸿蒙系统上的桥接层它的设计思路和RN在iOS、Android上的原生适配如出一辙只不过目标平台从UIKit/Android Framework换成了ArkUI框架。你可以在官方仓库里看到它的核心组成部分一个是react-native-harmony这个包它包含了RN运行时在鸿蒙侧的C实现和ArkTS层封装另一个是react-native-oh/tester这类配套工具用来跑RN官方的组件测试套件。在架构上它支持了RN新架构的Fabric渲染器和TurboModule模块系统这意味着你写的鸿蒙原生组件可以像iOS/Android的自定义组件一样通过统一的接口暴露给JS侧调用。值得一提的是这套方案的迭代速度很快。我们接入的时候是RN 0.75版本几个月后社区已经把适配版本推进到了0.77以上每次RN发版后鸿蒙侧都会跟进做兼容适配。所以你在选型时尽量挑社区活跃维护的版本不要图新以RNOH官方文档里标注的“已支持版本”为准。1.3 为什么你的RN项目需要鸿蒙组件可能有人会问如果RN已经能在鸿蒙上跑了那为什么还要费劲去写原生鸿蒙组件直接在RN里用JS写业务不就行了吗这个问题问到了点子上。RNOH解决的是“RN代码能在鸿蒙上运行”的问题但鸿蒙的一大核心竞争力——分布式能力、系统级服务、安全组件——绝大部分只对原生ArkTS代码开放。比如你需要在应用里接入华为账号登录、调用系统级的原子化服务、或者操作分布式软总线让设备之间流转数据这些能力在RN的JS侧根本没有现成的模块可用必须通过编写鸿蒙原生组件在鸿蒙工程里也叫“扩展组件”或“HSP模块”来封装底层API然后桥接给RN调用。另外还有一类场景RN的跨端一致性再好也保不齐在复杂交互动画上会有点力不从心比如列表滑动跟手度、复杂手势的响应链。这时候把高频组件下沉到鸿蒙原生侧用ArkUI的能力去实现再通过桥接层暴露给RN既能保证体验又不用把整个页面都推倒重写。2. 环境准备与工程改造让RN跑在鸿蒙设备上2.1 需要准备的工具和版本清单在开始动手之前先把环境装齐这一步省不了。下面是我实际踩过坑之后整理出来的推荐配置按官方文档的版本要求来能少走很多弯路工具推荐版本作用说明DevEco Studio5.0.3及以上华为官方IDE用来编译鸿蒙工程、连接真机调试Node.js18.x或20.x LTSRN开发的基础运行时JDK17鸿蒙工程Gradle构建需要注意别用JDK 8React Native0.75.0以上RNOH适配较完善的版本基线react-native-harmony与RN版本一一对应鸿蒙侧的核心适配包hdc工具DevEco内置类似于adb用于连接鸿蒙真机、查看日志这里要特别提醒一下JDK版本的问题。我们在初始化工程时有同事用的JDK 11结果Gradle构建直接报错排查了半天才发现是版本不兼容。DevEco Studio 5.x强制要求JDK 17所以如果你机器上装了多个JDK记得把JAVA_HOME环境变量指过去。2.2 从零初始化一个支持鸿蒙的RN工程RNOH官方推荐用react-native-oh/community-cli来初始化工程它会帮你把鸿蒙侧的工程骨架一起生成好。命令很简单npx react-native-oh/community-cli init RNHarmonyDemo这个命令会创建一个标准的RN工程但和普通RN工程最大的区别是它多了一个harmony目录里面就是鸿蒙原生工程的源码——包括entry模块、hvigor构建配置、以及通过oh-package.json5声明的鸿蒙依赖。初始化完成之后还需要安装鸿蒙侧运行时的核心依赖cd RNHarmonyDemo npm install npm install react-native-harmony0.75.5装完之后建议先跑一遍官方自带的检查脚本确认RN版本和react-native-harmony版本是否能对得上npx react-native check-harmony这个命令会列出当前工程鸿蒙侧的配置情况包括SDK路径、hvigor版本以及依赖匹配状态。如果显示OK说明工程基建已经打通可以进入下一步。2.3 在DevEco里跑起第一个鸿蒙版RN应用现在用DevEco Studio打开刚才生成的harmony目录注意不是在根目录点“打开”而是选中harmony这个子目录。DevEco会识别到这是一个鸿蒙工程然后自动同步依赖。首次同步的时间可能会比较长因为要下载鸿蒙SDK、编译工具链和C依赖我们团队第一次同步花了将近20分钟。同步完成后按照以下步骤就能把应用跑起来确认设备连接用USB连接鸿蒙真机在DevEco的设备列表里能看到设备型号。配置自动签名鸿蒙应用默认要求签名DevEco里可以用“File - Project Structure - Signing Configs”自动生成调试签名注意这一步需要登录华为账号。选择entry模块和真机设备点击Run按钮等待编译完成。如果你是第一次跑大概率会遇到编译报错最常见的错误是Native依赖下载失败。这时候别慌看报错日志里提示的下载地址把对应的.tar.gz包手动下载到~/.ohpm目录下再重新编译就好。跑起来之后你在真机上会看到RN的初始页面。但我得提前打预防针很多人第一次跑看到的往往是白屏而非RN页面。别急着骂街这个问题的排查思路我放在后面的章节专门讲。3. 深入开发用ArkTS写一个能跟RN通信的鸿蒙组件3.1 ArkTS和ArkUI先花10分钟抓住核心如果你之前没有接触过鸿蒙原生开发第一次打开ArkTS代码时可能会觉得既熟悉又陌生。熟悉的是语法ArkTS是TypeScript的超集变量声明、类型系统、接口定义几乎和TS一样陌生的是UI写法ArkUI用的是声明式开发范式界面结构写在build()方法里用链式调用的方式配置组件属性。我拿一个最简单的例子来说Component export struct HelloComponent { Prop message: string Hello build() { Column({ space: 10 }) { Text(this.message) .fontSize(20) .fontColor(#333333) Button(Click Me) .onClick(() { // 事件处理逻辑 }) } .padding(20) } }看到没Column是垂直布局容器类似于RN里的View的flexDirection: columnText就是文本组件Button是按钮。组件的状态管理通过装饰器实现Prop表示从父组件传入的简单属性State表示组件内部状态Link表示与父组件共享状态。对于RN开发者来说最需要转变的一个思维是RN里一个页面是由无数个JS组件组合而成而ArkUI里你其实是在用类似“Flutter的Widget树”的思维构建UI。组件树越深状态管理越要谨慎。3.2 实战目标做一个环形进度组件好了理论铺垫完毕现在进入正题。我们的目标是写一个环形进度组件它接收两个参数——progress百分比数值和size组件尺寸在鸿蒙原生侧用ArkUI的Progress组件渲染一个圆形进度环同时支持进度变化时平滑动画过度到新值。这个组件的价值在于它刚好覆盖了RN侧不擅长的高频UI绘制场景。如果你用RN自带的Animated去做环形进度动画计算和绘制全在JS线程跑帧率很难稳定在60帧而放到鸿蒙原生侧ArkUI的组件动画直接走系统渲染管线性能和流畅度完全不在一个量级。选择Progress组件还有个考虑它本身在ArkUI里的配置方式很典型涉及属性配置、样式定制和事件回调学会了它其他UI组件的桥接思路也就一通百通了。3.3 在鸿蒙侧实现原生组件并导出首先在鸿蒙工程的entry/src/main/ets目录下创建一个CircleProgressView.ets文件。代码如下import { Component, Prop, State } from react-native-harmony; Component export struct CircleProgressView { Prop progress: number 0; Prop size: number 100; State progressValue: number 0; aboutToAppear() { this.progressValue this.progress; } onProgressChange(progress: number) { this.progressValue progress; } build() { Stack({ alignContent: Alignment.Center }) { Progress({ value: this.progressValue, total: 100, type: ProgressType.Circle }) .width(this.size) .height(this.size) .color(#3D7FFF) .backgroundColor(#E5E5E5) .style({ strokeWidth: 8 }) .animation({ duration: 300, curve: Curve.EaseOut }) Text(${Math.round(this.progressValue)}%) .fontSize(18) .fontColor(#333333) } .width(this.size) .height(this.size) } }这个组件做的事情并不复杂用Prop接收RN侧传入的progress和size初始化时把进度存入内部状态progressValueUI渲染时用Progress组件画一个圆形进度环并在中间显示进度百分比文字。animation属性让进度条变化时有300毫秒的平滑过渡。注意我在这里引入了一个onProgressChange方法它是留给RN侧动态更新进度用的下面会讲怎么把它暴露出来。接下来需要把自定义组件注册到RN的组件管理器中。在RNOH框架里这一步是通过TurboModule机制实现的。新建一个CircleProgressModule.ets文件import { TurboModule } from react-native-harmony; export class CircleProgressModule extends TurboModule { static readonly NAME CircleProgress; updateProgress(progress: number): void { // 通过全局事件或状态管理把新进度值推送给UI this.emitEvent(onProgressChange, { progress }); } }然后在模块注册表里声明这个模块import { CircleProgressModule } from ./CircleProgressModule; export const createNativeModules { CircleProgress: CircleProgressModule, };RNOH框架在应用启动时会自动扫描并注册这些模块。这样JS侧就能通过NativeModules.CircleProgress访问到原生模块的方法了。3.4 在RN侧接入并传递参数和回调鸿蒙侧的桥接层做完回到RN工程里我们要做两件事封装一个RN组件来承载原生组件以及通过NativeModules调用原生模块的方法。先封装RN端组件。新建CircleProgress.tsximport React, { useEffect, useRef } from react; import { requireNativeComponent, NativeModules, Platform } from react-native; const NativeCircleProgress requireNativeComponent(CircleProgressView); const { CircleProgress } NativeModules; interface Props { progress: number; size?: number; } const CircleProgressWrapper: React.FCProps ({ progress, size 100 }) { const progressRef useRef(progress); useEffect(() { progressRef.current progress; if (Platform.OS harmony CircleProgress) { CircleProgress.updateProgress(progress); } }, [progress]); return NativeCircleProgress progress{progress} size{size} style{{ width: size, height: size }} /; }; export default CircleProgressWrapper;要注意的是在RNOH的适配版本里requireNativeComponent依然可用它会把JS组件映射到鸿蒙侧的同名原生组件上。progress和size两个prop会被序列化后传递给原生组件并自动更新到Prop变量。使用的时候就更简单了import CircleProgress from ./CircleProgress; const App () { const [progress, setProgress] useState(0); useEffect(() { const timer setInterval(() { setProgress((p) (p 100 ? 0 : p 1)); }, 200); return () clearInterval(timer); }, []); return ( View style{{ flex: 1, justifyContent: center, alignItems: center }} CircleProgress progress{progress} size{120} / /View ); };跑起来之后你应该能在鸿蒙真机上看到一个环形进度条以每秒5%的速度递增到100%后自动归零重来中间的动画过渡丝滑得像斧头帮的舞步——这就是原生组件桥接带来的性能红利。4. 实践中的坑启动白屏、调试与性能调优4.1 启动白屏问题排查与解决在所有“人生第一次RN鸿蒙运行”里白屏可能是最常见的一道坎。我见过太多开发者在论坛里发帖问“RN项目跑鸿蒙为什么白屏”其实绝大多数情况下问题并不在RN或RNOH本身而在下面这三个地方。第一个是Bundle加载路径错误。RN应用需要从Metro服务拉取JS Bundle在鸿蒙侧运行时如果entry/src/main/ets/entryability/EntryAbility.ets里的loadBundle方法配置的路径不对应用启动后就会因为找不到Bundle而白屏。常见的正确写法是import { loadBundle } from react-native-harmony; loadBundle({ bundlePath: entry/src/main/resources/base/assets/index.bundle, sourceMapPath: entry/src/main/resources/base/assets/index.map, });注意这里的路径是相对entry/src/main目录的如果你的Bundle文件名不叫index.bundle记得同步修改。第二个是Metro服务没有启动或者端口不通。不管是模拟器还是真机调试RN都需要Metro提供Bundle。鸿蒙真机的调试方式和安卓类似通过USB连接后DevEco会做端口转发。如果Metro服务没启动或者主机防火墙挡了8081端口应用拉了空Bundle自然就是白屏。建议统一用npx react-native start先在终端把Metro跑起来然后再启动鸿蒙应用。第三个是版本不匹配导致的无声失败。这种情况最坑RN版本和react-native-harmony版本不一致编译能通过但运行时初始化失败且没有明显报错。我们当时就是RN 0.76配了0.75的适配包白屏了一下午最后用npx react-native check-harmony才查出来。所以再次强调版本匹配置关重要别偷懒跳过去。排查白屏问题时有个很有用的技巧在DevEco的Log窗口里加一个过滤条件只显示包含ReactNative或RNOH的日志。RNOH在启动过程中会打很多关键日志比如Bundle加载成功、RootView创建完成、JS执行完毕等。哪一步还没打出来问题就出在那一块定位起来会快很多。4.2 开发调试三板斧日志、断点、抓包跨端开发最怕的就是问题定位难代码到底是JS侧的锅还是原生侧的锅边界很模糊。我的经验是一套完整的调试链路能省下一半排查时间。第一斧是JS侧日志。RNHOH支持JS层的console.log输出到DevEco的Log窗口但默认日志级别可能过滤掉了INFO级别以下的信息。如果你发现看不到JS日志去DevEco的Log配置里把过滤级别调到INFO或者在JS侧用console.warn和console.error这两个级别的日志默认一定展示。第二斧是原生侧日志。ArkTS代码里的hilog输出在DevEco的Log窗口里通过关键字CircleProgress等模块名可以快速过滤。我们平时写原生组件时习惯在每个桥接方法的入口和出口各打一条日志传入参数和返回结果都打出来这样JS侧有没有调用到、参数有没有对齐一目了然。第三斧是抓包。在真机上调试时不管是Metro请求还是业务HTTP请求都可以用抓包工具查看。鸿蒙系统目前对中间人抓包的限制还比较宽松开发模式下基本能抓到明文流量。我们排查过一个登录模块的问题就是用抓包确认了RN侧发出去的请求Body是乱码最后定位到是序列化编码格式没对齐跟鸿蒙桥接层一点关系都没有。4.3 常见问题速查表我把这一轮项目中遇到的典型问题整理成了一张速查表遇到类似情况可以直接照着排查问题现象可能原因解决方案应用启动直接闪退RN与RNOH版本不匹配运行npx react-native check-harmony检查版本兼容性启动后白屏Bundle加载路径错误检查EntryAbility.ets里的loadBundle路径配置启动后白屏Metro服务未启动终端运行npx react-native start保持服务常驻原生组件无法渲染组件名拼写不一致确保ArkTS侧组件名与JS侧requireNativeComponent参数完全一致属性传值不生效属性名与Prop变量名不一致统一使用驼峰命名检查大小写组件显示出来但样式异常鸿蒙单位与RN的dp不一致鸿蒙侧用vp单位计算尺寸时要考虑像素比转换动画卡顿动画逻辑跑在JS线程把高频动画下沉到ArkUI原生组件中实现真机调试时刷新无反应设备与Metro端口不通检查USB连接重新执行端口转发命令除此之外还有一个非常实用的周边HarmonyOS NEXT的纯血环境没有安卓兼容层所以不要再问“在鸿蒙上能不能直接跑安卓包”这种问题了答案是不能。RN项目想在鸿蒙上跑唯一的正道就是走RNOH的原生适配路线。5. 把组件放进真实工程工程化与扩展建议5.1 组件包化和版本管理如果你只是写一两个演示组件给领导看那把代码直接塞在entry模块里就够了。但一旦组件数量超过三个或者要交给多个业务团队使用就必须考虑包化治理。鸿蒙侧的组件打包格式是HSPHarmonyOS Shared Package类似于安卓的AAR或iOS的Framework。思路是把通用组件放到一个独立的HSP模块里业务模块通过依赖HSP来引用组件。这样做的好处是组件可以独立迭代、独立发布版本变更不会影响主工程同时多个业务可以共享同一份原生代码避免重复开发。在RN工程层面对应的做法是把桥接组件封装成独立的npm包。仓库结构大概是这样的- harmony/ - hsp_circle_component/ # 鸿蒙原生HSP模块 - src/main/ets/ - CircleProgressView.ets - CircleProgressModule.ets - src/ - components/ - CircleProgress.tsx # RN侧封装组件 - package.jsonRN侧的同时发布npm包时要把版本号跟鸿蒙HSP包的版本号关联起来。我们内部定了一个规则npm包版本和HSP版本保持一致大版本号的变更意味着桥接API可能不兼容小版本号则只是增量修改。这样依赖方升级时心里有数不会出现JS侧调了原生侧没有的方法这种尴尬情况。5.2 鸿蒙分布式能力怎么从RN侧调用我前面提到过把组件下沉到鸿蒙原生侧最大的动力之一是可以调用系统能力。这里以分布式文件读写为例讲讲桥接的思路。鸿蒙的分布式能力体现在你可以通过distributedKVStoreAPI实现跨设备的数据同步。比如用户手机上的便签数据可以通过分布式数据库自动同步到平板设备上。这个能力在RN侧没有现成封装但我们可以在鸿蒙原生模块里封装一个DistributedStoreModule暴露给JS侧export class DistributedStoreModule extends TurboModule { async put(key: string, value: string): Promiseboolean { // 初始化分布式数据库 // 写入键值对 // 返回写入结果 } async get(key: string): Promisestring | null { // 根据key读取值 } }JS侧就能写成这样const DistributedStore NativeModules.DistributedStore; const saveData async (key: string, value: string) { const ok await DistributedStore.put(key, value); if (ok) { console.log(数据已同步到分布式数据库); } };这种封装模式可以复用到任何鸿蒙系统能力上华为账号登录、推送、安全存储、原子化服务跳转等。核心思路都是同一个——原生侧封装JS侧调用参数传递遵循JSON序列化规则。5.3 团队协作的建议谁写JS谁写ArkTS最后聊聊团队分工。RN和鸿蒙混合开发对团队的技能栈要求其实很高既要有懂RN的跨端开发又要有懂ArkTS和ArkUI的原生开发。我见过不少团队在这上面栽跟头要么是RN开发自己硬啃ArkTS写出来的原生代码性能拉胯还难维护要么是原生开发闭门造组件桥接API设计得极其反人类JS侧调用起来痛苦不堪。比较合理的分工方式是原生侧的同学负责把鸿蒙的系统能力封装成粒度适中的桥接模块设计好API的输入输出JS侧的同学负责业务逻辑和UI组装只通过桥接模块调原生能力不直接碰ArkTS代码。两边约定好一套接口规范原生侧保证实现JS侧保证调用各自在各自的领域内发光发热。另外务必把RNOH官方文档里“Supported Components”和“Supported Modules”的清单打印出来贴在工位上。这份清单列出了当前版本支持哪些RN内置组件和模块比如View、Text、ScrollView都有但有些第三方库如react-native-maps就可能没有对应适配。开发前先查清单能避免很多“JS侧跑着跑着突然崩了”的意外情况。写在最后的经验小结这一趟RN鸿蒙适配走过来最大的感受是技术方案永远是在权衡中前进的RNOH不算完美它要求你同时懂RN和鸿蒙要求你接受一些第三方库的适配空白要求你在性能吃紧时愿意下沉到原生侧写代码。但它切切实实地救活了我们的存量RN工程让我们不用把几千个页面推倒重写。如果让我给后来者一个建议那就是先别急着大规模迁移。找一个RN工程里的核心业务模块按这篇文章的流程把它改造成“RN壳鸿蒙原生组件”的混合模式在真机上完整跑一遍验证性能和体验。等技术团队对这个模式成熟了再逐步扩大鸿蒙侧的实现范围。另外RNOH的社区更新很快每隔几周就会有新的适配版本和文档更新保持关注及时跟进你会发现这条路会越走越顺。
返回列表