最近一个做跨端开发的朋友找我吐槽,说领导突然丢来一个需求:把公司App的React Native版跑到鸿蒙手机上,还要顺手封装几个“鸿组件”。我一听就知道,这又是一个被鸿蒙生态吸引、却低估了工作量的团队。说实话,React Native和鸿蒙开发结合这件事,没有想象中那么神秘,但也绝对不只是一个npm包装完就结束。它要求你同时理解RN的桥接机制、ArkTS的组件写法、Stage模型的工程约束,以及真机调试那套链路。这篇文章我就把这几个月跑通的经验拆开讲,特别是“在RN项目里集成鸿蒙应用”和“封装鸿组件”的完整过程。
1. 为什么要在React Native里做鸿蒙组件
1.1 跨端方案绕不开的鸿蒙生态
鸿蒙OS是华为推出的分布式操作系统,覆盖手机、平板、智能屏和各类IoT设备。现在很多App都在做鸿蒙版本适配,但团队里不一定有人熟悉ArkTS和ArkUI,也没有精力完全另起一套原生代码。这种情况下,最现实的路线就是保留React Native的JavaScript/TypeScript业务层,把鸿蒙当成一个新的“原生平台”接进来。于是“鸿组件”这个概念就出现了——它不是一个官方术语,而是开发者对“运行在鸿蒙原生侧、由RN调用的组件和模块”的通俗叫法。
为什么绕不开?因为RN本身只提供跨平台运行时,最终要渲染到具体平台的UI框架上。在iOS上它渲染到UIKit,在Android上渲染到View,到了鸿蒙上就需要一个适配层把RN的Shadow Tree映射到ArkUI的组件树。这层映射不是白来的,需要原生侧提供组件管理器、事件分发器和生命周期绑定。也就是说,你写的业务代码是RN的,但每一个被调用的原生组件,背后都要有鸿蒙代码兜底。
1.2 选型对比:直接原生ArkTS还是RN桥接
很多团队在立项时会纠结:既然都要写鸿蒙原生代码,为什么不干脆全用ArkTS写?我的判断标准很简单:看你的核心业务代码是不是已经在RN里沉淀了很久。如果是一套成熟业务,强行用ArkTS重写一遍,意味着两套代码库长期并行维护,每次需求变更都要双倍工时。用RN桥接鸿蒙,则可以让业务层继续复用,鸿蒙侧只做“壳”和“原生能力补充”。
这里有一个容易被忽略的细节:RN在鸿蒙上的性能瓶颈通常不在JS执行,而在于原生组件的创建和更新频率。如果你的界面里有大量高频刷新的自定义组件,比如图表、游戏画布,那无论你用哪种方案,都要仔细设计组件边界,把高频更新的部分尽量收敛在ArkUI侧,而不是每一次状态变化都走一遍RN桥接。基础的业务页面、列表、表单,RN在鸿蒙上的表现已经完全可用。
| 方案 | 业务复用率 | 原生能力覆盖 | 团队学习成本 | 长期维护成本 |
|---|---|---|---|---|
| 纯ArkTS重写 | 低 | 高 | 高 | 高 |
| RN桥接鸿蒙 | 高 | 中高 | 中 | 中低 |
| WebView套壳 | 中 | 低 | 低 | 高(体验差) |
我最终选择的是RN桥接方案。理由很直接:业务迭代压力大,不可能给鸿蒙单独养一条产品线。但我也提醒一句,桥接方案不是“零原生开发”,你至少要有一个成员能看懂ArkTS,理解鸿蒙工程结构,否则遇到原生报错就抓瞎。
2. 鸿蒙开发基础:不补课直接上手会踩坑
2.1 ArkTS和ArkUI,别当它是TypeScript
很多RN开发者第一次打开鸿蒙工程时会觉得ArkTS眼熟,因为语法上很像TypeScript。但如果你真把它当TypeScript写,很快就会踩坑。ArkTS在TypeScript基础上做了静态类型强化,限制了一些动态特性,比如any的使用场景很受限,对象字面量必须符合明确类型,函数和方法不支持太多隐式转换。这背后是方舟编译器做静态优化的需求,宁可多写几个interface,也不要依赖运行时动态拼对象。
ArkUI则是鸿蒙的声明式UI框架,写法和SwiftUI、Flutter越来越像。一个最小的页面长这样:
@Entry @Component struct HelloPage { @State message: string = 'Hello HarmonyOS'; build() { Column({ space: 12 }) { Text(this.message) .fontSize(24) .fontWeight(FontWeight.Bold) Button('点击更新') .onClick(() => { this.message = 'Hello React Native'; }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } }注意这里的状态是@State修饰的,数据变了,组件自动刷新。这和RN里的useState在思路上是一样的,但底层实现完全不同。RN是通过重新执行render函数做diff,ArkUI则是细粒度观察依赖,只更新被@State标记的变量所关联的UI节点。理解了这个区别,你写鸿组件时就不会犯“频繁创建临时对象导致UI刷新异常”的低级错误。
2.2 Stage模型与组件生命周期
鸿蒙开发的工程模型不是Activity,也不是ViewController,而是Stage模型。入口是UIAbility,可以理解成一个应用能力的容器,页面则放在src/main/ets/pages目录下。每个页面组件都有aboutToAppear、aboutToDisappear、onPageShow、onPageHide等生命周期回调,和RN组件的useEffect有对应关系,但并不完全一致。
@Entry @Component struct DetailPage { aboutToAppear(): void { console.info('页面即将出现'); } onPageHide(): void { console.info('页面隐藏'); } build() { Text('Detail') } }如果你要在鸿组件里做资源申请、事件订阅、定时器管理,一定要在合适的生命周期里做清理,不要依赖RN侧来通知你“页面已经卸载”。很多时候RN侧组件已经卸载了,鸿蒙原生组件可能还挂在视图树里,或者反过来,原生侧已经销毁了,JS侧还在回调。我的建议是:原生组件自己管理好生命周期,JS侧只做数据驱动。
2.3 三个高频布局和状态知识点
ArkUI的布局核心是容器组件Column、Row、Stack、RelativeContainer等,其中Stack是绝对定位容器。RN开发者容易在这里犯迷糊:Stack类似View+position: absolute,但它的alignContent控制所有子组件在主轴和交叉轴上的对齐方式,子组件可以通过alignSelf覆盖父级约束。
比如有人问“Stack布局子组件怎么控制在底部上方100的位置居中”,直接按直觉写margin或者position很容易偏。最稳的做法是这样:
Stack({ alignContent: Alignment.Bottom }) { Text('我在底部上方100,水平居中') .margin({ bottom: 100 }) }alignContent: Alignment.Bottom表示子组件整体靠底部对齐,默认水平居中;再给子组件一个bottom方向的margin,就实现了“底部上方100居中”。如果你想要某个子组件特立独行,给那个子组件加alignSelf覆盖,比如alignSelf(ItemAlign.Start)让它单独靠左。掌握这个组合方式,做悬浮按钮、底部面板、角标提示都会顺手很多。
@State是组件内可变状态,@Prop是父传子的单向数据,@Link是双向同步。封装鸿组件时,尽量把对外暴露的属性设计成@Prop或普通参数,不要滥用@Link,否则RN侧一个props变化可能引发原生侧多节点更新,性能不好控制。
3. 在React Native项目中集成鸿蒙应用
3.1 环境准备:这些工具一个都不能少
想在RN项目里跑鸿蒙平台,你得先满足一套工具链。我用的是当前社区里比较常见的组合:Node.js 18+、DevEco Studio 5.0+、HarmonyOS SDK、JDK 17,还需要安装React Native CLI。这里重点提醒:DevEco Studio安装的时候要勾选SDK组件,默认可能只装了基础工具链,真机调试还要配置签名,不是装上就能跑。
| 工具 | 版本建议 | 用途 |
|---|---|---|
| Node.js | 18 LTS或更高 | 运行RN CLI和Metro |
| DevEco Studio | 5.0+ | 鸿蒙工程IDE、SDK管理、真机调试 |
| HarmonyOS SDK | 与DevEco配套 | 编译鸿蒙原生代码 |
| JDK | 17 | 鸿蒙工程Gradle/Hvigor构建 |
| react-native-harmony | 当前稳定版 | RN到鸿蒙的运行时适配层 |
环境配置的坑一般集中在版本不匹配上。比如Node版本过低,导致Metro的依赖装不上;或者DevEco Studio的SDK版本和react-native-harmony编译要求不一致,构建时直接报找不到ohos相关SDK接口。我的建议是先翻一遍官方的版本兼容表,再动手安装。不要拿“最新版本”直接冲,RN桥接鸿蒙的成熟度还没有到随便升级都稳的程度。
3.2 初始化鸿蒙工程,把RN项目“嫁接”进去
现在社区里最常用的是react-native-harmony方案,官方文档通常叫RNOH。它的基本思路是:创建一个标准的RN项目,然后初始化一个鸿蒙工程目录,把鸿蒙原生代码放在harmony目录下,JS业务代码继续放在RN的src里。
我实际用的步骤大概是这样:
# 1. 创建RN项目 npx @react-native-community/cli init MyApp # 2. 进入项目 cd MyApp # 3. 安装鸿蒙适配依赖 npm install react-native-harmony # 4. 初始化鸿蒙工程目录 npx rnoh init执行完以后,项目里会多出一个harmony文件夹,里面是完整的DevEco工程。这时候用DevEco Studio打开harmony目录,它就能识别到底层配置。注意一定要打开harmony目录而不是项目根目录,否则IDE只看到RN的文件,看不到鸿蒙的构建脚本。如果你用的是自己已有的RN项目,先检查一下React Native版本是否在支持范围内,版本太低或太高都可能出现初始化失败。
3.3 跑通真机调试链路
工程能编译不等于跑通。第一次跑真机,我卡了快一下午,最后发现问题出在Metro的加载地址上。模拟器可以直接访问宿主机localhost,但真机不能,你需要在应用启动时指定电脑的局域网IP。
在鸿蒙侧的entry模块里,一般会有一个配置BundleCodeLoader的地方。标准RN是从http://localhost:8081/index.bundle加载JS,真机要改成http://你的电脑IP:8081/index.bundle。同时确保手机和电脑在同一WiFi下,Metro命令窗口不要关,防火墙不要拦截8081端口。这一套和Android真机调试很像,但鸿蒙的配置项名称不同,别对着Android的文档找。
跑通之后,再用DevEco Studio的Log窗口看鸿蒙侧日志,用React Native的Metro窗口看JS侧日志。两边日志拉通对照,才能快速定位问题出在原生层还是业务层。
4. 开发“鸿组件”:一次完整的桥接实操
4.1 先搞懂JS和ArkUI怎么通信
鸿组件听起来很高端,实际上就是把RN需要的原生能力封装成两个东西:一个是原生UI组件,负责渲染;一个是原生Module,负责提供非UI能力。在RN老架构里叫ViewManager和NativeModule,新架构里叫Component和TurboModule。react-native-harmony同样实现了这套抽象,只是底层对接的是ArkUI组件和鸿蒙系统API。
你可以这样理解:JS侧的人要订餐,ArkUI侧的人要接单。JS侧发一个“我要创建一个进度条”的指令,原生侧接到后,调用对应的ArkUI构造函数创建出真实组件;JS侧再发一个“progress改成80”,原生侧就更新组件属性。整个过程看起来是同步的,实际上中间隔着一道桥接层。所以你在原生侧接口里写的每个方法,都要想着它会被JS高频调用,参数尽量简单,不要传复杂对象。
4.2 实例:做一个环形进度条鸿组件
我用手头的“进度环”组件举个例子。需求很简单:RN传入一个progress数字,鸿蒙侧画一个环形进度条,用户点击时把点击事件传回RN。这个组件在鸿蒙侧是一个ArkUI组件。
@Component export struct ProgressRing { @Prop progress: number = 0; onRingClick: () => void = () => {}; build() { Column() { Progress({ value: this.progress, total: 100, type: ProgressType.Ring }) .width(120) .height(120) .onClick(() => { this.onRingClick(); }) } } }这里@Prop接收外部传入的进度值,onRingClick是一个回调函数占位,真正的实现由RN侧注入。如果你需要更复杂的样式,还可以在ArkUI侧暴露颜色、尺寸、动画时长等属性,原理都一样。
4.3 在RN侧集成并调用鸿组件
有了原生组件,接下来要把它注册给RN。大致思路是创建一个ViewManager,然后在JS侧通过requireNativeComponent或代码生成的方式拿到这个组件。代码可能长这样,注意不同版本API会有差异:
// ProgressRingViewManager.ets import { ViewManager } from 'react-native-harmony'; export class ProgressRingViewManager extends ViewManager { public createViewInstance() { return new ProgressRing(); } }注册完成以后,RN侧就可以把它当成普通组件来用:
import React from 'react'; import { requireNativeComponent, Platform } from 'react-native'; const ProgressRing = requireNativeComponent('ProgressRing'); export function ProgressRingExample({ progress, onRingClick }) { return ( <ProgressRing style={{ width: 120, height: 120 }} progress={progress} onRingClick={onRingClick} /> ); }如果你的项目用了New Architecture和Codegen,可以让工具自动生成类型接口,但刚开始没必要上全套Codegen,先跑通再逐步完善。直接写requireNativeComponent的缺点是类型提示弱,但胜在直接,适合验证链路。
4.4 参数同步和事件回调的注意事项
实际封装时,最常见的毛病不是写不出来,而是写出来的组件更新不及时。ArkUI里@Prop接收的是值拷贝,如果RN侧传入一个对象或者数组,你在子组件里修改它,父组件不会感知,因为它们是各自独立的引用。这时候要把数据拆成多个简单类型,或者改用@Link做双向绑定。
事件回调也要注意序列化。RN和鸿蒙侧通信时,事件数据本质上要经过一个序列化层。你如果试图通过回调传一个Date对象、Map或者自定义类实例,很可能会在桥接层被转成普通对象,丢掉原型链。我处理事件参数的规矩很简单:只用基本类型、字符串或者扁平JSON对象。这个习惯帮我避开了很多八竿子打不着的“取值undefined”问题。
5. 常见问题与排查技巧实录
5.1 RN启动白屏
“RN启动白屏”是搜这个词的人最多的时候,我自己也遇到过不止一次。在鸿蒙上白屏,优先检查几个点:Metro服务是否正常启动、入口bundle是否加载成功、鸿蒙工程网络权限是否打开。
我常用的排查路径是:先在Metro终端看有没有Bundling成功的日志;如果Metro没输出,说明App根本没有发起bundle请求,去鸿蒙工程里查加载地址;如果Metro有请求,但页面还是白屏,大概率是JS执行报错,需要连接调试器看console。还有一个常被忽略的原因是签名问题,未签名的真机应用可能无法访问某些系统资源,导致异常退出。
5.2 鸿蒙依赖部署失败
有人会用到类似HarmonyBrew这一类的依赖安装工具,安装鸿蒙相关命令行工具或SDK包。我第一次用它部署依赖时也失败了,报错信息非常抽象,后来总结出几个高频原因。
第一,安装目录不能有中文或空格,否则脚本解析路径会出错。第二,Node版本太旧会导致请求依赖包时CLI工具崩溃。第三,如果失败信息里带有网络超时,先检查下载源的地址能否访问、是否设置了代理,但不要一上来就乱改系统代理。最后,清理缓存重新部署,很多问题其实是上次中断留下的脏文件。部署依赖这件事,关键要看日志,不要只盯着最终的失败文案,日志里才有真正的报错点。
5.3 Stack布局子组件怎么控制在底部上方100的位置居中
这个问题前面已经给了标准写法,但我要强调为什么不能想当然用position. 在ArkUI里,Stack的alignContent决定了所有子组件的默认对齐,用position做绝对定位虽然也能放到指定坐标,但坐标会依赖父容器尺寸,屏幕一变就错位。用margin配合对齐方式,则是相对布局,组件会自动适配不同屏幕。如果你的界面还要适配折叠屏、横竖屏切换,更应该用相对思路,而不是把坐标写死。
Stack({ alignContent: Alignment.BottomCenter }) { Text('底部上方100') .margin({ bottom: 100 }) }注意Alignment.Bottom和Alignment.BottomCenter在这里效果等同,但后者语义更明确,推荐直接用BottomCenter。如果子组件有多个,只想让其中一个这样定位,就给那个子组件设置alignSelf。
5.4 自定义组件不更新或事件收不到
有一种情况很隐蔽:RN侧props已经变了,鸿组件表面上看没变。原因是ArkUI的@Prop在组件初始化后对父组件的更新感知有讲究,需要父组件重新创建该组件或改变绑定键。最直接的排查方式是在鸿侧组件的aboutToAppear和属性更新方法里打日志,看它有没有收到新值。如果连日志都没打印,去查RN侧组件名和原生注册名是否完全一致。
事件收不到一般是回调命名不一致。RN侧onRingClick会对应到原生侧的原生事件映射,大小写和拼写必须严格匹配。很多团队为了图省事,JS侧写onClick,原生侧写onRingClick,两边都不报错,但事件就是断的。
5.5 常见问题速查表
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 启动白屏 | Metro未启动或地址错误 | 检查bundle加载地址和Metro日志 |
| 构建失败 | SDK版本不匹配 | 对照版本兼容表,统一SDK版本 |
| 真机无法加载bundle | 局域网IP不通 | 改宿主机IP,检查WiFi和防火墙 |
| 组件属性不更新 | @Prop用错 | 改用简单类型或@Link |
| 事件回调收不到 | 回调名不匹配 | 核对RN和原生侧事件名 |
| 依赖部署失败 | 缓存脏或目录非法 | 清理缓存,检查安装路径 |
6. 关于鸿组件开发,我的几点体会
这套东西做下来,我最大的体会是“别把鸿组件想得很玄,也别把它想得太简单”。说它不玄,是因为它本质上还是RN原生组件封装的老路子,Android和iOS上怎么做,鸿蒙上就怎么做;说它不简单,是因为鸿蒙的工具链和文档成熟度还没有Android那么高,很多报错需要你直接去读源码才能定位。
如果让我给刚上手的团队一个建议,我会说:第一个鸿组件不要做太复杂,先写一个带属性和点击事件的Text,完整跑通创建、更新、事件三条链路,再考虑业务里的重型组件。另外,RN和鸿蒙侧版本的升级一定要同步测试,不要只升RN版本,鸿蒙适配层也要跟着换。我现在所有相关依赖都固定版本号写进package.json,没有特殊情况不主动升级。最后一个小技巧:调试别只盯着Metro,DevEco Studio自带的HiLog筛选器非常有用,按关键字过滤鸿组件日志,很多问题一眼就能看出来。