如果你的团队手里有一套运行了很久的 React Native 代码库,业务方某天突然说“新设备要支持鸿蒙”,你脑海里多半会闪过三连问:RN 真的能跑在鸿蒙上吗?鸿蒙的系统能力怎么暴露给 JS?大家嘴里说的“鸿组件”到底是个什么东西?这三个问题其实指向同一件事:在 RN 项目里做鸿蒙化。我说的“鸿组件”,你可以理解成鸿蒙原生组件,既包括鸿蒙侧的原生 UI 组件,也包括用来给 JS 调用的原生能力模块。这篇文章就把这件事完整讲清楚,顺便把 RN 开发者在动手之前必须补的鸿蒙基础、桥接思路、实操路径和常见坑一次性说透。
我自己在做跨端适配的时候踩过不少坑,所以这篇文章不打算写得像官方文档那样四平八稳,而是按我实际推进项目的顺序来写:先想清楚架构,再补基础,然后动手写第一行桥接代码,最后是排障经验和建议。
1. 场景拆解与整体设计思路
很多人以为“RN 集成鸿蒙”就是装个插件、改几行配置就能跑起来,等真接手项目才发现,事情远没那么简单。RN 本身是一套 JavaScript 运行时加原生渲染桥接框架,鸿蒙则是一套从内核到应用框架都自成体系的分布式操作系统,两者之间没有现成的官方运行时对接。所以“在 RN 项目中集成鸿蒙应用”这件事,本质上做的不是“移植”,而是“搭桥”。
1.1 “鸿组件”到底指什么
先把这个词拆清楚。在 RN 社区里,我们聊鸿组件一般分两类:
第一类是鸿蒙原生 UI 组件,比如一个地图组件、一个富文本组件、一个系统设置面板。这类组件要用 ArkUI 编写,然后通过 RN 的原生组件机制暴露给 JS 侧,让 React 代码能像使用普通 RN 组件一样使用它。这类组件解决的是“RN 自绘组件画不出来、或者不想用 RN 重写”的场景。
第二类是鸿蒙原生能力模块,比如分布式数据读写、设备之间互联互通、系统能力查询。这类模块不涉及界面,只需要把鸿蒙的原生方法通过 TurboModule 或 NativeModule 的方式暴露给 JS。RN 业务代码调用一个 Promise,底层自动走鸿蒙的 API,这类能力才真正发挥鸿蒙系统的独特性,也是“在 RN 里开发鸿组件”最有价值的部分。
两种组件有一个共同点:它们都需要你同时懂 RN 的跨端通信机制和鸿蒙的原生开发方式。RN 侧决定“暴露什么接口”,鸿蒙侧决定“怎么调系统能力”,中间的桥接代码需要你自己写。
1.2 三条技术路线的取舍
我在推进鸿蒙化的时候,先盘过市面上可行的路线,主要就三条:
第一,等官方支持。RN 官方目前没有把鸿蒙作为一级平台来支持,这条路短期内走不通,只能保持关注,不能作为项目计划的一部分。
第二,用社区适配层跑完整 RN 容器。社区里确实有把 RN 运行时移植到鸿蒙的适配方案,目标是让整套 RN 架构直接跑在鸿蒙设备上。这种方案长期看是正路,但目前覆盖的系统 API 和第三方原生模块还有限,如果你的业务重度依赖地理位置、扫码、推送等能力,接进来可能会发现很多原生模块没有对应实现,需要自己补。
第三,自研原生壳加桥接层,做混合栈集成。也就是 RN 业务代码里可以跳转到鸿蒙原生页面,同时 RN 内通过桥接调用鸿蒙的系统能力。这条路工作量最可控,也最容易在一个阶段里看到明确结果。我最终也是选择的这条路。
三条路线不冲突,可以分阶段走:先把桥接层跑通,保证核心业务链路可用;后续社区适配层成熟了,再逐步迁移。
1.3 边界划分:哪些逻辑放 JS,哪些放鸿蒙侧
设计跨端桥接时,最容易犯的错是把业务逻辑写进原生模块里。我的原则很简单:鸿蒙侧只做“系统能力的最小封装”,比如读取设备状态、调用分布式能力、驱动原生 UI 组件;所有页面路由、状态管理、数据格式化、网络请求策略,一律留在 RN 侧。
一个典型的边界划分表:
| 模块 | 鸿蒙侧职责 | RN 侧职责 |
|---|---|---|
| 分布式数据模块 | 增删改查、订阅变更 | 数据模型、缓存策略、业务校验 |
| 原生地图组件 | 渲染地图、处理手势 | 数据源、事件转发、样式参数 |
| 系统能力模块 | 查询设备信息、权限状态 | 界面展示、权限引导流程 |
| 原生页面容器 | 加载鸿蒙 page、生命周期通信 | 路由调度、业务参数传递 |
边界划清楚之后,桥接接口设计起来就非常快,后续调 JS 代码也不用担心“某个逻辑被锁死在原生层,改不了”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手之前:RN 开发者必须补的鸿蒙基础
直接在 RN 项目里写鸿蒙原生代码是不现实的。你不需要成为鸿蒙专家,但下面几个基础点必须过一遍,不然看代码都会看不明白。
2.1 语言和 UI:ArkTS 与 ArkUI 的定位
鸿蒙主推的开发语言是 ArkTS,它是在 TypeScript 基础上扩展出来的,语法上对前端开发者非常友好。装饰器、UI 描述、状态管理这些概念都类似,RN 开发者上手成本比从零学 Java 和 Kotlin 低很多。
但要注意:ArkTS 虽然长得像 TS,它禁止了一些动态特性,比如用 any 类型做隐蔽类型跳转就可能直接编译报错。我刚开始切换的时候,就吃过“代码在 TS 里能过、迁到 ArkTS 里报错”的亏。适应方法只有一个:先写几个最小的原生 Page,跑通再往上堆功能。
ArkUI 是鸿蒙的声明式 UI 框架,和 React 的声明式思想接近,你写惯了 JSX,再看它的 @Component、@Entry、@State 装饰器,接受度会很高。它跟 RN 最大的区别是:RN 用 JS 描述 UI,ArkUI 用 ArkTS 描述 UI,两者都要经历编译期和运行期的框架处理。
2.2 应用框架:Stage 模型和 Ability 的关系
鸿蒙应用开发现在主推 Stage 模型。Stage 模型里每个应用可以有多个 UIAbility,一个 UIAbility 就是一个可交互的页面单元,类似 Android 的 Activity。RN 侧如果要跳转到鸿蒙原生页面,本质上就是从一个 UIAbility 启动另一个 UIAbility,或者启动同一个 UIAbility 的不同页面。
我给 RN 开发者的建议是:先把 UIAbility 生命周期和页面路由搞清楚,不需要深入全套应用框架。后续写桥接模块时,你会频繁和这些生命周期打交道,比如应用退到后台再回前台、页面被系统回收,这些场景都影响 JS 和原生模块之间的状态同步。
2.3 分布式能力:真正值得桥接给 RN 的东西
RN 项目最大的局限在于,它把所有业务都限制在单台设备里。鸿蒙的分布式能力让应用可以跨设备协作,比如读取另一台设备的分布式数据库、调用另一台设备上开放的接口。这部分能力在 iOS 和安卓上都没有现成对标物,所以在设计桥接模块时,优先级最高。
我建议优先桥接的设备分布式数据管理接口和分布式任务调度接口。RN 侧只要封装一层“获取设备列表”“打开分布式数据库”“订阅数据变更”就能支撑不少创新业务,比如双端协同编辑、设备间文件互传。具体接口调通之前,你得先准备两到三台测试设备,而且必须登录同一个账号体系,否则设备列表永远是空的。这一点后面会专门讲。
3. 从零实操:开发一个鸿蒙原生模块并接入 RN
理论上再怎么讲,不如跑通一个最小 demo。下面这个流程我从工程创建到 JS 调用全部走了一遍,你按顺序操作,基本不会卡壳。
3.1 环境和工程创建
第一步是装好开发工具。鸿蒙原生开发使用的是配套的 IDE 工具,创建工程时记得选择支持 Stage 模型的模板。SDK 版本建议直接选当前生态主推的版本,不要选太老的,否则有些新接口的分布式能力封装不存在。
创建工程时选空模板即可,不需要选带复杂示例的模板,示例代码反而干扰你理解最小结构。工程创建完,先跑一次空包,确认模拟器或真机环境能正常拉起应用。这一步很多人会跳过,结果后面桥接报错时,根本分不清是环境问题还是代码问题。
配置鸿蒙工程的构建文件时,有几个参数需要认真核对:
| 配置项 | 建议值/说明 |
|---|---|
| 应用包名 | 和 RN 工程包名保持一致,避免签名冲突 |
| 目标 SDK 版本 | 用当前生态主推版本,别用 beta 版 |
| 权限声明 | 按需申请,不用的权限不要申请 |
3.2 在鸿蒙侧创建原生模块
接下来是重头戏。我要演示一个“查询分布式设备能力”的原生模块:JS 侧传一个设备 ID 进来,鸿蒙侧返回该设备是否支持某种能力。
代码如下,实际导入路径以你接入的适配层为准,但整体结构是通用的:
typescript复制// DeviceCapabilityModule.ets
import { TurboModule } from 'native/RNOH';
export class DeviceCapabilityModule extends TurboModule {
checkDeviceCapability(deviceId: string, capability: string): Promise<boolean> {
return new Promise((resolve) => {
try {
// 调用鸿蒙系统能力检查接口,这里用通用占位
const result = checkDistributedCapability(deviceId, capability);
resolve(result);
} catch (err) {
resolve(false);
}
});
}
}
这段代码逻辑很简单:模块类继承自 TurboModule,暴露一个方法,内部调用鸿蒙系统接口,然后返回 Promise。注意我用了 try/catch 包住调用,因为设备不在线、能力不支持等情况下,系统接口可能直接抛异常;如果异常不处理,JS 侧拿到的就是 unhandled promise rejection,排查起来更麻烦。
写完模块后,还需要在鸿蒙侧的模块注册表里注册这个类,否则 JS 侧永远拿不到这个引用。注册方式不同适配层略有不同,但核心就是“把模块实例加入初始化列表”。
3.3 在 RN 侧封装 JS 接口
原生模块注册完成之后,JS 侧调用是最简单的部分:
javascript复制import { NativeModules } from 'react-native';
const { DeviceCapabilityModule } = NativeModules;
export async function checkDeviceCapability(deviceId, capability) {
try {
const supported = await DeviceCapabilityModule.checkDeviceCapability(
deviceId,
capability
);
return supported;
} catch (e) {
return false;
}
}
这里建议包一层自己的导出函数,不要直接让业务代码抓 NativeModules。原因一个是便于统一处理异常,另一个是将来如果底层从 NativeModule 切到 TurboModule,你只需要改这个封装文件,业务方无感知。
如果你要封装的是原生 UI 组件,JS 侧写法稍有不同:
jsx复制import { requireNativeComponent } from 'react-native';
const HarmonyMapView = requireNativeComponent('HarmonyMapView');
export default function MapContainer(props) {
return <HarmonyMapView {...props} />;
}
requireNativeComponent 会把这个字符串当成原生视图对应的组件名,RN 在原生侧寻找同名注册视图。组件名两边拼错一个字母就直接白屏,所以保持字符串一致是最重要的事。
3.4 构建运行与联调
两边代码写好后,先把鸿蒙原生工程单独构建一遍,再跑 RN 打包产物。这里有一个实数:桥接层调试初期,RN 侧代码和原生侧代码不是同时构建的,如果 RN bundle 启动后 JS 调原生接口报错,你需要同时看两个地方的日志。鸿蒙侧的 IDE 调试输出里能看到原生模块是否被调用,RN 侧的 Metro 或打包日志里能看到 JS 报了什么错误。
联调阶段我强烈建议你用真机,不要只用模拟器。分布式设备能力在模拟器环境经常表现不完整,设备间互相发现和连接在真机上才能测出真实结果。几台设备要保存在同一网络环境和统一账号体系下,这是分布式能力调试的基本条件。
3.5 权限声明与数据校验
模块如果涉及系统敏感能力,比如读取设备信息或者访问分布式数据,必须在鸿蒙工程的配置文件里声明对应权限。这个文件是 module.json5,权限声明大概长这样:
json5复制{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.DISTRIBUTED_DATASYNC"
}
]
}
}
权限漏声明时,很多系统接口不会直接抛错误,而是静默返回空结果或 false。我遇到过设备列表查出来一直是空的,排查了大半天,最后发现是权限没配。所以调分布式相关接口之前,先检查配置文件里的权限列表,能省去大量无效排查时间。
4. 常见问题与排查技巧实录
这一节全部来自实际操作。我把桥接开发中遇到的最典型问题按优先级列出来,你可以直接照着对照。
4.1 构建失败:SDK 版本与构建工具不匹配
最普遍的报错是编译失败,原因是 SDK 版本和构建工具版本对不上。鸿蒙生态更新很快,一旦 SDK 升级,构建工具版本不跟着升,各种奇怪报错都会冒出来,比如接口找不到、资源文件编译不通过、符号找不到。
我的排查路径是:先看错误消息指向的是不是 SDK 相关目录,如果是,优先检查构建配置里 targetSdkVersion 和 compileSdkVersion 是否一致。两边不一致,要么指定同一个匹配的版本,要么整体升级构建工具。这条排查顺序能解决 80% 的构建失败。
4.2 JS 调用原生方法报 undefined
JS 侧报某个原生方法不是函数,通常不是方法写错了,而是原生模块根本没注册成功。排查思路:
| 检查点 | 具体操作 |
|---|---|
| 模块类名 | 确认 JS 侧访问的名称和原生侧注册名称完全一致 |
| 注册列表 | 确认模块被加入了初始化列表 |
| 构建缓存 | 清理构建缓存,重新构建原生工程 |
| 原生日志 | 查看鸿蒙侧日志,确认模块初始化是否执行 |
我在第一次跑通前,基本把这几项逐个查了一遍。最快的办法是先写一个 hello 函数,只返回字符串,等 JK 侧能调到,再往上加真实逻辑。
4.3 分布式能力调用无结果
分布式能力调用失败,现象非常迷惑:接口不报错,但返回结果是空的。常见的坑就那几个:设备没有登录同一账号、设备间网络隔离、目标设备不在线、权限没开。
我建议先做一个连通性检查:在鸿蒙原生页面里先直接调用一次系统接口,看能否正常返回设备列表。原生侧能通,说明问题出在桥接;原生侧也查不到,那就是权限或网络环境问题。这种“先缩小范围”的思路,永远比一头扎进桥接代码里找原因靠谱。
4.4 性能排查:跨桥通信不能滥用
桥接通信有成本,每次 JS 调用原生方法都有序列化和反序列化开销,频繁调用会让 UI 出现明显卡顿。我见过有人在滚动列表里调分布式数据查询接口,每个 item 渲染都触发一次原生调用,结果列表帧率掉到个位数。
解决方案有两个:一是批量传递,一次传一组数据,不要在循环里逐个调用;二是把高频数据预拉到 JS 侧缓存,只在必要时刷新。跨端通信里,最贵的操作就是大量小包高频并发,设计接口时要下意识往“一次调用,返回完整数据”的方向走。
5. 几点实操心得
文章写到这,核心内容基本都讲完了。最后分享几个我在实操中形成的习惯,算不上系统方法,但对少踩坑很有帮助。
第一,先跑通最小闭环再铺开业务。我会先做一个 hello 模块,从鸿蒙原生层传到 JS 再返回数据,走通整个链路,然后才开始接分布式能力、权限、复杂 UI。任何一步出了问题,都知道是自己最后改的代码造成的,排查范围非常小。
第二,桥接接口尽量一次设计到位。接口一旦发布给多个业务方使用,后续改参数名、改返回结构,牵涉面就非常广。在设计阶段多花点时间,把所有可能用到的参数和返回形态想清楚,比上线后做兼容省力得多。
第三,日志比经验更可靠。鸿蒙侧接口的报错信息和 RN 侧日志混在一起,一开始会觉得乱,但静下心把两边的日志一起看,绝大多数问题都有清晰的因果链。不要凭感觉猜,每一步都用日志确认。
第四,如果团队从 RN 转做鸿蒙化,别抱着“在 RN 里兼容鸿蒙”的心态,而要想“接一根原生触点,把鸿蒙的独特能力导流到 RN 业务里”。这两种心态决定你做出来的架构是临时补丁还是一个可持续演进的混合栈。
这个方向的探索空间还很大,后续你可以继续做原生 UI 组件的批量下沉、分布式场景的业务抽象,甚至逐步替换掉社区适配层里还不完善的能力。先把最小闭环跑起来,后面的路会越走越顺。
