OpenHarmony上基于React Native的网络切换提示组件开发
在社区应用开发的迭代过程中流量消耗问题曾经一度成为客服通道的高频话题用户从WiFi切到移动网络时视频还在后台自动播放等账单出来第一反应就是应用在偷跑流量。要解决它最直接的方案是做一个网络切换提示。在OpenHarmony平台上我选择了用React Native来承载这个能力底层依赖NetworkInfo这套网络信息API。这篇博文会完整记录这个功能的开发思路、桥接设计、代码实现和排坑过程适合正在做RN适配OpenHarmony或者准备在跨端应用里实现网络感知功能的朋友参考。做这个功能之前先泼一盆冷水OpenHarmony的RN生态还不像Android/iOS那么成熟很多原生模块要么缺位、要么适配得不完整尤其是网络状态监听这种和系统能力强相关的部分基本都要自己动手补。但也正因为如此做好一个网络提示组件能帮你把RN在OpenHarmony上的原生扩展套路全部摸一遍后面再做其他能力会顺畅很多。我下面把整个过程拆成环境准备、NetworkInfo桥接、交互落地、问题排查四个部分来讲尽量把每个为什么这样做都说清楚。1. 被忽视的流量杀手网络切换提示到底解决什么问题1.1 一个让客服炸锅的场景去年我们做的是一个带视频播放的社区类应用用户在WiFi环境下刷信息流视频默认自动播放。很多人的使用习惯是躺在床上刷着刷着就睡着了或者从客厅走到阳台WiFi信号变弱手机自动切到了蜂窝网络。这个过程系统层面是静默完成的视频App如果不在前台做一些感知处理就会继续用移动网络拉流一晚上下来GB级别的流量消耗就这么产生了。这种事一旦发生用户的体感是App在偷流量投诉对象就是产品而不是运营商。客服工单量飙升我们技术团队被迫在下一轮迭代中加入网络状态感知功能。需求本身并不复杂监听网络从WiFi切换到蜂窝网络的瞬间给用户弹一个明确的提示必要的时候暂停自动播放把流量决策权交还给用户。这个场景背后其实是所有内容消费型App的共性需求视频、音乐、下载、直播、甚至地图类应用都需要在流量成本变化时做出可见的反馈。没有提示用户等于在蒙眼消费有提示用户至少知道发生了什么。一个只有几十行代码的提示组件把大量的客诉问题掐灭在了源头这个投入产出比相当可观。1.2 为什么是RNOpenHarmony而不是ArkUI重写团队当时的技术储备主要在React Native这条线上已有的业务代码、状态管理、基础组件全是RN的。如果为了适配OpenHarmony用ArkUI从零重写等于维护两套完全平行的前端工程业务迭代要双倍投入。用RN跑在OpenHarmony上业务层代码基本可以在Android/iOS/OpenHarmony三端复用只需要针对系统差异部分编写原生适配层。这个复用的价值在NetworkInfo这个功能上体现得很明显。网络感知属于典型的跨端能力Android有ConnectivityManageriOS有NWPathMonitorOpenHarmony有ohos.net.connectionAPI形态各不相同但最终给业务侧的信息是统一的当前是WiFi还是蜂窝网络是否可用。通过RN把它们抽象成一个统一的JS接口上层业务代码完全不用关心当前跑在什么系统上。当然选择RNOpenHarmony也要接受它的代价适配层要自己写排错手段相对有限文档和社区案例都不多。这也是我写这篇博文的原因——把已经蹚过的坑记录下来后面的人能少走一些弯路。2. 环境准备在OpenHarmony上跑起React Native工程的完整链路2.1 工程搭建的两种方式与选型建议在OpenHarmony上启动一个RN工程实际有两种路径。第一种是用社区适配框架提供的脚手架模板直接初始化生成一个包含ArkTS壳工程和React Native JS工程的双层结构。这种方式的好处是工程结构是现成的依赖版本也经过验证适合从零开始、没有历史包袱的项目。第二种路径是把现有RN项目迁移进来保留原有的RN代码仓库单独创建一个OpenHarmony工程作为宿主把RN打包成bundle后放入宿主工程加载。这种方式适合已有成熟RN代码、只是需要新增一个OpenHarmony端的情况。优点是不用动业务代码缺点是需要处理存量原生模块的兼容性凡是涉及Android/iOS原生能力的第三方库都要逐个检查在OpenHarmony上是否有对应的ArkTS实现。我们当时用的就是第二种路径因为信息流、播放器、推送这些都是现成的只想尽快在OpenHarmony上跑通。如果你也在评估迁移我的建议是先盘点一下项目的原生依赖。纯JS组件直接复用没问题但像地图、支付、Push这类强依赖系统SDK的模块大概率要替换或自己适配这些才是迁移工作量的主要来源。React Native的bundle加载思路在OpenHarmony上也是一样的本地加载还是远端加载、是否拆包都直接影响首屏体验。如果RN业务包比较重建议在OpenHarmony壳工程里做一个原生的启动页等JS bundle执行完并渲染出第一帧之后再做切换不然用户看到的就会是一段白屏。2.2 与Android/iOS工程的三处关键差异把这个工程搭起来之后你会发现RN在OpenHarmony上的开发模式和Android/iOS并没有本质区别但在三个具体环节上有明显差异也是新手最容易卡住的地方。第一原生模块的编写语言是ArkTS。Android上用Java/Kotlin写NativeModuleiOS上用Objective-C/SwiftOpenHarmony上用的是ArkTS语法类似TypeScript但底层运行时完全不同。写惯了Android原生代码的人第一次接触ArkTS会有点不太习惯尤其是装饰器、UIAbility生命周期这些概念需要花点时间适应。第二事件桥接的姿势不完全一样。RN侧通过NativeEventEmitter监听原生事件这个机制在OpenHarmony上是通用的但原生侧怎么向JS侧emit事件不同适配框架的API可能略有差异。有的框架封装了RNEventEmitter有的依赖全局事件总线写桥接代码之前先看一下框架提供的模板工程里是怎么做的能省很多事。第三权限声明体系独立。Android用AndroidManifest.xmliOS用Info.plistOpenHarmony用module.json5里的requestPermissions字段。系统级权限的授权策略也和Android不一样这个下面单独说。2.3 网络权限与module.json5配置网络状态监听在OpenHarmony上需要一个名为ohos.permission.GET_NETWORK_INFO的权限它属于system_grant类型的普通权限也就是说不需要在运行时向用户弹窗申请只要在module.json5里声明即可。下面是我在entry模块下的配置{ module: { name: entry, type: entry, requestPermissions: [ { name: ohos.permission.GET_NETWORK_INFO, reason: 用于检测当前网络类型和网络连接状态在切换蜂窝网络时提醒用户注意流量消耗, usedScene: { abilities: [EntryAbility], when: inuse } } ] } }reason字段建议写清楚用途虽然系统授权不要求用户确认但在应用市场审核时它是会被审查的。when字段的inuse表示仅在前台使用如果应用需要退到后台也继续感知网络状态要改为always。我强烈建议配合inuse使用既满足业务诉求又尽量缩小权限范围审核风险和用户隐私成本都更低。配置完成后在ArkTS侧可以直接导入连接管理模块进行网络查询不需要额外初始化。我经常遇到有人忘记配权限就直接调用API结果getDefaultNet一直返回失败这种问题排查起来不难但很消磨耐心。3. NetworkInfo核心实现网络类型识别、事件订阅与桥接3.1 ohos.net.connectionOpenHarmony原生网络能力剖析OpenHarmony的网络连接管理模块是ohos.net.connection它对外提供网络句柄获取、网络能力查询和网络状态事件订阅三类能力。我们做网络切换提示核心只用两张牌getDefaultNet加getNetCapabilities负责当前状态快照on系列监听器负责变更通知。getDefaultNet()返回当前默认网络的句柄NetHandle拿到句柄之后调用getNetCapabilities(netHandle)返回结果里有一个bearerTypes数组。这个数组里装的是网络承载类型枚举值常见的如下表所示枚举值含义BEARER_WIFIWiFi网络BEARER_CELLULAR蜂窝网络移动数据BEARER_ETHERNET有线以太网BEARER_BLUETOOTH蓝牙网络注意一个网络连接可能同时具备多种承载类型比如同时连了WiFi和以太网bearerTypes数组里会有两个元素。判断网络类型时不能简单用要用includes去检测数组里是否包含指定类型。事件订阅方面connection模块提供了netAvailable、netLost、netCapabilitiesChange等事件。netCapabilitiesChange是最细粒度的事件网络能力一变它就触发但它携带的数据是网络句柄和新的能力详情需要我们自己解析出当前网络类型。netAvailable表示网络可用netLost表示网络丢失这两个事件的覆盖面更粗。做切换提示时netCapabilitiesChange是主力另外两个是辅助。3.2 桥接层把网络状态从ArkTS侧送进JS侧RN和原生侧的通信在Android/iOS上有成熟的TurboModule机制在OpenHarmony的适配层里同样有对应的模块封装能力。我们需要做两件事暴露一个getNetworkType方法给JS侧调用让JS可以主动查询当前网络状态以及在网络事件触发时向JS侧emit事件实现被动感知。下面是我在ArkTS侧写的桥接Module代码做了简化但核心链路是完整的// NetworkInfoModule.ets import connection from ohos.net.connection; import { RNNativeModule, RNEventEmitter } from react-native-openharmony; export class NetworkInfoModule extends RNNativeModule { private emitter: RNEventEmitter | null null; constructor(ctx: any) { super(ctx); this.emitter this.createEventEmitter(NetworkInfoModule); this.registerNetworkListeners(); } getNetworkType(): Promisestring { return connection.getDefaultNet().then((netHandle) { if (netHandle null) { return none; } return connection.getNetCapabilities(netHandle).then((caps) { if (caps.bearerTypes.includes(connection.NetBearType.BEARER_WIFI)) { return wifi; } if (caps.bearerTypes.includes(connection.NetBearType.BEARER_CELLULAR)) { return cellular; } return unknown; }); }).catch(() none); } private registerNetworkListeners(): void { connection.on(netAvailable, () { this.emitter?.emit(networkChange, { status: available }); }); connection.on(netLost, () { this.emitter?.emit(networkChange, { status: lost }); }); connection.on(netCapabilitiesChange, (data) { this.emitter?.emit(networkChange, { status: capabilitiesChange, data }); }); } }这里有三个容易踩的细节。第一createEventEmitter的调用时机必须在Module构造完成之后很多适配框架在这块有严格顺序要求。第二netHandle为null表示当前没有默认网络也就是完全断网的状态这个分支要返回none而不是继续调用getNetCapabilities否则会抛异常。第三事件名networkChange要和JS侧监听时保持一致这个字符串没有自动校验拼错了就会静默失败排查起来特别痛苦。3.3 JS侧统一状态管理hooks封装与业务解耦原生侧的东西就绪之后JS侧需要做一层封装把桥接能力和业务逻辑解耦。这样在组件里调用时看起来就像是一个普通的React hook不需要关心底层是OpenHarmony还是Android。// networkInfo.js import { NativeModules, NativeEventEmitter } from react-native; const { NetworkInfoModule } NativeModules; const networkEmitter new NativeEventEmitter(NetworkInfoModule); export function getNetworkType() { return NetworkInfoModule.getNetworkType(); } export function subscribeNetworkChange(callback) { const subscription networkEmitter.addListener(networkChange, callback); return () subscription.remove(); }调用侧的业务逻辑关键在于判断变化方向。netCapabilitiesChange触发的频率其实相当高信号强度变化、网络地址变化都可能导致回调如果我们不做判断就提示用户那App会变成一个只会弹横幅的牛皮癣。// useNetworkTip.ts import { useEffect, useRef } from react; import { getNetworkType, subscribeNetworkChange } from ./networkInfo; export function useNetworkTip(onTip: (type: wifi-to-cellular | disconnected | reconnected) void) { const lastTypeRef useRefstring | null(null); useEffect(() { let mounted true; getNetworkType().then((type) { if (mounted) { lastTypeRef.current type; } }); const handleChange async (event: any) { const currentType await getNetworkType(); const lastType lastTypeRef.current; if (lastType wifi currentType cellular) { onTip(wifi-to-cellular); } else if (currentType none) { onTip(disconnected); } else if (lastType none currentType ! none) { onTip(reconnected); } lastTypeRef.current currentType; }; const unsubscribe subscribeNetworkChange(handleChange); return () { mounted false; unsubscribe(); }; }, []); }注意我在事件回调里主动调用了getNetworkType()而不是直接用event.data解析。原因是在某些网络快速切换的场景下netCapabilitiesChange携带的句柄可能已经失效主动查询虽然多一次桥接开销但拿到的状态最真实。网络感知这种低频操作准确率比性能重要得多。4. 提示交互落地从网络事件到顶部横幅的完整链路4.1 Toast、顶部横幅还是全屏弹窗提示形态选型网络状态变化提醒的呈现方式一般有三种选择系统Toast、页面顶部横幅、全屏弹窗。三者的定位完全不同用错了轻则体验平庸重则打断用户操作。提示形态优点缺点适用场景系统级Toast实现简单调用系统能力样式不可控文字不可点容易被系统其他内容遮挡断网重连、状态恢复页面顶部横幅样式可控可点击可带icon需要自己管理动画和定时隐藏WiFi切蜂窝的流量提醒、下载暂停提醒全屏弹窗曝光最强可承载复杂交互强打断用户反感度高必须用户主动确认的敏感操作比如继续使用移动网络下载吗我的建议是分层配合流量从WiFi切到蜂窝这种成本可能上升的场景用顶部横幅提示因为用户可能正在看视频一个弱打断的提示足够但又不至于强到让人烦完全断网这种场景用轻量Toast提示即可而如果应用里存在大流量下载任务需要用户确认是否继续才用全屏弹窗。我们最终的生产方案就是顶部横幅加Toast的组合。下面的实现细节主要围绕顶部横幅展开。4.2 自动消失横幅的实现细节顶部横幅的核心诉求有三个从顶部滑入时平滑不突兀停留时间可控支持点击关闭或跳转。用RN的Animated组件实现起来很直接。// NetworkTipBanner.tsx import React, { useEffect, useRef } from react; import { Animated, Text, TouchableOpacity, StyleSheet } from react-native; interface BannerProps { visible: boolean; type: wifi-to-cellular | disconnected | reconnected; onClose: () void; } const MESSAGE_MAP { wifi-to-cellular: 当前使用移动数据请注意流量消耗, disconnected: 网络连接已断开, reconnected: 网络已恢复, }; export default function NetworkTipBanner({ visible, type, onClose }: BannerProps) { const translateY useRef(new Animated.Value(-120)).current; useEffect(() { if (!visible) return; Animated.timing(translateY, { toValue: 0, duration: 250, useNativeDriver: true, }).start(); const timer setTimeout(() { Animated.timing(translateY, { toValue: -120, duration: 200, useNativeDriver: true, }).start(() onClose()); }, 3000); return () clearTimeout(timer); }, [visible]); if (!visible) { return null; } return ( Animated.View style{[styles.banner, { transform: [{ translateY }] }]} TouchableOpacity activeOpacity{0.8} onPress{onClose} style{styles.touchArea} Text style{styles.text}{MESSAGE_MAP[type]}/Text /TouchableOpacity /Animated.View ); } const styles StyleSheet.create({ banner: { position: absolute, top: 0, left: 0, right: 0, backgroundColor: rgba(32, 32, 32, 0.95), paddingTop: 48, paddingBottom: 12, paddingHorizontal: 16, zIndex: 1000, }, touchArea: { flexDirection: row, alignItems: center, justifyContent: center, }, text: { color: #fff, fontSize: 14, }, });这里有两个细节值得注意。第一banner放在绝对定位的顶层容器里zIndex要足够大避免被页面其他浮层盖住但如果页面里本身有全屏模态框建议把banner挂在模态框之上的兄弟节点。第二useNativeDriver: true在RN的Android/iOS上已经很稳定但在OpenHarmony的适配层上可能会有版本差异如果发现动画掉帧或者不生效可以先改成false验证是不是原生驱动的问题。4.3 防抖、事件优先级与弱网边界处理网络事件不像用户点击它不受人的主观控制存在短时间内反复切换的可能。比如在电梯里、在地铁进出站时信号来回横跳如果没有防护提示横幅会像抽风一样反复弹出。我的处理策略分三层。第一层是类型判定只有网络类型实际发生变化时才触发提示单纯的信号强度变化不处理这在useNetworkTip里已经做了。第二层是时间防抖同一个提示类型在5秒内不重复出现用时间戳记录上一次触发时间小于间隔直接丢弃。第三层是优先级覆盖如果断网提示刚显示1秒用户网络恢复应该立刻切到恢复提示而不是等断网提示的3秒展示完。let lastTipTime 0; const TIP_INTERVAL 5000; function shouldShowTip(type: string): boolean { const now Date.now(); if (type reconnected) { return true; // 恢复提示需要打断断网提示 } if (now - lastTipTime TIP_INTERVAL) { return false; } lastTipTime now; return true; }弱网边界的情况也要考虑。getDefaultNet在手机关闭WiFi但蜂窝信号也差的时候可能返回一个非空句柄但getNetCapabilities内部报错我们在Promise的catch里统一返回了none这会导致一瞬间把当前状态误判成断网。实际体验中这种误判时间很短用户基本感知不到但如果你的业务有断网就暂停播放的逻辑要注意给这个暂停操作加一个恢复路径否则网络恢复后播放器不会自动重新开始。5. 实测排坑白屏与渲染异常一次定位5.1 启动白屏bundle加载时序导致的视觉空白RN应用在OpenHarmony上启动时最容易遇到的就是白屏。我在接入NetworkInfo功能之前打开App的首帧渲染明显比Android慢帧率大概差了300到500毫秒。白屏的根源在于启动链路变长了原生壳工程要先启动UIAbility然后创建RN运行时再等待JS bundle加载、解析、执行最后JS侧完成首屏渲染。在这个链路中任何一步慢都会让用户看到白屏。OpenHarmony的模拟器和低端设备上由于CPU和内存能力有限bundle加载耗时可能翻倍。我试过几种缓解方案效果从高到低排列。最有效的是在ArkTS壳工程层做一个原生的启动闪屏页直接把windowStage.loadContent切到RN页面之前先挂一个原生页面等RN侧首帧渲染完成后再关闭。这需要RN侧通过桥接给原生侧发一个首帧已渲染的信号。其次是优化bundle把基础库和三方库做分包入口页面只依赖核心包把非首屏的模块放到require的运行时加载。第三是在开发阶段开启bundle预加载生产环境把bundle打成单文件减少IO次数。要注意的是启动白屏和NetworkInfo这个功能本身没有直接关系但在我们的场景里因为是在同一个迭代里处理的所以把它也纳入了一并排查。如果你在接入RNOpenHarmony时遇到白屏优先怀疑启动链路的资源竞争而不是具体某个功能模块。5.2 渲染异常ArkUI与RN视图层级的冲突排查另一个高频问题是画面渲染异常。OpenHarmony的渲染层是基于ArkUI的RN的视图要嵌入到ArkUI的节点树中这个跨框架的视图嵌套本身就是渲染异常的高发区。具体表现有两种一种是页面局部出现黑块或白块另一种是动画过程中整体闪烁。遇到这类问题我的排查链路是先做减法再做隔离。先注释掉页面上所有自定义的原生组件用纯RN组件跑一遍如果异常消失说明冲突出在原生组件与RN视图混用上如果异常还在再注释掉动画相关的代码看是否是useNativeDriver导致的问题。这个减法过程虽然笨但比猜要快得多。我们实际遇到的案例是NetworkTipBanner在入场动画期间页面底部的视频区域出现了短暂的黑屏闪烁。通过减法定位后确认是banner使用position: absolute加zIndex时在OpenHarmony上触发了视图层级重组底层视频播放器的Surface帧没有同步。最后处理方式是不用useNativeDriver把动画改为JS驱动虽然性能稍差一点但避开了底层图层同步的问题。在你写自己的RNOpenHarmony应用时记住一个原则优先保持渲染链路简单花哨的动画效果放在最后再叠加。OpenHarmony的RN适配层还在快速迭代越是复杂的渲染组合越容易踩到框架本身的bug。6. 设备兼容与性能优化从liteos-m边界到x86模拟器6.1 liteos-m与传统OpenHarmony的架构边界聊设备兼容必须先说清楚一个边界问题。OpenHarmony不仅运行在智能手机上也覆盖各种IoT设备其中一部分轻量设备使用的是liteos-m内核和轻量级系统能力。这类设备的内存可能只有几百KB到几MB级别连标准的JS引擎都跑不动更不用说React Native这种完整的跨端框架。所以用RN开发OpenHarmony应用这件事有一个默认前提目标是标准系统设备比如手机、平板、带屏的富媒体设备。如果你的产品需要覆盖到liteos-m级别的传感器、门锁、智能灯这类设备应该走OpenHarmony的轻量级开发路线用ArkTS或C/C直接开发而不是套RN。这个边界在选型时就要想清楚否则后续会出现框架跑不起来但已经在项目上投入了大量时间的被动局面。主页面上用RN做富交互内容轻量设备用原生方案做数据采集和上报两条路并用才是覆盖完整产品矩阵的合理架构。6.2 x86模拟器与真机行为差异开发阶段用x86模拟器调试网络行为经常和真机不一样。我踩到的具体问题是模拟器的netCapabilitiesChange事件触发频率远高于真机而且有时在WiFi和蜂窝之间切换网络时getDefaultNet返回的句柄类型不够稳定导致JS侧偶尔把WiFi误判成unknown。这不一定是代码bug更可能是模拟器宿主网络栈以软件方式模拟多种网络类型时API返回的时序和真机有差异。真机上基站和AP之间的切换是操作系统统一调度的模拟器则直接复用宿主机网络状态中间少了很多中间状态。因此网络功能不能只在模拟器上验证必须在真机上跑一遍完整的切换场景WiFi切蜂窝、蜂窝切WiFi、关闭数据、飞行模式开关以及弱网状态下的断线重连。如果不具备多台真机条件至少用一台开发板和一台手机做交叉验证。6.3 性能优化让网络回调不拖累UI线程最后说性能。NetworkInfo桥接本身是低频操作但它触发的时间点往往正好卡在用户操作的高频期比如正在滑动信息流时网络切换UI线程既要处理手势响应又要处理横幅动画这就可能造成明显的掉帧。优化手段集中在三个地方。第一所有网络事件回调里不要做重活只做状态记录真正影响UI的状态变更用InteractionManager.runAfterInteractions延迟执行避开手势动画的关键帧。第二顶部横幅的显隐尽量用预先创建好的节点做显隐控制而不是每次动态mount和unmount减少视图层级的频繁重建。第三JS侧在多个页面同时监听网络事件时要注意在页面卸载时统一调用unsubscribe否则事件监听器积少成多也会造成内存和性能问题。实测下来这三项优化做完之后连续切网场景下的掉帧概率大幅下降用户体验没有出现明显的顿挫感。最后再分享一点个人体会。网络提示这种功能在需求列表里永远排不进P0但它对用户信任的建立非常重要——一次清晰及时的提示能避免一次冗长的客服工单。如果重新做一次我会把网络事件状态机单独抽成一个模块把wifi、蜂窝、无网、恢复这些状态建模清楚以后就算增加双卡切换、物联网网关这些场景也能在不改UI层代码的情况下扩展底层逻辑。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →