最近用 React Native 搞鸿蒙跨平台开发,第一个练手任务就是做一个积分明细页面。需求很简单:纯静态、不接接口、不搞分页,就是拿一个列表把积分记录展示出来。但真正动手才发现,这样一个"入门级"页面,踩坑的点一点不少。
先说结论:RN 的代码是真的能跑在鸿蒙上的,通过社区适配方案把 React Native 引擎装进 HarmonyOS 的壳工程里,写 JS/TS 的页面最终会打包成一个 hap 安装包。这个技术路线的好处在于,同一套列表逻辑和样式代码,将来可以平行复用到 Android 和 iOS,对个人开发者或者小团队来说,省掉的不只是一点点重复劳动。这篇就把我从环境搭建到页面落地的完整过程记录下来,适合那种刚摸到鸿蒙开发门槛、又不想一上来就啃 ArkTS 的小白。
1. 需求拆解:一个积分明细页面到底在练什么
1.1 为什么要用 RN 写鸿蒙页面
很多人对"React Native 跑鸿蒙"的第一反应是不太可信。RN 不是 Meta 主导的跨平台方案吗,怎么又跟鸿蒙扯上关系了?实际上,鸿蒙 NEXT 系统发布之后,原生的 Android APK 已经没法直接跑了,存量 App 总不能全部用 ArkTS 重写一遍,于是通过 React Native 的 C++ 核心层去适配鸿蒙就成了一条务实路线。目前社区主推的方向是 react-native-harmony 这类适配方案,它把 RN 的运行时、渲染管线、触摸事件都接到了鸿蒙的 ArkUI 框架上,JS 侧写的组件最终会映射成鸿蒙原生组件去渲染。
选择这条路的核心原因有三个:第一,前端或者 RN 开发者不需要重新学一遍 ArkTS 的状态管理和组件模型,学习成本低很多;第二,现有 RN 工程里大量的业务组件、npm 库、工具函数可以直接复用,不至于推倒重来;第三,鸿蒙、Android、iOS 三端共用一套业务代码,积分明细这类"用户资产"页面天然适合这种模式。
当然,这条路线也有代价。鸿蒙适配的成熟度还不像 Android/iOS 那么高,个别原生模块可能没有鸿蒙实现,或者部分第三方 RN 库无法直接使用。但对于纯 UI 展示型的页面,比如积分明细、订单列表、消息通知,完全够用。
1.2 把"静态页面"的边界划清楚
新手最容易犯的错,是拿到一个需求恨不得把所有功能一次性做完。这里我给自己定的边界非常明确:只做静态积分明细页面,数据写死在代码里,不接后端接口;不做下拉刷新和上拉加载;不做点击跳转。所有交互逻辑后续再说。
那这个页面练的是什么?核心是两个能力:一是列表数据的组织能力,包括如何定义 TypeScript 接口、如何构造 mock 数据、如何把数据渲染成 UI;二是列表组件 FlatList 的灵活运用,包括 header、分隔线、空态、性能优化相关的 props。这两块能力是任何列表页面的通用基础,静态页面练好了,后面接接口只是把写死的数据源替换成网络请求而已。
页面本身的视觉结构也顺便梳理清楚。顶部放一张积分卡片,显示当前总积分和本月已获得、已使用;中间是筛选标签区域,虽然静态页面不做筛选逻辑,但留出 UI 占位;下方就是积分明细列表,每条记录包含时间、变动类型、积分正负值和变动后余额。这样一个页面做完,既覆盖了列表开发的基本功,又保持了视觉上的完整度。
2. 环境准备:RN 鸿蒙开发的最低配置清单
2.1 需要装哪几样东西
RN 鸿蒙开发的环境和纯 RN 开发不太一样,主要区别在于多了一个鸿蒙壳工程。我从零开始配过一遍,按重要性排个清单:
| 依赖 | 版本建议 | 作用 |
|---|---|---|
| Node.js | 18 LTS 或以上 | 运行 npm、Metro 打包器 |
| DevEco Studio | 5.x 以上 | 鸿蒙原生工程的 IDE,负责编译 hap 包 |
| HarmonyOS SDK | API 12 以上 | 鸿蒙的系统 SDK,随 DevEco 下载 |
| react-native | 0.72 以上 | RN 框架本体 |
| react-native-harmony 适配包 | 与 RN 版本对应 | RN 与鸿蒙之间的适配层 |
提醒一句,DevEco Studio 必须装,因为 RN 在鸿蒙上不能独立运行,它必须嵌入到一个鸿蒙原生的壳工程里。这个壳工程本质上是一个标准的 HarmonyOS 应用工程,里面通过特定方式加载 RN 的 JS Bundle 并渲染页面。所以我们的开发工作流其实是"RN 写逻辑和 UI,鸿蒙工程负责编译打包"。
2.2 初始化工程与接入鸿蒙壳工程
初始化有两种方式:一是自己手动把 RN 工程和鸿蒙工程拼在一起,二是用现成的模板工程跑一遍。对于小白,我强烈建议先找社区现成的 react-native-harmony 模板仓库,克隆下来再改,而不是从头搭建。原因很简单,适配层的版本匹配关系比较多,RN 版本、适配包版本、鸿蒙 SDK 版本三者必须对齐,稍有偏差编译就过不去。手动搭的话踩坑成本太高。
具体接法大概分几步:
- 用 npx react-native init 初始化一个 RN 项目,或者直接使用模板仓库里的 RN 目录。
- 在 package.json 里添加 react-native-harmony 相关的依赖包,然后 npm install。
- 用 DevEco Studio 打开鸿蒙壳工程目录,确认 SDK 版本和 RN 适配版本匹配。
- 把 RN 项目的入口组件通过 AppRegistry.registerComponent 注册,并在鸿蒙侧配置好要加载的组件名和 Bundle 路径。
有一点值得单独说:RN 页面在鸿蒙上的表现形式并不是独立的 Activity,而是看你注册的组件挂在哪个页面。在这个积分明细项目里,我在鸿蒙壳工程中加了一个入口页面,这个页面启动时会加载 RN 运行时并渲染注册的 PointsDetail 组件。理解了这种宿主关系之后,后续调试时思路会清晰很多——RN 只是壳工程里面运行的一层引擎。
3. 页面实现:从数据模型到 FlatList 列表
3.1 先定数据结构
写列表页面的第一步不是敲代码,而是先想清楚数据长什么样。积分明细的每一条记录,我定义了这样一个 TypeScript 接口:
interface PointRecord { id: string; createTime: string; // 变动时间 typeName: string; // 变动类型:签到、消费抵扣、兑换、退款 point: number; // 变动积分,正数表示增加,负数表示扣减 balanceAfter: number; // 变动后余额 }字段不多,但每一个都有讲究。id 是列表渲染的 key,必须唯一且稳定,不能用数组下标。createTime 虽然目前只是展示字符串,但后续如果要做时间分组,最好在后端返回标准时间戳,前端再格式化。point 用 number 而不是 string,是因为后续可能要参与数值计算和排序。balanceAfter 单独存一个字段,比用户在 UI 上自己心算余额要靠谱得多。
mock 数据我给了 12 条,覆盖增加、扣减、退款等多种类型,让列表看起来更真实。比如"每日签到 +5""购物消费抵扣 -500""积分兑换 -800""订单退款 +200",时间从近到远排列。这样页面一出来就能直观看到正负积分在视觉上的不同展示效果。
3.2 用 FlatList 渲染明细列表
列表组件我选了 FlatList,不是 ScrollView 加 map 遍历。差异在数据量上来之后会非常明显:FlatList 是虚拟列表,只渲染屏幕内可见的节点,滑动时动态回收和创建;而 ScrollView 会把所有子节点一次性全部渲染。积分明细少的时候看不出差别,一旦上万条,ScrollView 直接卡到怀疑人生。
基础代码长这样:
import React from 'react'; import { FlatList, StyleSheet, View } from 'react-native'; const PointsDetail: React.FC = () => { const renderItem = ({ item }: { item: PointRecord }) => ( <View style={styles.itemContainer}> {/* 这里渲染单条明细 */} </View> ); return ( <View style={styles.page}> <FlatList data={MOCK_POINTS} renderItem={renderItem} keyExtractor={(item) => item.id} ItemSeparatorComponent={() => <View style={styles.separator} />} showsVerticalScrollIndicator={false} /> </View> ); };这里每个 props 都有它的用途。keyExtractor 告诉 FlatList 每条数据的唯一标识,避免渲染错乱;ItemSeparatorComponent 在每两条之间渲染一条分隔线,省得在 renderItem 里手动加 borderBottom,样式更可控;showsVerticalScrollIndicator 关掉滚动条,视觉上更清爽,如果你觉得没有滚动条会让用户困惑也可以保留。
单条明细的 UI 结构我分成三块:左侧是类型名称和变动时间,中间是空的或者放状态标签,右侧是变动积分数额和变动后余额。这个布局看起来简单,但用 flex 布局写的时候要注意主轴对齐,时间文本和类型名称如果长度不确定,需要设置 numberOfLines 防止换行撑乱列表。
3.3 让列表"看起来"像个专业的明细页
数据能渲染出来只是第一步,距离"能拿出手"还差得远。这个页面的视觉细节我花了大量时间打磨,每一条都是实际开发中总结出来的:
积分卡片是整个页面的视觉重心。纯白卡片配圆角,放在浅灰背景上,卡片左上角显示"我的积分",中间用大字号展示总积分数值。大数字建议用 tabular-nums 类似的等宽数字特性,这样数字跳动时不会左右抖动。Android 和鸿蒙端对这个支持不一定完全一致,实在不行可以在数字部分用统一的字重和字号来缓解。
正负积分的颜色区分要果断。增加用品牌色或者绿色系,扣减用灰色或者橙色系,不要两个都用深色,否则一眼看过去分不清是加还是减。我这里增加用了 #16A34A,扣减用了 #64748B,并在数值前面显式带上 +/- 符号,双保险。
时间格式也别偷懒。mock 数据里时间是一个完整字符串,但 UI 上最好拆成两行:日期占一行,精确时间用小号灰色字放后面。这种细节会让页面信息层级更清晰。用户扫一眼先看到"12月20日",需要精确到几点几分的时候再看小字。
列表头部 ListHeaderComponent 也值得一提。积分卡片和筛选标签我都是通过 FlatList 的 ListHeaderComponent 渲染的,而不是把 FlatList 放进一个 ScrollView 里。这样整个列表的滚动是一体的,而且 FlatList 的虚拟化能力不会被破坏。如果你用嵌套滚动容器,等到将来加下拉刷新时会非常痛苦。
4. 调试、打包与真机运行
4.1 Metro 联调:开发阶段的正确姿势
静态页面也需要频繁调试,每次改代码都重新打包整个 hap 是不现实的。开发阶段推荐的方式是开 Metro 服务,让鸿蒙应用从 Metro 加载最新的 JS Bundle,改完代码保存就能看到效果。
启动顺序是这样的:
npm startMetro 起来之后,确认控制台打印的端口是 8081。然后用 DevEco Studio 把鸿蒙工程跑到模拟器或者真机上,应用启动时会尝试连接 Metro。关键点来了:如果应用找不到 Metro,就会白屏。这个白屏问题简直是我见过最多的问题,后面单独展开讲。
连接 Metro 的原理其实跟浏览器访问网页有点像。应用启动的时候会去拉一份 JS Bundle,开发者模式下这份 Bundle 由 Metro 实时提供,每次保存代码 Metro 都会增量编译。看到终端里出现 "Bundling complete" 且绿色字体的日志,说明这次编译成功,接下来去设备上看效果就行。
4.2 打正式 Bundle 并生成 hap 包
开发调试没问题之后,需要把 JS 代码打包成一份独立的 Bundle 文件,随鸿蒙工程一起编译成 hap。这一步是为了保证应用不依赖 Metro,离线也能运行。
打包命令大概是这样的:
npx react-native bundle --platform harmony --entry-file index.js --bundle-output ./harmony/bundle/index.jsbundle --assets-dest ./harmony/bundle/assets打完包后,确认 Bundle 文件被放进了鸿蒙工程对应的资源目录,再用 DevEco Studio 的 Build 功能打出 hap。这里有个特别容易出错的点:Bundle 路径必须和鸿蒙侧代码里配置的加载路径一致,否则打出来的包在真机上依然会白屏。我的习惯是把 bundle 输出到固定目录后,再去鸿蒙工程源码里搜一下 "jsbundle" 关键词,核对路径是不是同一个。
真机安装 hap 用的是 DevEco Studio 自带的签名配置,个人开发直接用自动签名就行。装上之后如果应用能正常打开并且看到积分列表,那这套打包流程就算通了。
5. 常见问题与排查思路实录
5.1 启动白屏:谁没遇到过呢
白屏这个问题,几乎每个 RN 鸿蒙开发者都会遇到,而且原因五花八门。我帮大家按出现频率排了个序:
| 现象 | 大概率原因 | 排查方法 |
|---|---|---|
| 连接 Metro 时白屏 | Metro 没启动,或端口不对 | 看终端 Metro 日志,确认 8081 端口 |
| 打正式包白屏 | Bundle 路径配置不一致 | 搜索 jsbundle 关键字核对路径 |
| 启动后闪一下白屏然后退出 | 原生模块没注册或 so 库缺失 | 看 DevEco 的 Log 窗口报错信息 |
| 模拟器白屏但真机正常 | 模拟器不支持当前 SDK 特性 | 换真机测试 |
排查白屏的核心思路是分清"JS 没加载"和"JS 有报错"。如果 JS 没加载,通常 Metro 日志和鸿蒙引擎日志里没有任何 JS 输出;如果 JS 有报错,Log 里会看到异常堆栈。我遇到过一种情况,是某个第三方库在鸿蒙适配层不支持,导致整个 Bundle 执行中断,表现也是白屏。这种只能靠注释代码逐步排查,把可疑库一个个注释掉,直到定位到问题。
5.2 列表滚动卡顿与渲染异常
静态列表滚动卡顿,一般不是组件性能问题,而是踩了渲染的坑。最常见的是在 renderItem 里写了复杂的内联函数或者创建了大型对象,导致每次渲染都要重新计算。我把每一条明细的渲染封装成纯函数式组件,props 不变就不会重复渲染,卡顿问题自然消失。
另一个问题是列表更新时闪烁。这里务必保证 keyExtractor 返回的 id 稳定,不要用 index。如果用了 index 作为 key,列表中间某条数据变化时,React 会认为整条列表都变了,导致所有行重新渲染,闪烁和崩溃都是这么来的。数据多的时候这种问题尤其明显。
5.3 真机安装失败与闪退
hap 安装失败最常见原因是签名问题和系统版本不兼容。DevEco Studio 的自动签名一般能解决签名问题,但如果你的手机系统版本太旧,低于工程配置的最低系统版本,安装会直接报错。项目里把 compatibleSdkVersion 设置得比最低可用版本低一点,这样能覆盖更多真机。
闪退问题就要看日志了。鸿蒙设备的 Log 信息里会输出 RN 引擎相关的崩溃日志,重点关注 "ReactNative" 和 "ArkTS" 两个 tag。我踩过的一个坑是,鸿蒙工程里忘了配置某个 native module,导致 JS 侧调用原生方法时崩溃。对于静态页面而言,这种崩溃不常见,但一旦遇到,就得回头检查 react-native-harmony 的初始化配置。
除此之外,样式在鸿蒙上的兼容性也值得单独说一嘴。部分 RN 样式属性在鸿蒙端支持得不够完整,比如某些情况下 boxShadow 的写法需要调整成 border + 背景色的组合方案。遇到样式不生效,不要死磕属性本身,换个实现思路往往更快。
6. 加个彩蛋:这个页面还能怎么往前走
写到这里,这个静态积分明细页面的核心内容已经全部完成了。按照惯例,最后聊一点我自己做技术选型时的真实感受。
如果你以前写过 Vue 里的列表,或者用过小程序里的 scroll-view,再回到 RN 的 FlatList 会有一点点不适应——它把"数据驱动列表"这件事做得非常彻底,所有 UI 表现都围绕着 data 和 renderItem 这两个核心转。理解了这一点,后面不管接接口也好,加下拉刷新也好,都是在往这套机制里填充能力。
我个人建议是,静态页面做完之后别急着丢,试着做三件事:第一,把 mock 数据改成从本地 JSON 文件加载,模拟一下异步请求;第二,给 FlatList 加上下拉刷新和空态占位,这几乎是生产环境必有的功能;第三,抽一个通用的列表组件,把这次写的样式和渲染逻辑沉淀下来,下次做订单列表的时候直接套用。这三件事做完,你对列表开发的理解会彻底不一样。