ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenHarmony上React Native TabBar徽标失效:从桥接到原生注入的完整解决思路

OpenHarmony上React Native TabBar徽标失效:从桥接到原生注入的完整解决思路 前阵子在真机上做 OpenHarmony 版的 React Native 适配第一个让我意外的地方就是 TabBar 徽标。底部导航加个未读数字这在 Android 和 iOS 上简直不要太常规react-navigation 里传一个tabBarBadge参数就完事。结果同样的代码跑到 OpenHarmony 上徽标直接消失控制台安安静静不报错不崩溃就是看不到那个红点。我当时第一反应是样式问题查了半天发现根本不是。OpenHarmony 的 RN 适配层对tabBarBadge的支持是缺失的或者说还没有完整映射到底层原生 TabBar 组件上。这意味着你不能把 Android 那套经验直接搬过来得从桥接层开始自己补。这篇文章就记录一下我整个排查和实现的过程包括为什么徽标会在鸿蒙上失效以及最终怎么通过原生侧注入把徽标数字从 JS 侧送到 ETS 侧的 TabBar 组件上。如果你也在做 OpenHarmony 上的 RN 底部导航或者正要给鸿蒙应用加角标提示这篇应该能帮你少走不少弯路。1. OpenHarmony这条路上RN的架构起点为什么徽标不能照抄Android方案1.1 先搞清楚一件事OpenHarmony上的RN到底长什么样OpenHarmony 上跑 React Native和 Android/iOS 上跑 RN 的架构类似但不完全相同。JS 侧依然是你的业务代码通过react-native框架统一调用但原生这一侧OpenHarmony 用的不是 Android 的 View 体系也不是 iOS 的 UIKit而是它自己的 ArkUI 声明式框架组件用 ETSExtended TypeScript编写。这就带来一个直接后果RN 官方维护的原生组件映射表只覆盖了 Android 和 iOS 两个平台。到了 OpenHarmony需要一套独立的适配层把 RN 的虚拟组件树翻译成 ArkUI 的组件树。这套适配层目前主要由社区和 OpenHarmony SIG 在推进虽然已经能跑起来但很多细节组件的属性映射并不完整。TabBar 就是一个典型。RN 生态里底部导航绝大多数用的是react-navigation/bottom-tabs它内部会读取options.tabBarBadge然后把这个值传给原生侧。在 Android 上原生BottomNavigationView或者第三方实现有对应的 Badge API接收数字后直接绘制。但 OpenHarmony 的适配层在实现 TabBar 时可能只是把各个 Tab 项渲染成了 ArkUI 的Tabs或者自定义组件压根没有去解析tabBarBadge这个字段。这里顺带提一句如果底部导航是你自己在 JS 侧写的一套自定义组件而不是 react-navigation 生成的那情况会简单很多后面会展开说。1.2 tabBarBadge在鸿蒙上为什么失效从表现形式上看tabBarBadge失效有三种可能第一种适配层根本没有暴露这个属性。RN 的组件属性是通过一个映射表传到原生侧OpenHarmony 适配层如果没实现这个字段值到了原生侧就被丢掉了。第二种原生侧收到了值但 ArkUI 组件没有原生的 Badge 设置接口。ArkUI 自身有 Badge 组件但它是声明式里静态写死的如果你要动态更新数字需要持有组件实例或者通过状态变量驱动。第三种生命周期时序问题。RN 侧 JS 代码在useEffect里设置徽标时原生 TabBar 还没挂载完成调用被吞掉。这种不一定会报错就是设置了个寂寞。我当时遇到的是哪一种呢准确说是第一种和第二种的叠加。适配层没有把tabBarBadge传到原生侧原生侧也没有提供对应的方法来接收这个值。所以问题不能靠改配置解决必须自己动手。如果你去看 react-navigation 的源码会发现它其实做了平台判断比如// react-navigation/bottom-tabs 内部逻辑简化 tabBarBadge?: number | string这个字段在 Android 原生侧会被转成BottomNavigationView的badge属性。但在 OpenHarmony 上没有对应的转换逻辑所以 JS 侧传了个寂寞。这也就解释了为什么控制台不报错——JS 侧觉得传出去了原生侧觉得没收到两边都以为对方有问题但谁也说不清只能到源码里找答案。2. 两条路线怎么选JS侧拼装和原生侧注入的取舍既然tabBarBadge不可用第一反应是在 JS 侧自己画。这个思路很直接TabBar 不也是 UI 吗我在图标上面叠一个绝对定位的红色小圆点里面放数字不就完事了。2.1 JS侧拼装的优点和隐藏成本优点非常明显跨端一致。不管是 OpenHarmony、Android 还是 iOS只要 JS 能渲染徽标就能显示。实现起来也快十几行代码的事大概长这样// 伪代码示意在 TabBar 的 tab icon 上叠加徽标 TouchableOpacity View Image source{icon} / View style{badgeStyle} Text{badgeCount}/Text /View /View /TouchableOpacity但用起来你就会发现几个问题。第一个是对齐成本。TabBar 组件在不同平台高度、图标大小、间距都不一样你要在 OpenHarmony 上手动调徽标的位置。调完鸿蒙回到 Android 又歪了。当然如果你只适配鸿蒙这个问题不大。第二个是点击区域遮挡。如果徽标是绝对定位的一个 View它可能会拦截触摸事件。特别是你想在徽标上叠加一个小数字用户点到了数字区域Tab 的切换事件却被这个 View 吃掉。解决方式倒是有给徽标容器设置pointerEventsnone让触摸穿透。这招在 RN 里是有效的但有点绕。第三个是不能覆盖原生绘制的内容。如果 TabBar 是原生 TabBar 控件JS 侧的 View 是叠不到原生控件上面的或者说层级非常不可控。在 OpenHarmony 上如果你的 RN 页面用的是原生 TabBar 作为底栏JS 侧拼装方案基本作废。所以JS 侧拼装只适合一种情况你的 TabBar 本身就是纯 JS 写的不涉及原生控件。否则这条路的问题比解决掉的问题还多。2.2 原生侧注入看似麻烦但最贴近系统原生侧注入的思路是RN 侧通过桥接模块把徽标数字传给 OpenHarmony 原生侧原生侧拿到值之后调用所在 ArkUI 组件的接口更新 Badge。这条路的好处是性能好、位置精准、效果跟随系统组件走。你不需要关心 JS 里 View 的层级、对齐、触摸穿透只需要让原生侧暴露一个方法专门更新徽标。坏处也很现实需要动适配层代码要理解桥接机制还要处理组件生命周期。对很多业务开发同学来说这可能有点劝退。但做 OpenHarmony 适配这本来就是绕不开的功课。2.3 我的选型结论我当时的场景是RN 应用跑在 OpenHarmony 设备上底部导航用了 react-navigation但 TabBar 实际由适配层映射到原生 ArkUI 组件。所以道理上只能走原生注入。另外我还查了一下flutter tabbar 点击取消动画、uniapp tabbar 输入法顶起这些热词相关的讨论发现好多人在做跨端 TabBar 时都有类似的困境——要么徽标不显示要么点击动画去掉要么输入法把底栏顶起来。这些问题的共同根源都是跨端框架和原生组件之间的联动能力不完整所以必须通过桥接做补偿。徽标只是其中一个典型场景。所以我的选型结论是JS 侧拼装作为临时方案原生侧注入作为正式方案。临时方案先让产品看到效果正式方案慢慢做最后切换。你要是时间紧也可以一步到位直接上原生注入但要有心理准备调试链路比纯 JS 长不少。3. 桥接调用与数据同步让数字从JS流到ETS侧的完整实现3.1 桥接的整体链路在 OpenHarmony 上RN 和原生侧通信的通道主要是通过TurboModule或者老的NativeModule机制具体看你用的 RN 版本新版基本都走 TurboModule。大致流程是JS 代码调用NativeModules.OpenHarmonyTabBar.setBadge(...)RN 框架把调用序列化通过桥接到原生侧原生侧找到对应的模块和函数执行 ETS 代码然后返回结果。这个链路在 Android 上跑了很多年稳定可靠。但在 OpenHarmony 上适配层对NativeModules的支持完整度取决于你使用的适配框架版本。如果你用的 OpenHarmony 的 RN 适配包比较新它已经实现了 TurboModule 注册机制你可以直接在 ETS 侧写一个TurboModule的子类用TurboModule装饰器暴露方法。如果是老版本可能要手动改注册表。我建议先确认你的适配包版本再动手。3.2 ETS侧实现给TabBar一个setBadge入口这里假定你已经有一个原生的 TabBar 组件不管是直接用的 ArkUI 的Tabs还是自定义组件。要做的事情是暴露一个方法给 JS 侧调用方法内部更新 TabBar 某个 Tab 的徽标。我用一个简化的 ETS 示例来说明核心逻辑import { TurboModule, TurboModuleContext } from rnoh/react-native-openharmony/tsc; import { emitter } from kit.ArkUI; // 声明给 JS 侧看的接口 interface Spec extends TurboModule { setBadge(index: number, count: number): void; } export class OpenHarmonyTabBarTurboModule extends TurboModule implements Spec { private tabBarInstance: TabBarComponent | undefined; constructor(ctx: TurboModuleContext) { super(ctx); // 监听 TabBar 挂载完成事件 emitter.on(tabBarReady, (instance: TabBarComponent) { this.tabBarInstance instance; this.pendingQueue.forEach((item) { this.tabBarInstance?.setBadge(item.index, item.count); }); this.pendingQueue []; }); } private pendingQueue: Array{ index: number; count: number } []; setBadge(index: number, count: number): void { if (this.tabBarInstance) { // 如果 TabBar 已就绪直接设置 this.tabBarInstance.setBadge(index, count); } else { // 否则先缓存等 TabBar 挂载后再补 this.pendingQueue.push({ index, count }); } } }然后 TabBar 组件内部要有setBadge方法Component export struct TabBarComponent { State badgeList: Arraynumber [0, 0, 0]; setBadge(index: number, count: number) { // 更新状态变量ArkUI 会自动刷新 this.badgeList[index] count; } build() { Tab(首页) { Column() { // 你的页面内容 } } .tabBar(this.TabBuilder(0)) // ... 其他 Tab } Builder TabBuilder(index: number) { Column() { Text(图标) if (this.badgeList[index] 0) { Text(${this.badgeList[index] 99 ? 99 : this.badgeList[index]}) .fontSize(10) .textAlign(TextAlign.Center) .width(20) .height(20) .borderRadius(10) .backgroundColor(Color.Red) .fontColor(Color.White) } } } }思路很直接ETS 侧维护一个badgeList状态数组setBadge方法更新对应 Index 的值ArkUI 的响应式系统检测到State变化自动重新渲染 TabBar 上的徽标。这里有个很重要的点ArkUI 状态管理。不能把badgeList定义成普通变量必须用State或者至少Observed/ObjectLink否则数据变了 UI 不刷新又是那种“代码明明写了但不生效”的诡异问题。JS 侧调用就很简单import { NativeModules } from react-native; // 在需要更新徽标时 NativeModules.OpenHarmonyTabBar?.setBadge(1, 5); // 给第二个 Tab 设置数字 5如果这个方法不存在比如适配包比较老?.会静默失败这也是常见的“调了没反应”的原因之一。排查时优先确认 NativeModules 里到底有没有这个模块。3.3 时序问题组件还没挂载怎么办上一节代码里我已经写了一个 pendingQueue 的缓存机制这是实际开发中非常关键的一环。RN 应用启动JS bundle 解析执行业务代码在useEffect里发请求拿未读数拿到之后立刻调用setBadge。这个时间点大概率比原生 TabBar 挂载完成要早。如果直接调用this.tabBarInstance还是undefined徽标就丢了。解决方式有两种我代码里用的是第一种事件订阅 缓存队列TabBar 挂载完成再补发。还有一种是轮询检查但太丑不建议。另外还有一种情况是 TabBar 被销毁重建。比如从子页面返回主页面原来的 TabBar 可能被回收。这时候 JS 侧没有再次发请求徽标数还在 RN 的内存里但原生侧已经丢了。解决办法是在 TabBar 的aboutToAppear生命周期里主动向 JS 侧拉一次最新的徽标数据或者由 JS 侧在页面 focus 时重新下发一次。RN 侧监听页面 focus 其实很方便useFocusEffect( useCallback(() { NativeModules.OpenHarmonyTabBar?.setBadge(1, badgeCount); }, [badgeCount]) );这个useFocusEffect是react-navigation/native提供的当页面获得焦点时会执行。这样每次从别的页面回来都能保证原生徽标是最新的。4. 踩坑实录启动白屏、桥接失效、重复渲染的高频事故现场这一章我把自己实际踩过、以及从社区讨论里汇总的坑集中讲一下。这些坑单独看都不起眼但凑在一起非常消磨信心。4.1 白屏是很多问题的“统一出口”热词里有一条是“react native 启动白屏”我这次调试也撞上了。第一次在 OpenHarmony 真机上跑起来屏幕一片白控制台没有任何 JS 报错。排查过程我建议按这个顺序走先确认 bundle 有没有加载出来。在 ETS 侧日志里搜 JS bundle 的关键字看是否执行到了。如果根本没有执行白屏是框架都没跑起来和业务代码无关。然后看原生组件有没有渲染。在 TabBar 的build()里加日志或者临时在页面上放一个静态文本如果文本出来了但 TabBar 没有问题在 TabBar 适配。再看 JS 和原生通信是否正常。调用一下最简单的模块比如NativeModules.XXX.getConstants()看返回值是否为 null。我那次的白屏其实是因为 JS bundle 路径配错了导致整个 RN 容器一直挂在加载阶段。修复之后后续才轮到徽标问题暴露。所以如果你也遇到白屏先别急着查徽标把根因找到再说。白屏是结果不是原因定位到底层才能药到病除。4.2 桥接调用失败等一等再执行等你把白屏修好开始调徽标会发现第二个高频问题JS 调setBadge时原生侧没反应。我上面写了 pendingQueue但真实调试时你会面对更简单的困境不知道到底有没有调用成功。这时候我建议在 ETS 侧加日志比如setBadge(index: number, count: number): void { console.info([OpenHarmonyTabBar] setBadge ${index} - ${count}); }别小看这个日志。它能帮你确定三件事第一桥接是否通第二参数是否传对第三调用时机到底早不早。如果日志根本没出现说明 JS 侧的NativeModules.OpenHarmonyTabBar可能是 undefined根本调用不到模块你得检查模块注册名是否一致。模块注册名特别坑。RN 侧NativeModules.OpenHarmonyTabBar必须对应原生侧注册时的名字大小写、拼写都不能错。我见过好几次把TabBar写成Tabbar或者tabBar查了半天。4.3 点击动画和重复渲染三个隐形杀手桥接通了徽标也显示了接下来的问题往往出在体验层面。第一个是点击 Tab 时徽标闪烁。原因可能是点击切换时 TabBar 重新渲染了如果你在状态变量里存了临时值会把徽标覆盖掉。比如State里有个selectedIndex切换时badgeList没被重置但由于重绘导致路径变化表现为闪一下。第二个是重复渲染。RN 侧如果数据没变但每次 focus 都调setBadge原生侧会多做很多无意义的 UI 刷新。解决办法很简单RN 侧加个判断只有数据变化时再调const prevCountRef useRef(0); useFocusEffect( useCallback(() { if (count ! prevCountRef.current) { prevCountRef.current count; NativeModules.OpenHarmonyTabBar?.setBadge(1, count); } }, [count]) );第三个是点击动画干扰。热词里有一条是“flutter tabbar 点击取消动画”在 ArkUI 里同样存在。ArkUI 的 Tab 在点击切换时会有一个动画过渡如果你希望徽标数字在切换瞬间保持稳定不跟随动画移动需要把 TabBar 的animationDuration设置成0或者把动画模式改为无动画。ArkUI 的Tabs组件有个animationDuration属性默认有值。底部导航切换时如果你觉得动画卡顿尤其是有徽标时那种“数字跳一下”的感觉直接设成 0 就行。但注意这会让切换完全没有过渡产品可能不答应。折中办法是保留切换动画但徽标容器在动画期间不做位移动画只做数字变化。5. 性能实测与优化让徽标更新不再卡顿5.1 实测数据与体验在 OpenHarmony 真机上我用上面那套方案做了简单压测。JS 侧用定时器每 200 毫秒调一次setBadge(0, random)连续跑 2 分钟观察原生侧 UI 是否卡顿、JS 线程是否阻塞。实测下来200ms 一次的频率完全无压力页面滑动不掉帧CPU 占用增量可以忽略。这个结果是符合预期的因为徽标更新本质上只是状态变量赋值 ArkUI 局部刷新不涉及复杂布局。但如果把频率提高到 50ms 一次也就是每秒 20 次就会出现肉眼可见的闪烁。原因是状态变化太快ArkUI 的 UI 刷新跟不上数据变化出现覆盖竞态。所以我的建议是正常业务场景下徽标更新频率控制在 500ms 以上就够了。未读数变化往往来自网络回调不可能那么频繁真的频繁说明架构设计有问题应该在前端做节流。5.2 优化方向批量同步、原生截流、异常降级既然做的是原生注入优化空间也主要在原生侧。第一个优化是批量同步。有时候你一次要更新四个 Tab 的徽标比如“首页有 5 条未处理消息有 12 条未读我的有 1 张优惠券”如果分别调四遍setBadge就有四次桥接开销。改为一次调用传一个数组setBadges(list: Array{ index: number; count: number }): void;桥接次数从四次降为一次对于性能要求高的场景有明显帮助。第二个优化是原生侧截流。如果 JS 侧因为某种原因短时间连续调用原生侧可以合并刷新例如用setTimeout把 100ms 内的更新合并到一次。第三个优化是异常降级。如果桥接调用经常因为时序问题失败除了缓存队列之外还可以准备一个兜底原生 TabBar 在每次页面可见时主动从 JS 侧拉一次数据。不过这个需要 RN 侧配合提供一个 getter 方法。还有一点如果某些设备性能较差徽标数字更新可能引起布局抖动。特别是“0 - 1”从无到有这个过程组件尺寸变化会导致旁边图标位置变化。解决方式是提前预留徽标位置把徽标容器设为固定宽高用透明度切换而不是条件渲染切换。ArkUI 里用opacity控制显隐比if更平滑代价是始终占据布局空间。我在实际项目中最后的做法是TabBar 组件内部固定预留 20x20 的徽标区域默认透明度为 0需要显示时把透明度设为 1并更新数字。这样不管有没有角标图标的位置都稳定不会出现“有角标时图标偏左没角标时图标居中”这种让人崩溃的布局偏移。如果你也遇到角标导致 Tab 图标跳动的问题可以试试这个思路。它比动态计算布局可靠得多也省去了一大堆样式适配工作。最后分享一个我做 OpenHarmony 适配以来最大的体会这类跨端适配问题永远不要指望官方适配层能一步到位尤其是徽标这种“看似小功能、实则依赖完整桥接链路”的能力。把桥接机制、组件生命周期、状态管理几个核心环节搞明白以后类似的自定义原生组件联动都会从容很多。
返回列表