ARTICLE DETAIL

资讯详情

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

鸿蒙开发也能用React Native?从零搭建积分明细页全流程

鸿蒙开发也能用React Native?从零搭建积分明细页全流程 1. 为什么是 React Native写给刚接触鸿蒙开发的你先说说我的判断。过去这一年多身边问鸿蒙开发的人明显变多了但大家的第一反应往往很一致是不是得从 ArkTS ArkUI 从头学一套毕竟鸿蒙生态和 Android/iOS 都不一样了API 不同、UI 框架不同、开发工具也不同想想就劝退。我自己也经历过这个心理阶段。后来真正动手做才发现事情没有想象的那么非此即彼。鸿蒙应用开发现在有几条路可以走比如纯 ArkTS 原生开发、套 WebView 壳、用 Flutter 或者 React Native 做跨端适配。其中 React Native 这条路我一直比较关注因为它有个实打实的优势前端和移动端已有的 JS/TS 技术栈可以直接复用团队的现有能力不用推翻重来。拿这篇文章要聊的例子来说——一个静态积分明细页面。它看起来很简单就是标题栏加一个可滚动的列表每条明细包含类型、时间、积分变化最多再配一个状态标签。但真正从零搭建这套工程理解 React Native 在鸿蒙上到底怎么跑起来的中间涉及的环境配置、工程结构、组件选型、样式适配每一步都有不少新手容易踩的坑。这篇文章面向的是完全没接触过 React Native 鸿蒙开发的小白。我会从环境准备开始讲不走多余的弯路直接给你一条已验证过的路线然后完整拆解积分明细页面的实现过程。你跟着这篇文章走完能做出一个在鸿蒙模拟器或真机上正常运行、界面清晰、滚动流畅的静态页面同时也知道下一步该往哪个方向进阶。先说个重要的事实帮你建立正确的认知框架目前鸿蒙原生并不直接支持 RN 官方框架我们需要使用的是社区维护的 react-native-harmony 这个适配方案。它是将 React Native 运行时桥接到鸿蒙的 OpenHarmony 能力上让 JS 代码最终渲染成鸿蒙原生组件。这套方案也是目前 RN 上鸿蒙最常见、最成熟的做法。2. 开工前的环境准备这步最容易卡住新手我见过太多人把时间耗在环境上还没写一行页面代码就放弃了。所以帮你把工具链一口气捋清楚。简单说RN 鸿蒙开发需要四样东西Node.js、DevEco Studio、OpenHarmony SDK、以及 react-native-harmony 框架本身。2.1 工具清单与版本匹配工具建议版本用途说明Node.js18 或 20 LTS运行 npm 命令、打包 JS 代码OpenJDK17DevEco Studio 编译鸿蒙工程时依赖DevEco Studio5.x 及以上鸿蒙应用的 IDE编译和签名OHOS SDKAPI 12 或更高对应 HarmonyOS NEXT 版本react-native-harmony0.72.x 系列RN 到鸿蒙的桥接核心包这套版本组合是我目前实测下来最顺的尽量别混搭。尤其是 Node 版本太老或太新都可能导致 npm 依赖安装时编译报错。2.2 逐步安装流程第一件事安装 Node.js。去官网下载 LTS 版本装上就行没什么特别的门道。装完在命令行里确认一下node -v npm -v两个命令都能打出版本号说明 OK。第二件事安装 DevEco Studio。这个工具是鸿蒙开发的官方 IDE下载安装后第一次启动会让你配置 SDK 路径。如果只是跑模拟器用默认配置就行如果要连真机还需要去开发者后台申请签名证书这个后面单独说。第三步是初始化 RN 工程。这里有个坑想先帮你避开不要直接使用react-native-community/cli的默认初始化命令因为默认生成的工程只支持 Android 和 iOS没有鸿蒙的目录结构。正确做法是基于 react-native-harmony 提供的模板或示例工程来做改造。我建议直接拉一个官方示例工程作为起点。在命令行里执行git clone https://github.com/react-native-ohos/react-native-harmony.git cd react-native-harmony npm install依赖装完以后你需要在 DevEco Studio 里打开harmony子目录然后等待 IDE 完成同步。这一步首次会比较慢因为要下载鸿蒙 SDK 的组件并索引工程耐心等就行。2.3 处理最容易发生的环境报错新手在这步常见的报错有三个提前给你对应方案报错hvigor 编译失败大概率是 SDK 版本与工程要求的 API 版本不匹配。去 DevEco Studio 的 SDK Manager 里确认是否安装了 API 12 及以上的 SDK。报错npm install 过程中出现 node-gyp 错误检查 Node 版本是否过高或过低卸载后换 18 LTS 重试。报错找不到 ohos/hypium之类的缺失依赖不是你的问题是工程没同步完整在 DevEco Studio 里执行一次 File Sync and Refresh Project 即可。环境这块的关键心得是不要把时间浪费在没完没了的“试试其他版本”上面。当前的工具链已经相对成熟认准一条主版本路径走到底比反复折腾省心得多。3. 静态积分明细页面的结构设计先想清楚数据长什么样很多新手拿到页面第一反应是“开写”。但我建议先花五分钟想清楚两件事一、页面需要展示什么数据二、数据如何组织和流经组件。这两件事决定了代码的整体架构。3.1 数据模型设计积分明细页面的核心数据是一条一条的积分记录。每条记录至少要包含这几个字段id唯一标识列表项渲染时用于区分记录后面讲 key 时会重点解释type积分类型比如“每日签到”“消费奖励”“兑换商品”等time发生时间展示为可读的字符串amount积分变化值正数是增加负数是减少status状态比如“已完成”“处理中”“已过期”静态页面里它决定标签样式合理姿势是这样定义一个 TypeScript 接口interface PointRecord { id: string; type: string; time: string; amount: number; status: completed | pending | expired; }不要把字段定义成意思含糊的字符串数组。接口清楚后面写渲染逻辑、调样式都会顺畅很多。3.2 静态数据的组织方式既然做的是静态页面数据直接写在组件外层就行const records: PointRecord[] [ { id: 1, type: 每日签到, time: 2024-11-20 08:30, amount: 5, status: completed }, { id: 2, type: 消费奖励, time: 2024-11-18 14:22, amount: 20, status: completed }, { id: 3, type: 兑换话费, time: 2024-11-15 09:10, amount: -50, status: completed }, { id: 4, type: 限时活动, time: 2024-11-12 19:45, amount: 15, status: pending }, { id: 5, type: 积分过期, time: 2024-11-01 00:00, amount: -10, status: expired }, ];这里故意包含正数、负数和不同状态的记录方便后面验证列表渲染和样式适配是否全面。3.3 列表组件选型为什么不用 ScrollView页面是列表形态市面上有两个组件能实现ScrollView和FlatList。新手经常会纠结用哪个。直觉上ScrollView最简单因为它就是个可以滚动的容器把所有子视图放进去就行。但它的致命问题是不管有多少条数据它都会一次性全部渲染。数据少无所谓数据一旦到几百上千条就会出现明显的卡顿因为内存占用和渲染压力都在线性上涨。FlatList则不同它是基于虚拟列表机制实现的只渲染当前屏幕可见的若干条数据其他数据在滚动时按需渲染。用大白话说它不会一次把全家都叫来开会只喊几个进门滚动时再换一批进来。这在大数据量场景下性能远好于 ScrollView。我们的积分明细页虽然现在是静态数据但列表中后期必然要接真实接口数据量不可控所以直接用FlatList是更靠谱的选择也免得以后返工。3.4 组件的层级设计页面按功能拆成三个组件职责分开PointsDetailPage页面容器管理数据源和列表状态PointsListItem单条积分的展示卡片接收一条记录作为输入PointsHeader页面顶部标题栏包含返回按钮和页面标题这样一个简单的拆分好处是以后接接口、加下拉刷新、加跳转逻辑时改动范围可控不会一动全动。4. 完整代码实现从组件拆分到渲染细节环境通了结构想清楚了接下来就是把代码一层层写出来。我会按一个可直接运行的顺序从入口页面开始逐步构建。4.1 页面容器与数据源首先在工程里新建PointsDetailPage.tsx写页面主组件import React from react; import { View, Text, FlatList, StyleSheet } from react-native; import PointsListItem from ./PointsListItem; import PointsHeader from ./PointsHeader; interface PointRecord { id: string; type: string; time: string; amount: number; status: completed | pending | expired; } const records: PointRecord[] [ { id: 1, type: 每日签到, time: 2024-11-20 08:30, amount: 5, status: completed }, { id: 2, type: 消费奖励, time: 2024-11-18 14:22, amount: 20, status: completed }, { id: 3, type: 兑换话费, time: 2024-11-15 09:10, amount: -50, status: completed }, { id: 4, type: 限时活动, time: 2024-11-12 19:45, amount: 15, status: pending }, { id: 5, type: 积分过期, time: 2024-11-01 00:00, amount: -10, status: expired }, ]; export default function PointsDetailPage() { const [data] React.useStatePointRecord[](records); return ( View style{styles.container} PointsHeader title积分明细 / FlatList data{data} keyExtractor{(item) item.id} renderItem{({ item }) PointsListItem record{item} /} contentContainerStyle{styles.listContent} / /View ); } const styles StyleSheet.create({ container: { flex: 1, backgroundColor: #F5F7FA, }, listContent: { paddingHorizontal: 16, paddingTop: 8, paddingBottom: 24, }, });这里有一个细节值得关注就是keyExtractor。FlatList渲染列表项时必须让每项有一个稳定且唯一的 key内部用它来追踪哪些项需要更新、哪些项需要卸载。如果数据没有稳定的 id 字段也可以退而求其次用索引当 key但这会影响列表项的状态保持。在真实项目中接口返回的数据一般都有唯一 id直接用就好。4.2 顶部标题栏组件接着创建PointsHeader.tsx。别小看这个标题栏它是页面第一眼看到的部分在鸿蒙 RN 环境中实现时更要注意 padding 和安全区域的处理。import React from react; import { View, Text, StyleSheet, TouchableOpacity } from react-native; interface Props { title: string; onBack?: () void; } export default function PointsHeader({ title, onBack }: Props) { return ( View style{styles.header} TouchableOpacity style{styles.backButton} onPress{onBack} Text style{styles.backText}返回/Text /TouchableOpacity Text style{styles.title}{title}/Text View style{styles.placeholder} / /View ); } const styles StyleSheet.create({ header: { height: 44, flexDirection: row, alignItems: center, justifyContent: space-between, paddingHorizontal: 16, backgroundColor: #FFFFFF, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: #E5E5E5, }, backButton: { width: 50, justifyContent: center, }, backText: { fontSize: 16, color: #333333, }, title: { fontSize: 17, fontWeight: 600, color: #1A1A1A, }, placeholder: { width: 50, }, });这里的布局用的是典型的两侧元素宽度对称、中间标题居中的方式。左侧按钮占 50 宽度右侧放一个同样宽度的空白占位标题才真正居中。不这么做标题会偏向一侧。在鸿蒙上文字渲染引擎对fontWeight: 600的支持是没问题的无需担心。4.3 列表项组件的渲染逻辑重点部分来了PointsListItem.tsx。它接收一条积分记录渲染出一条完整明细。这一部分的交互逻辑不复杂但视觉和结构上的细节比较多。import React from react; import { View, Text, StyleSheet } from react-native; interface PointRecord { id: string; type: string; time: string; amount: number; status: completed | pending | expired; } interface Props { record: PointRecord; } const statusTextMap { completed: 已完成, pending: 处理中, expired: 已过期, }; const statusColorMap { completed: #10B26A, pending: #F5A623, expired: #B0B0B0, }; export default function PointsListItem({ record }: Props) { const { type, time, amount, status } record; const isPositive amount 0; return ( View style{styles.item} View style{styles.info} Text style{styles.typeText}{type}/Text Text style{styles.timeText}{time}/Text /View View style{styles.right} Text style{[styles.amountText, !isPositive styles.negative]} {isPositive ? ${amount} : amount} /Text Text style{[styles.statusText, { color: statusColorMap[status] }]} {statusTextMap[status]} /Text /View /View ); } const styles StyleSheet.create({ item: { flexDirection: row, alignItems: center, justifyContent: space-between, backgroundColor: #FFFFFF, borderRadius: 10, paddingVertical: 14, paddingHorizontal: 16, marginBottom: 10, }, info: { flex: 1, }, typeText: { fontSize: 15, fontWeight: 500, color: #1A1A1A, }, timeText: { marginTop: 4, fontSize: 12, color: #999999, }, right: { alignItems: flex-end, }, amountText: { fontSize: 18, fontWeight: 700, color: #333333, }, negative: { color: #E85C41, }, statusText: { marginTop: 4, fontSize: 11, }, });有几个细节值得展开讲讲它们对最终效果影响挺大。第一是数量字段的正负号处理。我用了isPositive这个布尔变量判断正数时在数字前拼号负数直接用原值并通过negative样式把负数的颜色改成偏红色。这个视觉区分符合用户对积分增减的直觉——增加让人觉得舒服减少则要有警示感。颜色这里我没用纯红色而是选了偏暖的#E85C41在白色卡片上看着不那么刺眼。第二是右侧区域的布局。我把积分数量和状态标签都放在右侧并右对齐左侧放类型和时间。这样用户在快速浏览时眼光在右侧就能捕捉到“我得了多少分、处于什么状态”效率更高。第三是列表项的间距和圆角。为什么要给每一项加固定的marginBottom和borderRadius因为卡片化设计能让用户在视觉上更清晰地区分每条记录同时减少长列表的压迫感。白色卡片配浅灰背景色#F5F7FA整体干净符合积分页这类功能型页面的调性。4.4 给列表补齐分割感如果觉得卡片之间的间距在视觉上还不够有“分隔感”可以再加一个分隔线方案。把列表项的样式从独立卡片改成平铺列表用分隔线区分每条记录是一种更传统的做法。实现方式很简单借助FlatList的ItemSeparatorComponent属性FlatList data{data} keyExtractor{(item) item.id} renderItem{({ item }) PointsListItem record{item} /} ItemSeparatorComponent{() View style{styles.separator} /} /separator: { height: StyleSheet.hairlineWidth, backgroundColor: #E5E5E5, marginLeft: 16, },StyleSheet.hairlineWidth在鸿蒙上代表一个物理像素的最小线条粗细用它做的分割线不会出现 1 像素线在某些高分辨率屏幕上过粗的问题。这个细节你可能不会注意但在高分屏上对比效果很明显。4.5 整合完成后的页面结构把三个文件放在一起后整体结构是页面容器View包裹背景色设置顶部标题栏PointsHeader列表FlatList每一项由PointsListItem渲染这样的代码跑起来不需要任何额外的导航库在工程里只要把这个页面组件指定为入口页面即可。如果你用的是 DevEco Studio 的示例工程找到对应页面的入口文件替换渲染的根组件就行。5. 样式细节与视觉还原把页面做得像设计稿很多新手的页面“功能没问题但难看”问题几乎都出在样式细节上。样式不仅仅是“颜色对不对”更是信息层级、间距节奏、交互反馈的综合结果。5.1 字体层级与视觉节奏积分明细页面的信息层级有三层积分类型最重要15 号字、中等字重、近黑色积分数量第二重要18 号字、加粗时间与状态辅助信息12/11 号字、灰色这样安排字号和字重用户第一眼看到的一定是类型和积分数量符合页面的使用场景。如果所有文字都用同样大小同样粗细用户就得逐字去读体验会差很多。字号的差值不用太大3 到 6 个像素的阶梯就够用差值太大会显得页面很“散”。5.2 间距规范宁可统一不要随意我建议页面内固定两套间距水平间距用 16垂直间距用 8 或 10 的倍数。看上面的代码列表的paddingHorizontal是 16列表项的paddingHorizontal也是 16类型标题和时间之间是 4 到 8列表项之间的间距是 10。统一间距会让页面产生一种稳定的节奏感这在设计上有个专门名词叫“栅格系统”简单说就是让一切元素都处在有规律的位置上看着就舒服。新手常见的做法是“随手调数字”这里 12那里 14这里又 18最终出来的效果往往很混乱。我的建议是从一开始就规定页面间距只允许使用 4、8、12、16、24最多再加个 32。超出这个范围要警惕重新考虑是否真的需要。5.3 克制的设计不做过多的装饰元素静态页面最容易犯的另一个毛病是拼命加装饰边框、阴影、渐变、动画全塞进去。积分明细页属于工具型页面用户来是为了快速查看信息而不是欣赏视觉效果。我的做法是背景色用低饱和度的浅灰而不是纯白让白色卡片有“浮起来”的感觉卡片圆角 10不会太圆显得轻浮也不会直角显得生硬阴影能不加就不加加了也只用极浅的投影避免在低端设备上渲染性能打折所有可点元素都保证有足够的触控区域不把点击区域缩成刚好文字大小这些原则不止适用于这个积分页面也适用于你后面做的很多鸿蒙 RN 页面。5.4 一个容易忽略的适配点安全区域你在模拟器上跑页面时可能觉得没问题但一旦上了带挖孔或刘海屏的真机顶部标题栏就可能撞上“岛”。这不是 RN 特有的问题而是所有移动开发都要面对的。简单处理方式是给标题栏加上合理的高度的同时用鸿蒙系统的 API 或者 RN 的 SafeArea 相关组件来避开安全区域。在 react-native-harmony 环境中你可以引入react-native-safe-area-context来统一处理。它能获取到设备的安全区域边距让你精确控制标题栏高度import { SafeAreaView } from react-native-safe-area-context;把页面的根容器从View换成SafeAreaView内部内容就不会被状态栏顶住了。这个库在鸿蒙的适配工作中也能正常工作我有在真机上验证过效果稳定。6. 常见错误与排查思路把踩过的坑一次讲清新手第一次跑通页面通常不会一帆风顺这里我盘点几个高频问题附带完整的排查链路你按顺序检查基本能解决问题。6.1 白屏问题排查链路首先是最常见也最让人绝望的模拟器打开后一片白什么也没渲染。相关热词里“react native 启动白屏”被频繁搜索说明遇到的人非常多。排查链路我按步骤走第一步检查 Metro 服务是否在运行。Metro 是 RN 的 JS 打包服务它不启动设备上就无法加载 JS 代码。在工程根目录执行npm start看到终端显示Metro waiting on ...说明服务正常。第二步确认鸿蒙侧是否连接上了 Metro。在 DevEco Studio 的日志面板里找Loading JS bundle之类的信息如果出现连接失败检查电脑和模拟器/真机是否在同一网络环境。模拟器一般没问题真机必须保证设备与电脑可互相连通。第三步如果 Metro 正常、连接也正常但页面仍白屏看日志是否有 JS 异常。常见的异常原因包括组件文件路径写错导入时找不到模块使用了鸿蒙侧暂不支持的 RN 组件或第三方库StyleSheet中有非法值导致样式解析失败但没抛明显错误针对第三步有个笨但有效的方法把页面内容逐段注释每注一段跑一次很快就能定位是哪段代码惹的祸。这种方法看着原始但比对着日志猜效率高得多。6.2 keyExtractor 警告与列表显示错乱当你运行列表但控制台出现 key 相关警告或者列表滚动后内容错乱几乎可以确定是 key 的问题。错误通常长这样Each child in a list should have a unique key prop。我们用了keyExtractor{(item) item.id}理论上不会出问题。但如果你后续把列表改成由状态动态生成或者数据源来自接口且接口返回的 id 不唯一就要注意了。id 不唯一时列表项的状态会出现错乱典型症状是滚动后某些卡片的状态对不上数据。排查方式是打印数据源逐一检查 id 是否唯一。接口数据里如果确实没有唯一 id可以组合多个字段生成唯一标识比如type _ time或者干脆在拉取数据后前端自己补一个自增 id。6.3 样式不生效或尺寸异常在鸿蒙 RN 上大部分 RN 样式属性是支持的但少数属性仍存在差异。比如position: absolute配合top/left/right/bottom可以实现绝对定位但某些场景下需要给父容器显式设置宽高否则定位可能会失效百分比的设置在不同容器层级下表现可能有差异尽量用固定值或 flex 布局实现elevationAndroid 阴影属性在鸿蒙上不完全等效阴影建议优先用boxShadow属性实现如果样式表现和设计稿对不上建议先在简化环境测试单个属性。比如先做一个只有一个View的页面设置期望样式跑通了再放回原页面。这样能快速确定是属性兼容问题还是层级覆盖问题。6.4 底部内容被遮挡列表底部最后一项内容可能被导航栏或底部安全区域遮挡这在有虚拟键盘、有底部横条的设备上很常见。解决办法在FlatList的contentContainerStyle里加上底部留白listContent: { paddingBottom: 32, }如果还有遮挡可以考虑用SafeAreaView包一层。这里多留一点 padding 不会有人嫌弃但内容被遮挡一定有人骂。7. 从静态页面到真实业务下一步该怎么改静态页面跑通只是第一步。如果你真的要用这个页面做业务有几件事避不开提前了解一下可以少走弯路。7.1 接入真实数据源在PointsDetailPage中目前的数据是硬编码数组。接接口后通常需要改成这种模式const [data, setData] React.useStatePointRecord[]([]); const [loading, setLoading] React.useState(false); React.useEffect(() { fetchPoints(); }, []); async function fetchPoints() { setLoading(true); try { const result await request(/api/points/detail); setData(result.list); } finally { setLoading(false); } }同时可以用FlatList自带的能力补上三种状态ListEmptyComponent空态提示比如“暂无积分记录”顶部下拉刷新onRefresh配合refreshing属性上拉加载更多onEndReached配合onEndReachedThreshold但要注意避免在加载中重复触发通常加一个isFetchingMore的状态锁7.2 页面间跳转与参数传递页面在当前 demo 中是独立组件真实应用里会有入口比如点击积分商城再进入积分明细。鸿蒙 RN 应用中的路由处理一般有两种方式一是使用社区适配好的 RN 导航库二是通过鸿蒙原生的页面路由能力。如果你用的是 react-native-harmony 脚手架建议先查一下官方推荐的导航方案不同版本支持的库不完全一样。无论用哪种方式核心要掌握的是“传参与接参”。以跳转到积分明细为例跳转时可能需要携带用户 id 或会员等级等参数目标页面接收后根据参数请求对应数据。7.3 列表性能的进一步优化FlatList本身已经帮我们干了大部分活但数据量大到一定程度后还能再做几件事getItemLayout如果每行高度固定提供这个属性能让列表跳过计算步骤滚动更顺滑memo包裹列表项组件避免父组件状态变化时所有列表项都重新渲染图片懒加载如果列表项里有图片用react-native的Image组件自带lazy能力或配合第三方组件控制加载时机积分明细页一般不会到几千条但这套优化思路对做商品列表、资讯列表一样适用掌握了好处很大。7.4 多端一致性验证做跨平台开发最忌讳的是一边跑通就宣布结束。React Native 本身是跨端方案同一套代码在规范环境、OpenHarmony 设备、真机上可能会有细微差异尤其是字体渲染、安全区域、触摸反馈这些感知明显的地方。建议你从一开始就养成真机预览的习惯模拟器上的视觉感受和真实设备存在差异。早期发现问题改造成本小很多。写在最后一些实际操作层面的经验跟着做完整套流程这个静态积分明细页面应该已经可以稳定运行了。最后再分享几条我做鸿蒙 RN 开发时觉得比较重要的实际经验写在正文之外算是一点自己的总结。第一环境配好以后固定一个组合不要再频繁升级工具链。React Native 和鸿蒙的适配层目前迭代比较快有些升级会带来不兼容但项目没有必要追新。稳定压倒一切。第二遇到奇怪的 UI 问题先不要怀疑 RN 框架本身先从自己的样式和组件结构排查。大部分问题都是因为层级、布局属性理解不到位。排查时多用小字、注释法不要抱着大段代码猜。第三把页面拆分成小组件这个习惯值得保持。项目会越做越大组件职责清晰改动时能救你一命。积分列表项、标题栏这种小组件看似不起眼但它们是整个页面最常被调整的部分。第四多看官方示例工程。react-native-harmony 的仓库里有很多例子遇到不熟悉的组件先去搜示例比自己盲试效率高得多。我刚入门时有段时间把文档翻来覆去看效果反而不如直接对着例子敲一遍来得实在。希望这篇内容能帮你把第一步迈出去。跨平台开发这条路上最大的门槛从来不是技术难度而是“觉得麻烦”的心理障碍。按步骤走跑通一个小页面成就感会让你继续往下走很久。
返回列表