
做跨端开发这几年我一直在关注鸿蒙OS的适配进展。最近把React Native简称RN工程真正跑进鸿蒙OS这件事完整落地了一遍这个过程比想象中复杂不少但也远没有网上说的那么玄乎。这篇文章我把自己从零开始摸索的路径整理出来包括鸿蒙OS的基础概念、RN工程要怎么接入鸿蒙应用、鸿蒙原生组件如何封装成RN可调用的模块以及我实际踩过的几个典型坑。很多内容在官方文档里找不到这么直接的表述完全是我这边实测下来后的经验总结。如果你正打算让RN团队接鸿蒙生态这篇文章应该能帮你少走大半个月弯路。先说清楚一件事标题里的“鸿组件”指的是鸿蒙原生组件。所以这篇文章的核心任务就是搞清楚“在RN项目里开发、集成鸿蒙原生组件”到底该怎么落地以及RN和鸿蒙两套技术体系之间是怎么打通关系的。1. 先理清思路RN和鸿蒙OS的本质差异做技术适配最怕的就是上来就动手连底层差异都没搞明白。RN和鸿蒙OS之间的适配核心难点不在于写几行代码而在于它们根本是两套完全不同的运行时体系。1.1 一个分布式操作系统的出场方式鸿蒙OS不是安卓的简单换壳而是一套面向全场景的分布式操作系统。什么是分布式用大白话讲就是手机、平板、手表、车机这些设备跑在同一套底层的分布式架构上设备之间可以互相调度硬件能力比如用手机控制车机屏幕、用手表调用手机摄像头用户感知上就像在用同一台设备。这个设计思路对RN开发者来说意味着什么最直接的一点就是鸿蒙OS的运行时和安卓运行时是完全独立的RN在安卓上依赖的那套Java/Kotlin桥接能力在鸿蒙上并不存在。你不能指望把安卓的RN工程直接拖进鸿蒙工程里就能跑起来必须做一次真正的技术对接。我在遇到这个问题之前也天真过想着RN不是跨端的吗多一个平台无非就是多写一个原生模块而已。实际调研后发现RN的架构层和应用层用的是JavaScript和C而鸿蒙的应用层开发用的是ArkTS和ArkUI底层运行时也不是V8或JSC那一套。这两者在最底层就没有天然交集所以“桥接”这个工作比安卓和iOS之间做适配还要重一些。1.2 “在RN中开发鸿组件”到底是什么方向这句话听上去有点绕我先把它拆明白。“在RN中开发鸿组件”并不等于你用ArkTS写一个页面然后嵌到RN里这种思路其实方向就反了。正确的方向是你的主工程是一个RN应用然后你通过RN的桥接机制把鸿蒙的原生组件封装起来让RN的JavaScript代码可以直接调用这些鸿蒙组件的能力。打个比方RN是一个大房子鸿蒙原生组件是房间里的一件家具。你要做的不是把整个房子搬进家具店里而是给家具装上一个标准接口让房子里的人可以按同样方式使用它。这个“标准接口”就是RN的桥接层。实际操作中有两种常见路径我帮你分清第一种RN工程作为主应用鸿蒙原生模块作为能力补充。适合已经有RN代码、想快速接入鸿蒙生态的团队。第二种鸿蒙原生应用作为主应用内部嵌入RN页面。适合以鸿蒙为主、需要复用RN业务代码的团队。这篇文章重点讨论第一种路径因为大多数RN团队的真实诉求是这个。2. 鸿蒙开发基础开工前必须掌握的概念在写第一行代码之前有几个鸿蒙开发的基础概念你必须搞明白。这些概念决定了你后续看文档的思路也是团队沟通时的共同语言。2.1 ArkTS、ArkUI和Ability鸿蒙开发绕不开三个关键字ArkTS、ArkUI、Ability。它们之间的关系可以用一个框架来理解ArkTS鸿蒙应用的开发语言在TypeScript语法基础上做了扩展是一门静态类型的语言。它负责写业务逻辑。ArkUI声明式UI框架类似于Flutter和RN的写法通过组件树来描述界面。它负责画界面。Ability鸿蒙应用的基本组成单元可以理解成安卓里的Activity和Service的混合体。它是应用被系统调度和运行的最小单位。你在鸿蒙侧开发一个组件通常是放在一个UIAbility或ExtensionAbility里。UIAbility负责提供一个页面窗口页面窗口里再放ArkUI组件树。RN要接入鸿蒙本质上是接入Ability的生命周期管理然后在页面窗口里渲染RN视图。我第一次接触的时候把Ability想成了安卓的Service结果生命周期对不上回调乱成一团。后来才意识到Ability更像是“应用的一个可调度入口”每个入口都有自己独立的一套生命周期。为了让你快速建立对应关系我做了个表格概念维度安卓鸿蒙OSReact Native开发语言Kotlin/JavaArkTSJavaScript/TypeScriptUI框架XML/ComposeArkUIRN组件应用单元Activity/ServiceAbility页面/模块组件通信Intent/Binder分布式软总线/Ability连接Bridge/TurboModule包管理APKHAPbundle从这个表能看出来RN和鸿蒙之间其实差了一层“运行时”。RN的JavaScript最终要渲染成原生视图在安卓上是渲染成Android View在鸿蒙上则需要渲染成ArkUI组件或者直接把鸿蒙的原生组件反向暴露给RN。2.2 Stage模型和组件生命周期鸿蒙OS的应用模型有两种一种是老旧的FA模型一种是新的Stage模型。做RN适配时官方推荐的是Stage模型也是当前主流模型。Stage模型的核心在于把应用分层UIAbility负责界面入口ExtensionAbility负责后台任务AbilityStage负责应用级的生命周期。这种分层让模块边界非常清晰组件化程度很高恰好和RN的模块化思路对得上。我记得最开始用的是FA模型结果文档里相关的示例越来越少社区也都在往Stage模型迁移最后推倒重来了一次。所以听我一句劝正式开始前务必确认你的鸿蒙工程是基于Stage模型的否则后面很多API根本对不上。在Stage模型下UIAbility的生命周期大概是这样几个阶段onCreateAbility创建适合做初始化。onWindowStageCreate窗口创建可以在这里加载ArkUI页面。onForeground/onBackground前后台切换类似安卓的onResume/onPause。onDestroy销毁做资源释放。RN的桥接层挂载时机通常选择在onWindowStageCreate之后。因为这个时候窗口已经准备好RN视图才能被贴合上去。另外还有一点容易忽略鸿蒙的组件是支持多设备形态的同一个Ability可能运行在手机、折叠屏、平板上。这意味着组件尺寸和布局不能写死要充分利用ArkUI的响应式布局能力。RN这边也一样Flex布局天然适配不同屏幕但当你嵌入一个鸿蒙原生的自定义组件时它内部如果是绝对定位写的就很容易在折叠屏上错位。这也是我后来在适配过程中真实遇到的问题。2.3 分布式能力与跨端组件的关系鸿蒙OS最鲜明的标签就是分布式能力。分布式软总线可以让设备之间像一台设备一样互相发现、连接和调用服务。这个能力对RN桥接有一个隐藏的影响如果你的鸿蒙组件需要调用另一个设备的功能桥接层就要考虑跨设备调用的场景。举个具体例子我想在RN里调用鸿蒙的分布式文件能力让手机端的RN应用读取平板端的一份文档。这时候RN的js侧发起请求桥接到鸿蒙侧鸿蒙侧再通过分布式软总线去调用远端设备。这个链路比普通桥接多了一层错误处理也要额外考虑比如设备离线、连接超时等。很多RN团队只想着“能跑通”就完事了忽略了分布式场景下的状态同步问题。我的建议是第一阶段先不做分布式能力先把单设备跑通把桥接层稳定下来。分布式能力作为第二阶段的扩展项单独设计接口。一口吃不成胖子桥接层本身就是一个大的工程点。3. 实操路径把鸿蒙组件接入RN工程接下来进入正题完整走一遍鸿蒙组件接入RN工程的流程。这个流程我拆成五个阶段每个阶段我都会标注关键动作和注意事项。3.1 环境准备与工程结构我需要准备的工具链包括Node.js环境版本建议用LTS版本。React Native工程需要确认版本。老架构和新架构的桥接方式有区别。HarmonyOS SDK通过DevEco Studio安装。DevEco Studio这个是鸿蒙开发的IDE。版本对应关系是我踩坑次数最多的地方。RN的新架构也就是从0.76开始全面启用的新架构默认使用TurboModule和Fabric这套是基于JSIJavaScript Interface的性能更好但鸿蒙侧的适配需要额外的工作量。老架构用的则是传统的Bridge组件适配起来相对成熟。我的建议是如果你用的是RN 0.73及以上版本直接走新架构路线因为鸿蒙侧已经在逐步跟进Fabric和TurboModule的支持。如果你用的是更老的门槛版本先确认你依赖的三方库是否还能兼容。工程结构上我采用的是双工程方案鸿蒙原生工程负责加载JS Bundle、提供原生组件和桥接模块。RN JS工程负责业务代码和UI层。然后在鸿蒙工程里通过构建脚本把JS Bundle打包成资源文件嵌入到鸿蒙应用的assets目录中。这样鸿蒙应用启动时直接加载本地Bundle不依赖远程调试服务器减小了联调复杂度。目录结构大概是这样的project/ ├── harmony-app/ # 鸿蒙原生工程DevEco Studio │ ├── AppScope/ │ ├── entry/ │ │ └── src/main/ │ │ ├── ets/ │ │ ├── resources/ │ │ └── module.json5 │ └── build-profile.json5 ├── rn-project/ # RN工程 │ ├── index.js │ ├── App.js │ └── package.json └── bundle/ └── index.bundle.js # 打包好的JS Bundle3.2 鸿蒙侧封装一个原生组件我要先做一个最简单的原生组件一个鸿蒙风格的按钮点击后能返回一个事件给RN侧。在鸿蒙侧组件的实现方式通常是一个自定义struct。它继承自基础的组件包装类内部用ArkUI声明自己的UI结构和交互逻辑。我们写一个非常简单的HarmonyButton组件Component export struct HarmonyButton { private label: string Harmony Button private onClick: () void () {} build() { Button(this.label) .width(100%) .height(44) .backgroundColor(#00A6E0) .onClick(() { this.onClick() }) } }这个组件本身很简单但它已经具备了一个原生组件的雏形有UI、有事件、有外部可以设置的属性。把它交给RN去调用就需要暴露接口。关键是这个组件必须挂到一个可以承载原生视图的容器里才能被RN的渲染层触达。在实践中我通常会在一个UIAbility的页面里创建一个native容器然后把HarmonyButton放进去。Entry Component struct Index { build() { Stack() { HarmonyButton({ label: 来自RN的原生按钮 }) } .width(100%) .height(100%) } }这只是鸿蒙侧的最小单元。下一步是把这个组件所承载的能力通过一个桥接类暴露给RN。3.3 桥接层暴露给RN调用RN和鸿蒙之间的通信本质上需要一个中转层。在新架构下这个中转层就是TurboModule。TurboModule的设计目标是让JavaScript在调用原生方法时更高效它直接通过JSI绑定到原生函数比老架构的异步Bridge快得多。在鸿蒙侧你要创建一个TurboModule的实现类类里的方法会被JavaScript直接调用。定义方式大致是这样的思路export class NativeHarmonyModule { showToast(text: string): void { // 调用鸿蒙系统的toast能力 } getDeviceInfo(): Recordstring, string { // 返回鸿蒙设备信息 } }然后在RN的JavaScript侧通过TurboModuleRegistry.get接口来获取这个模块的引用import { TurboModuleRegistry } from react-native; const NativeHarmony TurboModuleRegistry.get(NativeHarmonyModule); NativeHarmony.showToast(Hello from Harmony); NativeHarmony.getDeviceInfo().then((info) { console.log(info); });你可能会问老架构怎么办老架构用的是global.bridge或者RCTBridgeModule的方式鸿蒙侧实现对应的方法后通过事件机制发送给RN。新架构和旧架构的核心差别在于调用方式老架构走的是消息异步传递新架构直接走JSI同步调用性能上不是一个量级。如果你们的鸿蒙适配工作启动得比较早用的还是老RN版本建议尽早升级。3.4 RN侧调用与渲染原生组件的两种方式在RN侧接入鸿蒙原生组件有两种主流方式一种是通过HostComponent方式注册一个原生组件另一种是通过TurboModule把原生能力打包成API。两者各有适用场景如果是要渲染一个原生UI视图比如鸿蒙的日期选择器、地图、播放器用HostComponent方式RN直接把原生视图嵌入到自己的布局体系里。如果只是调用一个原生能力比如读取设备信息、调用分布式能力、发起文件管理操作用TurboModule方式直接通过API调用。我在实际项目里两种都用了。HostComponent方式的注册思路在原生侧你需要提供这个组件给RN的Fabric架构让RN知道“我有一个叫RTHarmonyButton的组件”。RN侧拿到这个组件名后直接当作普通React组件使用import React from react; import { requireNativeComponent } from react-native; const RTHarmonyButton requireNativeComponent(RTHarmonyButton); export function HarmonyButton(props) { return RTHarmonyButton label{props.label} onClick{props.onClick} /; }这里有一个常见的坑requireNativeComponent在鸿蒙侧要能正确匹配组件名如果你的鸿蒙原生组件注册名和这里不一致运行时大概率会出现“native component not found”的报错。事件回调方面RN侧传的onClick事件要和原生侧发送的事件名严格对应因为RN渲染层的触摸事件很多场景下并不是直接从原生组件冒泡过来的。某些鸿蒙SDK版本里组件点击事件需要手动绑定发送如果不做绑定RN侧永远收不到回调。我在一个版本上就遇到了这个问题鸿蒙原生按钮点击了RN侧的onClick死活不触发。最后定位半天发现是事件发送通道没有初始化完成需要等UIAbility窗口创建完成后才能注册。所以事件绑定的时机要放在组件挂载完成之后不是组件构造时就绑定。3.5 构建、联调与资源路径处理当桥接层和组件都写好之后接下来就是把JS Bundle打包进鸿蒙工程。打包命令很简单在RN工程根目录执行npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output bundle/index.bundle.js --assets-dest bundle/assets这里有个细节因为RN和鸿蒙之间没有正式的官方打包插件我在实践里是把鸿蒙当作一个“自定义平台”来打包的所以bundle的时候platform参数用的还是android但产出的是纯JS Bundle鸿蒙侧用ArkTS的resource管理能力来加载。然后在鸿蒙侧写一个加载器从资源文件读取Bundleimport { router } from kit.ArkUI; // 加载本地Bundle文件 const bundleSource await context.resourceManager.getRawFileContent(index.bundle.js); const jsBundle String.fromCharCode(...new Uint8Array(bundleSource));拿到Bundle字符串后再交给RN的运行时去初始化。这一步如果出现白屏大概率是Bundle的路径写错了或者资源文件没有真正打进hap包里。联调阶段我一般同时开两个终端终端一运行React Native的Metro服务方便开发时实时刷新。终端二运行鸿蒙工程的编译和安装命令定期打包看效果。但在发布模式下Metro服务不需要直接用本地打包出的Bundle文件即可。你需要特别注意打包结果excludes掉不必要的开发代码否则包体体积会非常夸张。下面是一张适配建议表按RN版本粗略划分RN版本架构鸿蒙适配方式我的建议0.70及以下老架构传统Bridge尽量避免除非必须0.71-0.75新旧共存Bridge初版TurboModule可以用于验证链路0.76及以上新架构为主TurboModuleFabric主力选择推荐4. 实战避坑常见问题与排查技巧技术文章写得再好都不如把真实踩过的坑摆出来。这一部分我整理了自己在适配过程中遇到的高频问题包括一张速查表和三个印象最深的排障经历。4.1 常见问题速查表问题现象可能原因排查思路编译报错找不到so文件RN依赖的三方原生库没有做鸿蒙端abi适配检查库的abi目录是否包含鸿蒙架构启动后白屏JS Bundle加载失败检查Bundle路径、Resource文件是否打进了hap包RN能跑但原生组件不显示原生组件注册名不匹配核对requireNativeComponent的名称点击事件收不到回调事件通道注册时机太早延迟到UIAbility窗口创建完成后注册字体大小和间距异常鸿蒙vp单位与RN dp单位换算不一致做单位换算或强制使用flex布局自适应报错“Cannot find module”Node模块路径解析不一致重新entire install清理缓存某些三方库在鸿蒙端崩溃库内部用了安卓API替换为鸿蒙兼容实现或用条件编译隔离4.2 三个印象最深的坑第一个坑事件命名不一致导致回调失效。我最初把鸿蒙原生组件里的回调命名为“onClicked”RN侧写的是“onClick”。结果编译不报错、运行不报错、界面也正常显示但就是点按钮没反应。排查了两天才发现RN的组件事件监听器要求事件名完全一致而且事件需要通过系统的event emitter发送不能直接在自定义组件里随便定义一个方法就叫回调。最后统一改成“onClick”并且把事件发送逻辑放到didMount里才解决。第二个坑Bundle加载路径写死。调试阶段我用的是绝对路径加载Bundle文件本地跑得好好的一打包到真机上就白屏。后来发现鸿蒙的资源访问和安卓有差别需要开发者用ResourceManager获取原始文件内容而不是直接拼一个路径去读文件。因为不同设备上的应用沙箱路径可能不一样写死路径的代码迁移到另一台设备上很容易整套失效。改用ResourceManager之后问题就不再出现了。第三个坑线程调度混乱。TurboModule的默认执行线程是从JS侧直接调过来的你无法保证它运行在UI线程上。我一开始在TurboModule里直接操作了UI组件结果运行到某个页面的时候频繁崩溃日志指向的是线程持有锁的问题。后来才明白涉及UI更新的操作必须切换到UI线程执行而耗时操作不能阻塞JS线程。解决办法是构建一个线程调度器在不同场景下把调用分发到正确的执行上下文。4.3 排障思路小结遇到问题的时候我是这样一步步排查的先看JS侧有没有捕获到异常RN的错误日志一般会提示是在模块加载阶段、渲染阶段还是方法调用阶段。再看鸿蒙侧的系统日志用hilog抓取关键字看有没有C层或ArkTS层的异常输出。最后用二分法定位注释掉部分代码比如只保留桥接能力、不渲染原生组件确认问题是出在通信链路还是UI渲染层。这一套流程走下来绝大多数问题都能定位到具体原因。我在这个项目上最重要的一条经验就是不要迷信单一日志一定要把JS侧、桥接侧、原生侧三端日志打通起来看才能还原完整的调用链路。5. 个人实操心得与一点务实建议做完整轮适配之后我最大的感受是RN接鸿蒙并不是一个“伪命题”但它确实比接一个普通的新平台要费力。因为它不是简单的换个SDK而是要在运行时、渲染层、模块通信三个层面都做对接。我个人在实际操作中的体会是先把最小闭环跑通比什么都重要。不要一上来就想着把所有原生组件都封装一遍或者一步到位接分布式能力。我建议所有团队都按照这个顺序推进第一步先做出一个最简单的“原生组件显示事件回调”的Demo验证整条链路是通的。第二步挑一个业务上确实要用到的原生能力比如扫码、推送、安全存储把它封装成TurboModule。第三步在性能和稳定性都满足要求后再考虑批量迁移UI组件和业务模块。还有一点经验也想分享下一定要锁定版本。RN版本、鸿蒙SDK版本、打包工具版本这三个版本只要有一个不一致就可能出现各种莫名其妙的兼容问题。我在项目里是直接把所有版本信息写进了一个配置文件里换机器、换人时先看配置而不是靠口头传递。最后给一个比较务实的小技巧如果你的团队里暂时没有懂ArkTS的开发者一定不要硬着头皮全靠RN知识去猜。桥接层的代码独立于业务但它决定了整个方案的成败。哪怕只花一周时间让团队里一个人先系统过一遍ArkTS的基础语法和ArkUI的组件模型都会让后续的适配效率提升很多倍。这一版适配工作做完之后概念验证已经稳定跑起来了。下一步我在打算把分布式文件传输能力接入到同一个桥接层里等实际跑通后再单独写一篇分享。如果你也在做类似的事情欢迎对照着这篇文章的思路先跑通最小Demo过程中的坑我们慢慢聊。