我第一次在RK3568开发板上把RNOH应用跑起来时,最想做的功能不是花哨的动画,而是网络切换提示——Wi-Fi断了要弹一条提示,从Wi-Fi切到蜂窝数据要提醒用户注意流量。因为开发板不像手机那么稳定,网口、Wi-Fi、热点之间的切换很频繁,应用如果对网络变化没感知,用户会觉得这是个半成品。结果翻遍社区包,发现 @react-native-community/netinfo 在OpenHarmony上根本没法直接用,它底层依赖Android的ConnectivityManager和iOS的NWPathMonitor,OH这边没有对应实现。从那一刻起我就知道,这个功能只能自己写原生桥接。
这篇文章就围绕这条链路展开:OpenHarmony的 @ohos.net.connection 系统能力怎么用,ArkTS侧如何暴露一个NetworkInfo桥接模块给React Native,JS侧怎么封装成hook,以及最后接上UI提示组件。适合在OpenHarmony上用RN做应用、又需要感知网络状态的开发者参考。坑我已经踩过一轮,你照着走能省不少时间。
1. 在OpenHarmony上拿网络状态,为什么不能直接抄NetInfo的作业
1.1 社区库失效的根源:Android/iOS系统API依赖
RN社区最常用的网络状态库是 @react-native-community/netinfo,这个库在Android端通过 ConnectivityManager 拿网络状态,在iOS端通过 NWPathMonitor 和 CoreTelephony 拿网络状态。它能在上层提供统一的 addEventListener 接口,核心逻辑却是纯平台相关的。
RNOH(React Native for OpenHarmony)是社区移植的RN版本,JS侧API层兼容性做得不错,但系统能力调用链完全是另一套。OH没有ConnectivityManager,也没有NWPathMonitor,所以NetInfo社区原包在OH上编译不过,或者运行时会直接报"方法不存在"。
有人可能想用RNOH官方移植的 @react-native-oh-tpl/react-native-netinfo,这个包确实存在,但它依赖的底层网络API在不同OH系统版本上表现不一致,有时能拿到事件,有时拿到的是空对象。我在项目里试过一次,最后还是决定自己封装。原因很简单:网络状态是应用的基础感知能力,我不希望它在某个系统版本上悄悄失效。
1.2 两条技术路线:轮询状态与事件订阅的取舍
既然要自己写,先想清楚技术路线。在OpenHarmony上拿网络状态有两条路:
路线一:轮询查询
通过 connection.getDefaultNet() 拿到默认网络句柄,再通过 connection.getNetCapabilities(netHandle) 解析网络类型,用 setInterval 定时查。
- 优点:实现简单,逻辑直白。
- 缺点:状态变化有延迟,最高可能慢1秒;每次查询都要走一次系统进程通信,频繁调用耗电;会漏掉短时间的网络抖动。
- 这个方案适合对实时性要求不高的场景,比如只做"当前网络类型"展示。
路线二:事件订阅
通过 connection.on('netAvailable')、connection.on('netLost') 等系统回调,在网络状态变化的瞬间收到通知。
- 优点:实时性高,延迟在毫秒级;没有额外轮询开销;不会漏事件。
- 缺点:需要正确管理回调注册和注销,否则模块销毁后事件还在,容易内存泄漏。
我的选择是事件订阅。网络切换提示这个功能,核心就是要"在切换发生的瞬间"给用户反馈,轮询的延迟体验很差。而且开发板场景下网络事件本身不频繁,事件订阅的功耗优势也不可忽略。
1.3 整体架构:一条清晰的桥接链路
网络状态感知需要打通三层:
text复制RN侧JS组件
|
| 调用NativeModules / TurboModule
v
ArkTS原生模块 (NetworkInfoModule)
|
| 注册系统回调
v
@ohos.net.connection
在UI层我用RN自带的 View、Text、Animated 做提示横幅,不用原生Toast。原因有两个:一是RN侧渲染的UI可以自定义样式,比如加图标、背景色、圆角,不受系统Toast限制;二是不需要为此引入额外的原生依赖。整个链路跑通后,你可以在任何RN页面上直接挂载这个提示组件,效果和Android上常见的网络提示条一样。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境踩点:RK3568开发板、hdc命令与RNOH工程的三件套准备
2.1 先通过hdc确认设备基础信息
拿到开发板第一件事,不是写代码,而是确认设备状态。RK3568和RK3588是OpenHarmony开发中最常见的两款芯片方案,市面上大量开发板用的是这两颗SoC。不同板子烧录的系统版本差异很大,有的跑API 9,有的跑API 10或更新版本,而 connection 模块的接口细节在不同版本上略有差别。
连接开发板后,先用 hdc list targets 看看设备是否在线:
bash复制hdc list targets
如果能看到设备序列号,说明hdc通道正常。接着查看系统版本和产品型号:
bash复制hdc shell param get const.product.name
hdc shell param get const.ohos.version
hdc shell param get const.ohos.apiversion
这三条命令分别返回产品名称、系统版本号、API版本号,对后续适配非常有价值。我在项目里见过一个很常见的错误:开发板烧的其实是API 9的系统,但RNOH依赖的某个API需要API 10,结果跑起来各种异常。先确认版本,能少踩一半的坑。
要注意 serial 和 devudid 的区别。hdc list targets 显示的只是串口/USB调试用的序列号,而 devudid 是设备唯一标识,在部分签名和鉴权场景下会用到。开发调试时经常用到的是serial,别搞混。
2.2 RNOH工程结构:原生代码应该放在哪个目录
RNOH工程从模板创建后,目录结构大致是这样:
text复制project-root/
├── entry/
│ ├── src/
│ │ └── main/
│ │ ├── ets/
│ │ │ ├── entryability/
│ │ │ ├── pages/
│ │ │ └── network/ // 自定义原生模块放这里
│ │ ├── resources/
│ │ └── module.json5
├── node_modules/
├── oh_modules/
├── build-profile.json5
└── hvigorfile.ts
entry/src/main/ets 目录是OpenHarmony应用原生代码的根目录。新建一个 network 子目录,专门放网络相关的原生模块,和页面代码分开,结构清晰。RNOH的原生模块在编译时会自动扫描注册,但不同版本扫描机制不一样——有些需要在 PackageProvider 里手动注册,有些用了注解自动注册。建议建立工程后先跑一个官方原生模块Demo,确认你的版本走的是哪条注册路径,再开始写业务代码。
2.3 工程里的权限与依赖配置
访问网络状态信息,需要在 module.json5 里声明权限,否则运行时可能报错或者拿不到完整信息。我建议在 requestPermissions 里加上:
json复制{
"name": "ohos.permission.GET_NETWORK_INFO"
}
这个权限是查询网络信息的基础权限,缺了它,部分设备上 getNetCapabilities 可能返回空结果或异常。虽然文档里描述得模棱两可,但实际测试下来,声明这个权限能避免大部分奇怪问题。
依赖方面,RNOH工程需要用 @ohos/react-native-harmony 作为RN的OH适配层,同时React Native本身版本必须匹配。当前RNOH社区的版本发布通常紧跟RN的release,建议直接使用RNOH文档推荐的RN版本组合,不要自己随意升级RN大版本,很容易出现编译期符号找不到的问题。
3. 从系统回调到RN组件:NetworkInfo桥接模块的完整代码链
3.1 原生侧:基于connection模块的监听封装
原生模块的核心逻辑在ArkTS侧。我创建了一个 NetworkInfoModule.ets,继承RNOH的TurboModule基类,注册 initNetworkListener 方法,把系统网络事件转发给JS侧:
typescript复制// entry/src/main/ets/network/NetworkInfoModule.ets
import { connection } from '@kit.NetworkKit';
import { TurboModule } from '@ohos/react-native-ohos/ts';
import { BusinessError } from '@kit.BasicServicesKit';
export class NetworkInfoModule extends TurboModule {
private callback: ((event: string) => void) | null = null;
constructor(ctx: any) {
super(ctx);
}
initNetworkListener(callback: (event: string) => void): void {
if (this.callback) {
// 防止重复注册
return;
}
this.callback = callback;
connection.on('netAvailable', (netHandle: connection.NetHandle) => {
this.handleNetAvailable(netHandle);
});
connection.on('netLost', (netHandle: connection.NetHandle) => {
this.sendEvent('lost', {});
});
}
private handleNetAvailable(netHandle: connection.NetHandle): void {
connection.getNetCapabilities(netHandle).then((caps: connection.NetCapabilities) => {
const networkType = this.resolveNetworkType(caps.bearerTypes);
const isMetered = !caps.capabilities.includes(
connection.NetCap.NET_CAPABILITY_NOT_METERED
);
this.sendEvent('available', {
networkType,
isMetered,
});
}).catch((err: BusinessError) => {
console.error(`getNetCapabilities failed: ${err.message}`);
});
}
private resolveNetworkType(bearerTypes: connection.BearerType[]): string {
if (bearerTypes.includes(connection.BearerType.BEARER_WIFI)) {
return 'wifi';
}
if (bearerTypes.includes(connection.BearerType.BEARER_CELLULAR)) {
return 'cellular';
}
if (bearerTypes.includes(connection.BearerType.BEARER_ETHERNET)) {
return 'ethernet';
}
return 'unknown';
}
private sendEvent(type: string, data: object): void {
if (this.callback) {
const payload = JSON.stringify({ type, ...data });
this.callback(payload);
}
}
dispose(): void {
// 组件销毁时注销回调,避免内存泄漏
this.callback = null;
}
}
这段代码里有两个容易被忽略的关键点。
第一,connection.on('netAvailable') 回调里拿到的 netHandle 是那一刻的网络句柄,需要及时去查 getNetCapabilities,不要在异步回调里拖太久,否则拿到的可能是已经失效的网络句柄。
第二,isMetered 的判断逻辑写得比较激进:只要 NET_CAPABILITY_NOT_METERED 不在能力列表里,就认为可能是计费网络。这样在Wi-Fi切换蜂窝时,isMetered 为true,UI层就可以提示用户注意流量消耗。这个判断在API 9+的OH系统上是可靠的。
3.2 JS侧:把原生回调封装成hook
拿到原生模块暴露的 initNetworkListener 方法后,JS侧需要把它封装成React风格的hook。核心逻辑是订阅事件、更新状态、组件卸载时清理:
typescript复制// src/hooks/useNetworkStatus.ts
import { useEffect, useRef, useState } from 'react';
import { NativeModules } from 'react-native';
const { NetworkInfoModule } = NativeModules;
export type NetworkType = 'wifi' | 'cellular' | 'ethernet' | 'unknown';
export type NetworkStatus = {
connected: boolean;
networkType: NetworkType;
isMetered: boolean;
};
const INITIAL_STATUS: NetworkStatus = {
connected: true,
networkType: 'wifi',
isMetered: false,
};
export function useNetworkStatus() {
const [status, setStatus] = useState<NetworkStatus>(INITIAL_STATUS);
const currentStatusRef = useRef<NetworkStatus>(INITIAL_STATUS);
useEffect(() => {
if (!NetworkInfoModule?.initNetworkListener) {
console.warn('NetworkInfoModule not available');
return;
}
NetworkInfoModule.initNetworkListener((eventStr: string) => {
const event = JSON.parse(eventStr);
const prev = currentStatusRef.current;
let next: NetworkStatus;
if (event.type === 'lost') {
next = { connected: false, networkType: 'unknown', isMetered: false };
} else {
next = {
connected: true,
networkType: event.networkType,
isMetered: event.isMetered,
};
}
currentStatusRef.current = next;
setStatus(next);
});
return () => {
// 组件卸载时注销回调
NetworkInfoModule.dispose?.();
};
}, []);
return status;
}
注意我用了 currentStatusRef 来保存当前状态,而不是直接依赖 status。这是因为事件回调是异步的,如果回调里读取的是过期的闭包状态,会出现前一次状态覆盖后一次更新的问题。用ref保存最新状态,每次事件触发都能拿到准确的上一次状态。
3.3 UI提示组件:Animated与图标库的组合
UI层我在RNoh应用里用一个自定义的 NetworkBanner 组件,挂在应用根组件下面。这样不管当前页面跳转到哪里,网络提示横幅都会覆盖在上面:
tsx复制// src/components/NetworkBanner.tsx
import React, { useEffect, useRef } from 'react';
import { Animated, StyleSheet, Text, View } from 'react-native';
import { useNetworkStatus } from '../hooks/useNetworkStatus';
import { WifiOff, Signal } from 'lucide-react-native';
export function NetworkBanner() {
const status = useNetworkStatus();
const translateY = useRef(new Animated.Value(-80)).current;
const shouldShow =
!status.connected ||
(status.connected && status.networkType === 'cellular') ||
(status.connected && status.networkType === 'ethernet');
useEffect(() => {
if (shouldShow) {
Animated.spring(translateY, {
toValue: 0,
useNativeDriver: true,
damping: 20,
stiffness: 200,
}).start();
} else {
Animated.timing(translateY, {
toValue: -80,
duration: 250,
useNativeDriver: true,
}).start();
}
}, [shouldShow]);
let bannerText = '网络连接已断开';
let BannerIcon = WifiOff;
if (status.connected && status.networkType === 'cellular') {
bannerText = status.isMetered
? '已切换到蜂窝数据,注意流量消耗'
: '已切换到蜂窝网络';
BannerIcon = Signal;
} else if (status.connected && status.networkType === 'ethernet') {
bannerText = '已切换到有线网络';
BannerIcon = Signal;
}
if (!shouldShow) {
return null;
}
return (
<Animated.View style={[styles.banner, { transform: [{ translateY }] }]}>
<BannerIcon color="#fff" size={16} />
<Text style={styles.text}>{bannerText}</Text>
</Animated.View>
);
}
const styles = StyleSheet.create({
banner: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 48,
backgroundColor: '#d97706',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
zIndex: 999,
},
text: {
color: '#fff',
marginLeft: 8,
fontSize: 14,
fontWeight: '500',
},
});
图标库用的是 lucide-react-native,这个图标库在OH上兼容性不错,RNOH社区近期也有它的适配包。之前我一直用自绘的矢量图,维护成本高,换图标库之后清爽很多。
3.4 状态机设计:提示不闪烁的核心
网络切换提示最大的体验问题是"闪烁":从Wi-Fi切到蜂窝,系统会先触发一次netLost(Wi-Fi消失),然后触发一次netAvailable(蜂窝建立)。如果UI在这两个事件之间弹了"断网"提示,紧接着又弹"蜂窝"提示,用户会看到提示条闪一下再变内容,非常掉价。
我处理的方式是在hook层维护一个 previousNetworkType,只在类型变化跨越关键状态时才触发UI:
- 从
wifi到cellular:弹出蜂窝提示。 - 从任意状态到
disconnected:弹出断网提示。 - 从
disconnected到wifi/ethernet:弹出恢复提示。 - 从
wifi到wifi(同类型变化):不弹。
这个判断逻辑可以放在组件层,也可以放在hook层。我建议放在hook层,组件只负责根据状态渲染,不参与业务判断。后续如果想扩展提示规则,只需要改hook里的状态机。
4. 编译部署那一堆坑:白屏排查、hap产物与可清理的目录
4.1 编译产物与哪些目录可以放心清理
RNOH工程用DevEco Studio构建,点击 Build > Build Hap(s)/APP(s) 后,最终产物的路径通常是:
text复制entry/build/default/outputs/default/entry-default-signed.hap
这个 .hap 文件就是要安装到开发板上的应用包。工程跑久了,build 目录底下会积累大量中间产物,磁盘空间紧张。可以放心清理的目录包括:
entry/build/:构建中间产物,删除后重新Build会自动生成。build/:根目录构建缓存,同理。.hvigor/:hvigor构建工具的缓存目录。.idea/:IDE的本地配置缓存,不影响构建。
绝对不能删除的目录包括:
oh_modules/:OpenHarmony侧的三方依赖,删了要重新下载。node_modules/:RN侧依赖,删了要重新npm install。build-profile.json5、hvigorfile.ts:构建配置文件,删了工程没法编译。
我一般会在连续几个版本迭代后清理一次中间产物,能释放几个GB的空间。但千万记得清理完重新Build一次,确认能出包再往下走。
4.2 启动白屏的完整排查链路
RNOH应用最常见的故障就是启动白屏。我在第一次接原生模块时就遇到了,现象是应用启动后屏幕一片白,没有任何RN内容加载出来。排查链路我拆成四步:
第一步:确认Ability能正常启动
bash复制hdc shell aa start -a EntryAbility -b com.example.app
如果命令返回success,说明OpenHarmony侧的Ability启动没问题,白屏问题出在JS层加载环节。如果启动失败,先查 module.json5 里的ability配置。
第二步:确认JS bundle来源
RNOH应用在debug模式下会从Metro服务器加载bundle,需要先启动Metro,并做端口映射:
bash复制hdc rport tcp:8081 tcp:8081
然后在电脑浏览器访问 http://localhost:8081/status,如果Metro正常,会返回类似 packager-status:running 的信息。如果端口映射没做,RN侧加载bundle会一直卡在连接超时,表现就是白屏。
第三步:抓取ReactNativeJS日志
bash复制hdc hilog | grep ReactNativeJS
RN侧的JS异常会打上 ReactNativeJS 标签。我遇到过的典型报错是 TypeError: Cannot read property 'initNetworkListener' of null,那就是原生模块注册失败导致 NativeModules.NetworkInfoModule 为空。
第四步:检查原生模块注册
RNOH不同版本的原生模块注册方式不同,有的需要在 PackageProvider 里添加模块类名,有的靠自动扫描。如果 NetworkInfoModule 没有正确注册,JS侧拿到的就是null,调用任何方法都会白屏。这一步往往是最容易忽略的。
4.3 调测时的常用hdc命令备忘录
调试阶段我高频使用的命令整理如下:
| 用途 | 命令 |
|---|---|
| 安装应用 | hdc install entry-default-signed.hap |
| 卸载应用 | hdc uninstall com.example.app |
| 启动应用 | hdc shell aa start -a EntryAbility -b com.example.app |
| 查看应用安装列表 | `hdc shell bm dump -a |
| 查看RN侧日志 | `hdc hilog |
| 查看原生崩溃日志 | `hdc hilog |
| 端口映射 | hdc rport tcp:8081 tcp:8081 |
| 发送文件到设备 | hdc file send local remote |
这些命令不用都记住,但 hdc hilog | grep ReactNativeJS 和 hdc install 建议刻在脑子里,它们是调试RNOH应用最重要的两条。
5. 真机实测:Wi-Fi切蜂窝、断网恢复和有线网口的边界处理
5.1 Wi-Fi断掉再切蜂窝的实测时序
把网络切换提示接入应用后,我在RK3568开发板上做了几次实测,重点观察从Wi-Fi切换到蜂窝数据的时间线。
实测步骤:开发板连接Wi-Fi,同时插入一张能上网的SIM卡(通过USB 4G模块),然后拔掉路由器电源。日志输出大致如下:
text复制netLost 触发 -> Wi-Fi连接断开
netAvailable -> 系统自动切换到蜂窝网络
getNetCapabilities -> networkType=cellular
从拔掉路由器电源到系统完成网络切换,整个过程大约在几百毫秒到1秒之间,具体取决于系统网络策略。RN侧连续收到了两次回调,第一次是 lost,第二次是 available 且类型为 cellular。
如果我完全按照事件顺序渲染,用户会先看到"断网"提示条,再被替换成"蜂窝"提示条,体验很糟糕。所以我做了一层防抖处理:收到 lost 事件后不立即显示断网提示,而是给系统一个短暂的切换等待窗口(比如300ms)。如果300ms内来了新的 available 事件,就只显示蜂窝提示;如果超过300ms仍无新事件,才显示断网提示。这个窗口值可以根据实际场景微调。
5.2 断网恢复与前后台切换的状态同步
完全断网的场景下,系统会触发 netLost,然后一直处于无网状态。当网络恢复时(比如重新插上网线或Wi-Fi恢复),会触发 netAvailable,RN侧正常收到事件。
但有一个场景是事件订阅覆盖不到的:应用在后台运行了很久,用户切回前台时,网络可能已经经历了多次切换,而系统只在切换发生的瞬间触发事件,后台应用可能错过了部分事件。此时UI显示的网络状态可能和实际不匹配。
解决方案是在应用回到前台时主动查询一次当前网络状态。RN侧可以用 AppState 监听 change 事件,当状态变为 active 时,调用原生模块新增的 queryCurrentNetwork() 方法:
typescript复制// 原生模块补充方法
queryCurrentNetwork(): string {
const netHandle = connection.getDefaultNetSync(); // 伪代码,实际需异步
// 解析网络类型并返回
}
有了主动查询兜底,事件订阅负责实时性,主动查询负责正确性,两条链路配合起来,网络状态才能做到可靠。
5.3 有线网口与Wi-Fi共存时,网络类型判断要有优先级
RK3568开发板有个和手机不同的特点:它自带一个RJ45网线接口。开发板上同时插着网线、连着Wi-Fi的情况很常见。此时 getDefaultNet() 返回的往往是优先级更高的有线网络,网络类型会判断为 ethernet。
如果UI层在 ethernet 和 wifi 之间反复横跳,会导致提示条频繁出现又消失。实际项目中,我把 wifi 和 ethernet 都归类为"非计费网络",只有从非计费网络切换到 cellular 时才提示,从 ethernet 切到 wifi 不做任何提示。这样既避免了提示疲劳,也保住了真正需要提醒用户的场景:蜂窝网络会产生数据流量费用。
5.4 判断"有网"不能只看网络类型:深入NetCapabilities
最后想说一个容易被忽略的细节:连上了Wi-Fi不代表能访问互联网。有些开发板连接的Wi-Fi热点本身没外网,或者网络处于半开状态。如果只看网络类型,会误判为"有网"。
要更准确地判断网络可用性,需要检查 NetCapabilities 里的能力标志。OpenHarmony的 NetCap 枚举里有几个关键标志:
NET_CAPABILITY_INTERNET:网络具备访问互联网的能力。NET_CAPABILITY_NOT_METERED:网络不计费。NET_CAPABILITY_VALIDATED:网络已验证可访问公网。
在 getNetCapabilities 返回的结果里,如果 capabilities 数组不包含 NET_CAPABILITY_INTERNET,即使网络类型是Wi-Fi,也要认为网络实际不可用。把这个判断加进网络状态处理逻辑后,提示条才能真正反映用户的真实体验,而不是只看表象。
我在原生模块里最终维护的判定逻辑是:先看是否 NET_CAPABILITY_INTERNET,再看网络类型,最后看是否计费。三者结合,才能给RN侧的UI组件提供准确、有用的信息。
整个项目跑下来,这套方案在RK3568和RK3588开发板上都稳定工作。如果你想在自己项目里复用,建议直接把 NetworkInfoModule.ets、useNetworkStatus.ts、NetworkBanner.tsx 三个文件拿过去,按自己的业务文案改一改就够用。顺带一提,RNOH的版本迭代很快,如果遇到原生模块注册方式或API变动,优先查你当前RNOH版本对应的官方文档,网上的旧教程不一定适用。
