
做 React Native 鸿蒙化这几个月我踩得最痛的不是什么复杂页面反而是一个平时根本不起眼的评分组件。老项目里一直用的是第三方评分库在 iOS 和 Android 上跑得挺好结果换到 React Native for Harmony 这套环境上直接白屏、点不动、星星显示成方框所有问题一股脑全冒出来。后来我把这个 Rating 组件重写了一遍支持全星、半星、禁用、自定义样式同时在鸿蒙真机和模拟器上验证通过。这篇文章就把我的实现思路、代码细节、踩坑过程以及大家最常遇到的“react native 启动白屏”问题怎么排查完整分享出来。如果你也在做 React Native 鸿蒙适配或者正准备在 HarmonyOS 里做一个评分类组件可以少走不少弯路。1. 为什么是“鸿蒙定制”的 Rating而不是直接用第三方库1.1 我迁移过程中遇到的三类问题先说背景。我的项目是基于 React Native 0.72.5 做的需要用 react-native-harmony 这套鸿蒙适配方案把它跑到 HarmonyOS 设备上。一开始我天真的想法是Rating 这种组件又不复杂原样搬过去就能用。现实很快打脸。第一类问题是第三方库本身没有鸿蒙适配。很多从 npm 安装的评分组件内部用了 iOS 和 Android 的专属组件比如SafeAreaView、TouchableNativeFeedback、Platform.select里大量平台分支这些在鸿蒙适配层要么没有实现、要么行为不一致。评分库往往还会依赖一些字体图标文件比如react-native-vector-icons在鸿蒙环境里字体文件加载路径完全不一样结果就是星星直接变成方块。第二类问题是触摸事件差异。评分组件核心是一颗颗可点击的星星最常见的事件模型是TouchableOpacity包一层然后在onPress里取值。Android 和 iOS 上这套逻辑毫无问题但在鸿蒙的 RN 兼容层上我遇到的实际情况是动画不够跟手、点击区域偶尔丢失、连续快速点击评分会出现漏事件。后来我才意识到这不是组件写得不对而是对触摸事件处理的方式需要更“底层”一点。第三类问题是半星的视觉实现。第三方库实现半星大多用两张星星图片叠起来一张灰色的底一张亮色的顶顶图宽度按百分比裁切。听起来很简单但在鸿蒙上图片裁切用overflow: hidden是有效的可如果星星不是图片而是字体图标裁切对齐就会出现像素级误差。半星本来就是视觉细节要求很高的东西一旦对齐不准用户一眼就能看出来。这些问题单个拆开都不致命叠在一起就足够让人崩溃。所以最靠谱的方案就是自己维护一个专用于 React Native for Harmony 的 Rating 组件。1.2 自研与原生封装的取舍在动手之前其实还有个选择直接用 ArkUI 自带的Rating组件做底层封装通过原生桥接暴露给 RN 调用。这个方案的好处是性能好、原生交互自然坏处也明显需要写 ArkTS 原生代码、注册组件、管理桥接对纯 JS/TS 团队来说维护成本不低。尤其像评分这种轻交互组件性能敏感度其实没那么高一点响应时间差异用户根本感知不到。所以我最后选择了纯 JS/TS 方案不依赖任何第三方 UI 库只在 RN 基础组件之上自绘星星逻辑。这样做的收益很直接代码可以在 iOS、Android、Harmony 三端百分之百复用不绑定特定的字体包样式自由度极高想改颜色、尺寸、图标、背景都非常容易排查问题也简单因为整条链路都在自己手里。代价是需要自己处理触摸手势、状态管理和视觉细节。但这恰恰是我写这篇文章想要填平的坑。2. Rating 组件整体设计与 API 定义2.1 对外 API 与 Props 设计一个评分组件的 API 设计直接决定它好不好用。我参照了社区里几款主流评分库的用法同时结合鸿蒙场景做了一些调整。下面是我最终定下来的 Props 设计Prop 名类型默认值说明valuenumber0当前评分值受控模式由父组件传入maxStarsnumber5星星总数allowHalfbooleanfalse是否允许半星disabledbooleanfalse禁用评分只展示不可交互sizenumber24星星尺寸宽高spacingnumber8星星之间的间距colorstring#cccccc未激活星星颜色activeColorstring#ffa93f已激活星星颜色onPress(value: number) void-点击回调返回分值customRender(index: number, active: boolean) ReactNode-完全自定义星星节点styleStylePropViewStyle-外层容器样式受控还是非受控我倾向于跟 Input 组件一样做“半受控”父组件传value进来展示状态用户点击时通过onPress通知父组件修改。组件内部默认不做 setValue这样评分状态始终掌握在业务层手里跟表单校验、提交逻辑配合起来最舒服。maxStars不建议定死为 5因为实际业务里经常有 3 星食堂服务评分、7 星某些平台精评分的需求做成参数一劳永逸。关于间距我加了一个spacing而不是用gap原因是鸿蒙适配层对gap的支持在特定 RN 版本里会有兼容问题。用显式marginRight虽然冗余一点但兼容性最好。2.2 组件分层结构实现上我分成了两个文件Rating.tsx负责整体交互逻辑Star.tsx负责单颗星的渲染。分层的理由很简单评分组件的交互难点在“如何定位到星星”视觉难点在“如何画出一颗半星”两者拆开之后改交互不影响视图改视图不需要动交互。组件结构大致是这样Rating ├── 外层容器 View │ ├── Star第 1 颗 │ ├── Star第 2 颗 │ └── ...外层容器需要设置flexDirection: row同时处理触摸事件。Star 组件只接收一个fill参数表示这颗星的填充比例0 代表全灰1 代表全亮0.5 就是半亮。我最初尝试过把整个评分条画在一个 Canvas 里通过绝对坐标算星星位置但那样做在鸿蒙上非常容易踩到像素密度换算的坑。用单颗 Star 独立组件、各自循环渲染的方式虽然组件数量多一点但每个组件边界清晰定位计算也简单到几乎不可能出错。3. 核心功能实现全星、半星、禁用与自定义样式3.1 全星与点击评分先说最基础的场景5 颗星点击第 3 颗评分就是 3。实现时我用了响应系统Responder System而不是简单地给每颗星包TouchableOpacity。原因开头提过TouchableOpacity在鸿蒙适配层下偶尔会出现点击丢失或动画卡顿而响应系统是 RN 触摸事件的最底层机制兼容性最稳。核心代码是这样的// Rating.tsx 核心逻辑 const handleStartShouldSetResponder () true; const handleResponderRelease (event: GestureResponderEvent) { if (disabled) return; const { locationX } event.nativeEvent; const starUnit size spacing; // 计算落在第几颗星 const nearbyStar Math.floor(locationX / starUnit) 1; // 限制边界 const value Math.min(Math.max(nearbyStar, 1), maxStars); onPress?.(value); };这里有个关键细节locationX是相对于响应者容器的坐标所以拿到之后要先除以(size spacing)算出是第几颗星。Math.floor拿到的是索引加 1 才能对应到“第几颗”。整颗星渲染很简单用文字符号或图片都行。我的默认实现用fontSize: size的 Text 加 Unicode 星形字符// Star.tsx 全星渲染 const Star ({ size, color, text }) ( Text style{{ fontSize: size, lineHeight: size, color, textAlign: center, }} {text} /Text );有人会问用字符做星星会不会在鸿蒙上显示不出来确实有可能这取决于设备字体文件。所以我在customRender上留了口子业务方可以直接传自己的图片或自定义字体组件。后面会细讲。3.2 半星是怎么做出来的半星是整个组件里视觉最微妙的部分。我的方案是一颗星拆成两层。第一层是底层完整渲染一个灰星第二层是顶层渲染一个亮星但外层包一个宽度为size * fill的裁切容器overflow: hidden这样亮星只露出左侧一部分看起来就是半亮。代码大概是这样// Star.tsx 半星实现 const Star ({ size, fill, color, activeColor, starText }) { const baseText starText ?? ★; return ( View style{{ width: size, height: size }} {/* 底层灰星 */} Text style{{ fontSize: size, lineHeight: size, color }} {baseText} /Text {/* 顶层亮星宽度按 fill 比例裁切 */} View style{[ StyleSheet.absoluteFill, { width: size * fill, overflow: hidden }, ]} Text style{{ fontSize: size, lineHeight: size, color: activeColor }} {baseText} /Text /View /View ); };这里需要特别注意两点。第一StyleSheet.absoluteFill在鸿蒙 RN 上一定要放在顶层裁切容器上这样两层的文字才可能完全对齐。如果直接写position: absolute, top: 0, left: 0效果一样但absoluteFill更简洁。第二裁切容器必须显式设置width: size * fill不能只依赖内部 Text 撑开。因为overflow: hidden的生效前提是容器自身有确定尺寸否则在部分鸿蒙机型上会出现裁切失效亮星直接整颗显示。点击交互上半星模式的定位要细化到“半颗”。我改用手势locationX先算出是第几颗星再算手指在这一颗星内部的横向偏移const handleResponderRelease (event: GestureResponderEvent) { if (disabled) return; const { locationX } event.nativeEvent; const starUnit size spacing; const index Math.floor(locationX / starUnit); const offset locationX - index * starUnit; // 点击落在左半边还是右半边 const isLeft offset size / 2; let value index; if (allowHalf isLeft) { value index 0.5; } else { value index 1; } onPress?.(Math.min(Math.max(value, 0), maxStars)); };用户点第 2 颗星的左半边得到 1.5点右半边得到 2。符合直觉。3.3 禁用状态与无障碍处理禁用状态最简单也最容易做漏。我给的方案是三层防护const handleResponderGrant () { // 第一层不响应触摸 if (disabled) return; }; // 第二层容器视觉降级 View style{[ styles.container, disabled styles.disabled, style, ]}第三层是在业务侧就算组件内部都判断了父组件依然不应该收到onPress。这个由上面的if (disabled) return;保证。视觉表现上禁用分两种完全置灰或者维持评分成色不变但降低透明度。我默认采用降低透明度因为有些业务需要在禁用态仍然展示当前评分比如订单完成后的“本次服务评分 4.5 星”颜色如果全灰了用户的阅读成本会上升。无障碍方面我个人推荐不要省掉accessible和accessibilityLabel。虽然很多 RN 鸿蒙项目暂时不会专门做无障碍测试但评分这类信息对屏幕阅读器用户很重要。我建议在容器上加一个动态的accessibilityLabel评分当前值四点五星共五颗星。这样至少基础语义是通的。3.4 自定义样式的完整玩法自定义样式这块我把它拆成三个层次覆盖不同复杂度需求。第一层是少量调整通过Props改颜色和尺寸。color、activeColor、size三个参数覆盖 80% 的场景。比如商家好评率展示用金色课程评价用蓝色短视频 App 用粉色改一行 props 就够。第二层是替换星星形状。starText可以改成★、☆、\u2605也可以用网络图片或本地图片。图片模式的实现是把 Star 内部的 Text 换成 Imageconst StarImage ({ size, fill, source }) ( View style{{ width: size, height: size, overflow: hidden }} Image source{source} style{{ width: size, height: size, opacity: fill 1 ? 1 : 0.4, }} / /View );图片模式下如果想做半星就不能用这一节前面提到的遮罩文字方案了。我的做法是直接用两张图叠加底层灰图完整展示顶层亮图用一个width: size * fill的裁切容器包住。这样不管星星是字体还是图片半星逻辑都能复用同一套fill参数。第三层是完全自定义渲染节点。我在 Rating 里加了customRender它接收当前星星的索引和激活状态返回任意 ReactNode。比如有些 App 的评分图标是个大拇指或者是个厨师的卡通头像这种需求必然靠这个口子解决。我在实际项目中就接过一个需求星级用表情符号比较难统一业务方最后用了 5 张不同的图标分别对应“非常差”到“非常好”每颗星星不仅颜色不同、形状也不同。这种需求只有customRender能干净地承接。4. 在 React Native for Harmony 工程里跑起来4.1 组件引入与最小接入组件本身写完后接下来就是把它接进 React Native for Harmony 工程。这一步看起来简单实际上很多人的项目连这一关都过不去因为环境配置细节容易踩坑。我以自己用的 0.72.5 版本为例说流程。首先确认项目已经初始化好 harmony 目录npx react-native init RNHarmonyRatingDemo cd RNHarmonyRatingDemo npm install react-native-harmony npx react-native-harmony init初始化完成后用 DevEco Studio 打开工程里的harmony目录。需要注意React Native 版本和 react-native-harmony 版本不是随便配的。比如 0.72 系列的 RN 要搭配对应 0.72.x 的 react-native-harmony不能直接拿 0.73 或 0.74 的适配包硬上。具体版本对应关系去仓库的 Release 页面对一下就行。接入 Rating 组件就很简单了。把Rating.tsx和Star.tsx放进项目的src/components/Rating目录然后在业务页面里import Rating from ./src/components/Rating; Rating value{score} maxStars{5} allowHalf activeColor#ff9500 onPress{(value) setScore(value)} /如果项目是纯 TypeScript强烈建议顺便导出一份RatingProps类型。我用这个组件的时候父组件的状态类型用的是number而不是string看起来是小事但表单提交时1.5变成1.5再转回数字的啰嗦事真的太多了。4.2 启动白屏的排查与解决热搜里“react native 启动白屏”我没有具体出处但 React Native for Harmony 项目启动白屏几乎每个人都会遇到我自己也在接入评分组件时被这个问题卡了整整一个晚上。白屏的本质是应用起来了但 React Native 的 JS Bundle 没有正确加载。在鸿蒙上最常见的原因有这几个。第一个原因Metro 服务没有启动或者设备的 Metro 地址不通。开发模式下React Native 需要一个 dev server 提供 bundle。鸿蒙真机要和电脑处于同一局域网并且需要在 DevEco 里配置正确的 server host。可以在启动时通过 Metro 终端观察有没有设备来拉取 bundlenpx react-native start如果终端里一直没有出现“Building/HMR”之类的日志基本就是设备还没连上 Metro。此时优先检查网络和端口。第二个原因release 包没有打 bundle。如果直接跑 release 模式但没有提前执行 bundle 命令应用启动后找不到 JS 文件就会白屏。对应到鸿蒙工程需要在 harmony 目录里确认是否有打包好的 bundle 资源并且在EntryAbility里配置正确的 bundle 路径。第三个原因权限缺失。鸿蒙应用访问本地 Metro 服务需要一个很基础的网络权限必须在entry/src/main/module.json5里声明{ name: ohos.permission.INTERNET }没有这个权限release 包加载本地 bundle 还好debug 包走网络加载必然失败。第四个原因相对隐蔽EntryAbility的窗口时序。React Native 的容器需要等窗口创建完成后再加载 JS有些工程模板默认的加载时序在部分 API 版本的 SDK 上会出问题表现为窗口已显示但 JS 迟迟不渲染。我当时的排查方式是打开 DevEco 的日志看到类似loadBundle failed的报错才定位到时序问题。分享一个我常用的白屏排查顺序[是否 dev 模式] → [Metro 日志有没有请求记录] → [module.json5 网络权限] → [bundle 是否已生成] → [EntryAbility 时序]。按这个顺序找基本五分钟内能定位到问题。5. 踩坑实录与问题速查5.1 高频坑我整理了几个评分组件在鸿蒙适配中高频出现的坑做成速查表方便你直接对照。现象可能原因解决方法星星显示为方块或乱码字体不支持 Unicode 星形字符换系统字体、改用图片、引用自定义字体点击星星没反应依赖 TouchableOpacity改用响应系统处理半星显示整颗亮星裁切容器没有显式宽度顶层容器必须设置width: size * fill半星位置对不准间距计算漏了 spacinglocationX定位时把 spacing 加进计算评完分会抖动一下父组件 setState 后重新渲染整个 Rating用useMemo缓存星星数组页面启动后一直白屏Metro 未连接或 bundle 未生成按白屏排查顺序逐项检查第 4 个坑我专门说一句。做半星触摸定位时很多人只算了size忘了星星之间还有spacing。结果就是点第 3 颗星的右半边系统以为是第 4 颗星的左半边评分直接偏移半颗星。横竖都对不上特别烦人。后来我在代码里统一用一个变量starUnit size spacing所有定位计算都基于它这个问题就再没出现过。第 5 个坑也值得展开。评分组件本身很小但如果父组件每次都创建新的数组或函数传入Rating 内部所有 Star 都会重新渲染。业务页面如果比较重评分时会明显感觉卡顿。我的解决方式是给每个 Star 包一层React.memo并且确保onPress通过useCallback传递。const handleRatingPress useCallback((value: number) { setScore(value); }, []);这里有个残次品如果你传了customRender那就没法做标准化 memo因为自定义函数每次父组件渲染都会变化。建议业务层用useMemo包一层customRender同时 Star 的 memo 比较逻辑里把这个函数引用考虑进去。5.2 排查工具与方法最后分享几个我实测有效的调试方法。第一个方法是给 Rating 组件加一个临时的可视化调试开关。我在开发版里会导出一个debug属性开启后会在星星周围画一圈虚线边框并且在视觉层打印locationX的实时数值。定位触摸偏移问题时这个开关比任何日志都直观。第二个方法是利用 Metro 的日志。鸿蒙设备连接 Metro 后按r可以 reload按d打开 dev menu。遇到白屏问题时别急着改代码先把 Metro 日志窗口最大化看看请求状态。第三个方法是善用日志分级。不要一上来就console.log满天飞把需要排查的点位拆清楚容器布局、Touch 事件、Star 渲染、父组件回调各自打不同的前缀。我习惯用[Rating][layout]、[Rating][touch]这种标签格式搜索日志时非常高效。我目前这个评分组件已经稳定运行在商详页、订单评价页和直播评分弹窗三个场景里全星、半星、禁用、自定义表情图标、简体中文与英文双语显示都验证过。如果要做后续扩展还有两个方向我觉得值得接下去做一个是滑动评分手指在星星上横向滑动连续打分体验会更顺滑但需要把handleResponderMove加进去计算位置时要额外处理快速滑动时的消息抽样另一个是评分后触发打点或轻提示动画让用户的评分行为获得即时正向反馈。这个组件本身结构很轻这两个方向都在原思路上做增量不需要推翻重来。