ARTICLE DETAIL

资讯详情

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

React Native 鸿蒙适配实战:自研 Rating 评分组件避坑指南

React Native 鸿蒙适配实战:自研 Rating 评分组件避坑指南 最近在做 React Native for Harmony 的鸿蒙化改造时一个绕不开的坎就是评分组件。我们线上项目原本用的是社区 rating 库实现星级评分结果适配 HarmonyOS NEXT 后不是首屏白屏就是星星点了没反应。折腾几天后我索性基于 React Native 自己写了一套 Rating 评分组件支持全星、半星、禁用态和自定义样式最后在鸿蒙真机上稳定跑通。这篇记录就是这次踩坑的全过程把组件的触摸算法、状态设计、样式体系和联调排障一起写完给正在做 React Native 鸿蒙适配的朋友一份能直接参考的方案。1. 社区评分库在 Harmony 适配中的兼容性缺口1.1 依赖链移植带来的隐性成本先说结论不是社区评分库不好而是它们的依赖链在 Harmony 的 RN 适配层上并不完整。常见的评分库内部往往引用了Pressability、react-native-svg、react-native-animatable等模块在 Android 和 iOS 上这些都是经过大量验证的基础设施但到了 React Native for Harmony 这套实现里原生模块的覆盖度还远达不到双端水平。举个例子有些库需要读取屏幕宽度来计算星间距底层依赖Dimensions有些库需要手势响应底层依赖一整套 Touchable 交互链。前者在基础桥接里基本可用后者也通常能跑。最怕的是依赖了 SVG 原生组件这类的重型模块在 Harmony 适配初期经常缺少对应的 JSI 实现最终表现就是星星区域空白、点击无响应严重时直接把整个 RN 页面带崩。我这次遇到的情况就非常典型页面在 Android 上一切正常切到鸿蒙真机后评分区域整块空白Log 里报的是某个原生模块找不到。顺着依赖树往上查问题根本不在“评分”本身而在评分库底层引用的 SVG 渲染组件并没有在鸿蒙侧完成适配。这种问题不是改一行配置能解决的只能换实现方案。1.2 自研方案的三个判断标准决定自研之前我给自己列了三条判断标准如果都满足就值得动手。第一需求边界是否清晰。我们的业务只需要全星、半星、禁用态和自定义样式不涉及复杂的动画曲线和模糊评分算法。这类需求用纯 JS 完全能覆盖不需要触碰任何原生代码。第二依赖树是否干净。自研组件如果能做到零原生依赖图标也用自有图片资源那么整个组件就是一段纯粹的 JavaScript随 JS bundle 一起加载天然规避了鸿蒙适配层原生模块缺失的问题。这是最大的安全边际。第三维护成本是否可控。评分组件的逻辑并不复杂一份两百行左右的组件代码比追着第三方库等 Harmony 支持进度要省心得多。与其每次升级适配层都要重新验证第三方库不如把这块攥在自己手里。事实证明这三条判断标准是有效的。自研组件替换上线之后评分区域白屏和不可点击的问题从根上消失后续改星星样式也只动一个文件。2. 全星与半星的触摸换算核心算法与实现2.1 落点坐标到分值的换算公式评分组件的第一个核心问题是手指按在星星行上如何换算成具体分值。我的做法是只在星星行容器上做响应而不是给每颗星单独绑定点击事件。容器总宽度W maxStars * starSize (maxStars - 1) * spacing然后用触点相对容器左侧的偏移量x计算比例。核心代码如下const containerWidth maxStars * starSize (maxStars - 1) * spacing; function getValueByX(x: number): number { // x 是触点相对星星行容器左上角的横向偏移 const ratio Math.max(0, Math.min(1, x / containerWidth)); const raw ratio * maxStars; if (allowHalfStar) { // 向上取整到最近的 0.5 return Math.ceil(raw * 2) / 2; } // 全星模式点在第几颗星范围内就亮第几颗 return Math.min(maxStars, Math.ceil(raw)); }这里有个容易混淆的点为什么全星用Math.ceil(raw)而不是Math.round(raw)因为当手指落在第 4 颗星的左半边时raw可能只有 3.4如果取四舍五入会得到 3用户明明点在第四颗星上结果只亮了第三颗体感上像慢了半拍。Math.ceil(raw)的语义是“手指落在第 K 颗星的区间内就选中第 K 颗”配合星星的视觉边界反馈更直接。半星模式稍微特殊。Math.ceil(raw * 2) / 2等同于把分值轴切成 0.5 的刻度然后向上对齐。手指点在某个星星的左半侧选中半星点在右半侧或超过右边界选中整星。这个公式和用户的直觉非常一致实测下来不需要额外修正。2.2 星星渲染与半星显示的两种方案半星显示有两种常见实现。第一种是准备三张图空星、半星、全星根据分值在渲染时做判断切换。第二种是我推荐的裁剪叠加方案用空星打底上面盖一层宽度按比例裁切的全星图。function StarItem({ fillRatio, size, spacing, emptyIcon, filledIcon }) { // fillRatio 为 0~1表示当前这颗星被点亮的比例 return ( View style{{ width: size, height: size, marginRight: spacing }} Image source{emptyIcon} style{{ width: size, height: size }} / View style{{ position: absolute, left: 0, top: 0, width: size * fillRatio, height: size, overflow: hidden, }} Image source{filledIcon} style{{ width: size, height: size }} / /View /View ); }父组件渲染时第index颗星的点亮比例是Math.max(0, Math.min(1, value - index))。比如value 3.5那么前 3 颗星的fillRatio都是 1第 4 颗是 0.5第 5 颗是 0。裁剪叠加的优势在于只需要两张图而且天然支持任意精度——以后业务如果要求 0.1 粒度连算法都不用改。三图切换的方案在视觉上更保险不会出现裁剪层在某些真机上边缘发虚的情况但多维护一张图灵活性也差一些。我最终选了裁剪叠加具体原因写在后面的联调章节里。2.3 滑动手势与连续评分的实现细节很多评分组件是点哪亮哪但移动端用户已经习惯了“滑动调分”的交互。为了实现连续滑动我在星星容器根节点上挂载了响应器而不是给每颗星挂TouchableView pointerEvents{disabled ? none : auto} onStartShouldSetResponder{() !disabled} onMoveShouldSetResponder{() !disabled} onResponderMove{(e) handleChange(e.nativeEvent.locationX)} onResponderRelease{(e) handleChange(e.nativeEvent.locationX)} {renderStars()} /View这里用的是 RN 最基础的响应器属性没必要把PanResponder再包一层。评分区域通常只有几十到百来像素JS 层直接处理事件完全没有性能压力引入多余封装反而让触摸响应链变复杂。在 Harmony 真机上locationX的语义和双端一致可以直接用。不过建议在初始化时用onLayout缓存一次容器宽度避免每次触摸都重复计算containerWidth。如果后续遇到locationX偏移问题备选方案是用pageX - 容器左上角绝对坐标来兜底但实测鸿蒙适配层没有出现这个偏差。另外提醒一句如果应用要支持 RTL 布局x / containerWidth需要翻转为(containerWidth - x) / containerWidth否则阿拉伯语等从右往左阅读的语系下评分方向会反过来。3. 禁用态的三层处理交互、视觉与无障碍3.1 交互层的封闭与复原禁用态最容易想到的实现是“不渲染 Touchable”但评分组件用的是响应器系统禁用时不仅要让手势失效还要让整个星星区域从命中测试里摘除。最稳的做法是给根容器设置pointerEventsnone。这个属性会把整个子树从触摸命中树里摘掉无论是星星行还是外层包着的 View都不会再响应点击。实测在 React Native for Harmony 适配层里是支持的。如果某些早期适配版本对该属性支持不完整退化方案是用条件渲染disabled时渲染一个不挂任何事件处理器的包裹View本质上就是把交互从事件树上剥掉。这里有一个细节禁用状态必须是可逆的。评分组件经常用于“提交后锁定分数”的场景但也可能用于“编辑已提交的评分”比如订单评价之后允许追评修改。所以不要在disabled分支里直接卸载组件或清空内部状态只封闭事件入口渲染结构保持一致恢复属性后评分能力立即回归。3.2 视觉层的灰化与半星正确显示禁用不等于把组件变成一张死图。如果disabled时直接把整个容器opacity调到 0.3半星状态下用户会分不清哪个星是半亮的。我的处理方式是优先把填充色替换成disabledColor而不是整体降透明度。具体到代码就是在渲染星星时判断disabled如果是禁用态用disabledColor覆盖filledColor。因为裁剪叠加方案本身是动态算填充比例的所以半星的形状结构不会被破坏只是颜色变灰可读性远好于全容器蒙一层透明度。如果没有配置disabledColor再退回opacity: 0.5。另外给容器加上一个不可点击的灰色背景层也能明显增强禁用态的视觉区分。这个背景层放在星星下方用绝对定位铺满注意别挡住星星本身。3.3 无障碍标签与 Harmony 适配现状评分控件在无障碍场景里属于典型的可调整组件。iOS 上有increment/decrement动作Android 上有类似 seek 类的无障碍操作RN 通过accessibilityActions暴露这些能力。不过 Harmony 适配层对这套动作协议的支持到什么程度我建议不要做假设。保守且有效的做法是把评分容器的accessible设为true用动态生成的accessibilityLabel描述当前状态比如“评分 3.5最高 5 星”再通过accessibilityHint提示可滑动调整。实测鸿蒙设备上的屏幕朗读对 RN 组件的兼容还在完善中这部分必须在真机上验证一遍至少保证 label 能读出来。如果团队对无障碍要求严格还可以考虑在禁用态额外报一句“评分已锁定”帮助使用屏幕朗读的用户区分当前状态。这个成本很低收益却很明显别忽略。4. 自定义样式 API 与渲染链路的落地4.1 props 如何拆分才不至于臃肿自定义样式不是简单加几个参数而是要设计层次感。我最终定义的组件接口大概长这样export interface RatingProps { value: number; maxStars?: number; starSize?: number; spacing?: number; allowHalfStar?: boolean; disabled?: boolean; emptyColor?: string; filledColor?: string; disabledColor?: string; emptyIcon?: ImageSourcePropType; filledIcon?: ImageSourcePropType; renderStar?: (params: { index: number; fillRatio: number; disabled: boolean }) React.ReactNode; onChange?: (next: number) void; containerStyle?: StylePropViewStyle; starContainerStyle?: StylePropViewStyle; }参数按四类划分数值类value、maxStars、allowHalfStar、外观类尺寸、间距、颜色、渲染覆盖类图标图片、自定义渲染函数、行为类disabled、onChange。不建议暴露二三十个细粒度样式参数比如“星星圆角半径”“阴影颜色”这种。参数过多会让调用方选择困难也让组件内部样式判断逻辑变得复杂。高频参数控制在十个以内再提供一个renderStar完全覆盖入口剩下的交给调用方自己按需组合。4.2 三种图标方案的取舍图标是跨端渲染最容易翻车的地方。我对比过三种方案方案优点不足内置 PNG 双图空星/全星无额外依赖渲染稳定尺寸可控需要设计出图换主题需换资源文字字形★改色方便零额外资源字体差异大鸿蒙默认字体下可能显示不一致SVG 或自定义绘制矢量化清晰度高依赖 react-native-svg 等库Harmony 适配风险高最终我选了 PNG 双图方案。首先是因为裁剪叠加只需要空星和全星两张图其次是零原生依赖适配风险最低。如果你的设计稿需要特殊形状的星星比如圆角星、渐变星直接让设计导出透明底 PNG。另外强烈建议不要用 emoji 表情做星星。同一颗 emoji 在不同设备字体里渲染差异极大尤其是鸿蒙默认字体和 iOS 的系统字体同一个字符可能在两台机器上长得完全不一样评分组件这种高频视觉元素完全不适合交给字体控制。4.3 渲染优化与样式优先级评分组件通常只有 5 颗星性能瓶颈几乎不存在但有一个容易忽略的细节如果renderStar是外部传入的函数每次手指移动触发重渲染时5 颗星星的子组件都会重新执行这个函数。外部函数若没有用useCallback包裹会白白增加不少 diff 开销。建议把每颗星抽成React.memo的StarItemprops 只传index、fillRatio、尺寸、图标这些原始值。样式优先级也需要提前理清避免调用方和组件内部各自覆盖时打架。我的顺序是renderStar完全接管星星渲染此时图标和颜色 props 全部失效设置了emptyIcon/filledIcon时使用图片渲染否则用纯色渲染此时emptyColor/filledColor生效containerStyle和starContainerStyle在最后合并外部样式能覆盖默认值但不会覆盖前三层里已经确定的渲染内容。提示合并样式时注意StyleSheet.compose的传参顺序外部样式放后边才会覆盖默认样式。这个顺序如果反了调用方传进来的颜色会被默认值盖掉排查起来很费时间。5. 真机联调从启动白屏到评分组件可用的排查记录5.1 白屏问题出现时的第一反应清单React Native for Harmony 集成初期最常见的现象就是启动白屏原生壳进来了页面却一片空白。相信很多朋友都搜过“react native 启动白屏”这类关键词我这次也中招了。遇到白屏先按顺序排除几个最基础的问题Metro 是否在运行。开发调试模式下RN 页面靠 Metro 提供 bundleMetro 没起来一切免谈。bundle URL 是否配置正确。调试模式默认从localhost:8081拉取真机需要通过端口映射把请求转发到开发机。入口 Ability 是否调用了正确的 RN 加载路径。不同版本的适配层加载 API 名称有差异拼错一个方法名也能白屏。有没有查看运行日志。白屏不是“没有日志”而是日志被忽略。5.2 一步步定位到根因的排查链路我这边排查白屏的完整链路是这样的。首先是确认端口映射鸿蒙真机没有 adb用的是 hdchdc reverse tcp:8081 tcp:8081 hdc shell hilog | grep -i reactnative第一条命令把设备的 8081 端口反向映射到开发机让真机能访问到本地 Metro第二条命令实时过滤 React Native 相关日志。接下来看日志输出。如果看到Loading from Metro: http://localhost:8081/index.bundle?platformharmony之后一直卡住说明 bundle 拉取过程出问题优先查端口映射和局域网连通性。如果日志显示 bundle 已经加载完成但页面仍然空白那就是 JS 侧执行时抛了异常。JS 异常导致的白屏常见于第三方库依赖了鸿蒙适配层尚未实现的原生模块。排查方法用二分注释法先把评分组件从页面中注释掉如果页面恢复再逐步把依赖加回来。我这次就是这么定位到 SVG 原生模块缺失的——不是评分组件本身出错而是它背后引用的底层库在鸿蒙侧根本无法加载导致整个渲染进程报错。定位到根因后替换成自研组件的收益立刻体现出来组件不再依赖任何原生模块白屏问题从源头上消失。如果你的白屏也是第三方库原生依赖引发的光调端口和配 Metro 没用必须从依赖链上做减法。5.3 评分组件联调中的其他兼容问题组件能显示之后还有几个和 Harmony 相关的兼容细节值得记录。首先是裁剪层问题。裁剪叠加方案依赖overflow: hidden做半星裁切但在某台鸿蒙真机上偶发不裁切的情况看起来就像半星位置多出一截。实测解决方法是给裁剪层加一个 1px 的透明边框或者干脆预生成一张半星图片用于禁用态展示规避裁切不确定性。其次是图片资源路径。Harmony 的 RN 打包对require(./star.png)的解析方式和 Android 的 res 目录机制不同。模拟器上显示正常不能作为验收标准一定要在真机上验证一次图标加载。我遇到过一次模拟器正常但真机图标全黑的问题最后是资源放到打包系统要求的 asset 目录下才解决。第三是间距边界。spacing设为 0 时星星之间没有间隙连续滑动评分时坐标计算没有问题但视觉上贴太近容易误触。默认值我建议给 4至少保证半星裁剪后相邻星之间还有肉眼可辨的边界。最后分享一个实际体会一开始我也想过等社区库更新 Harmony 支持但评分这种组件逻辑简单、状态模型清晰自研成本实际上就一两天。把它收进公司公共组件库之后后续鸿蒙设备适配、深浅色主题、自定义星星造型都只需要改这一个文件。如果你也在 React Native for Harmony 上被评分组件折腾过希望这篇记录能帮你少走一段弯路。
返回列表