rn_for_openharmony商城项目app实战-首页实现
本文介绍了使用React Native for OpenHarmony搭建电商App首页的过程。首先通过FakeStoreAPI获取商品数据,然后构建了包含搜索栏、Banner轮播、快捷入口和商品列表的首页结构。文章重点分享了数据封装、组件布局和交互实现的关键技巧,如API封装便于维护、ScrollView实现Banner轮播、Emoji临时替代图标等实用方法,并提供了组件状态管理和导航跳转的代码
案例开源地址:https://atomgit.com/nutpi/rn_openharmony_buy
写在前面
做电商 App 的时候,首页永远是最让人头疼的。信息太多怕用户找不到重点,信息太少又显得寒酸。这篇文章记录一下我用 React Native for OpenHarmony 搭建商城首页的过程,踩过的坑和一些心得体会。
说实话,刚开始做的时候我也没想好要放哪些东西。后来参考了几个主流电商 App,发现大家的首页结构其实差不多:搜索框、Banner、快捷入口、商品推荐。那就按这个来吧。
数据从哪来
在写页面之前,得先解决数据问题。总不能全写死吧?我找了个免费的电商 API 叫 FakeStoreAPI,不用注册直接能用,挺方便的。
const BASE_URL = 'https://fakestoreapi.com';
export const storeApi = {
getProducts: async () => {
const res = await fetch(`${BASE_URL}/products`);
return res.json();
},
};
就这么几行代码,简单粗暴。fetch 请求完直接 json() 解析返回。
为什么要单独封装 API?
你可能觉得这么简单的请求直接写在组件里不就行了?但是想想,如果哪天接口地址变了,或者要加个 token 什么的,你得满项目找哪里调用了这个接口。封装一下,改一个地方就够了。
开始写首页
先把需要的东西都 import 进来:
import React, {useEffect, useState} from 'react';
import {
View, Text, StyleSheet, ActivityIndicator,
TouchableOpacity, ScrollView, Image, Dimensions,
} from 'react-native';
import {Product} from '../types';
import {storeApi} from '../api/store';
import {ProductCard} from '../components/ProductCard';
import {useApp} from '../store/AppContext';
import {TabBar} from '../components/TabBar';
这里 Dimensions 是用来获取屏幕宽度的,后面 Banner 图片要用。useApp 是我们自己写的全局状态 hook,里面有导航方法和未读消息数量这些东西。
接下来定义一下 Banner 数据。正式项目肯定是从后端拿的,这里先写死:
const {width} = Dimensions.get('window');
const banners = [
{id: 1, image: 'https://picsum.photos/800/300?random=1', title: '新年大促'},
{id: 2, image: 'https://picsum.photos/800/300?random=2', title: '限时折扣'},
];
关于图片尺寸
Banner 图片我用的是 picsum.photos 这个占位图服务,800x300 的比例在手机上看着还行。实际项目中图片尺寸要跟设计师对好,不然可能会变形或者加载很慢。
组件主体结构
export const HomeScreen = () => {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const {navigate, unreadCount} = useApp();
三个关键的东西:
products存商品列表loading控制加载状态navigate用来跳转页面,unreadCount是未读消息数
数据加载放在 useEffect 里:
useEffect(() => {
loadProducts();
}, []);
const loadProducts = async () => {
try {
const data = await storeApi.getProducts();
setProducts(data);
} catch (error) {
console.error('Failed to load products:', error);
} finally {
setLoading(false);
}
};
finally 的妙用
不管请求成功还是失败,
finally里的代码都会执行。这样就不用在 try 和 catch 里都写一遍setLoading(false)了。这个小技巧能让代码简洁不少。
顶部搜索栏
首页顶部是搜索栏和消息入口:
<View style={styles.header}>
<TouchableOpacity style={styles.searchBar} onPress={() => navigate('search')}>
<Text style={styles.searchIcon}>🔍</Text>
<Text style={styles.searchPlaceholder}>搜索商品</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.msgBtn} onPress={() => navigate('messageList')}>
<Text style={styles.msgIcon}>🔔</Text>
{unreadCount > 0 && (
<View style={styles.badge}>
<Text style={styles.badgeText}>{unreadCount}</Text>
</View>
)}
</TouchableOpacity>
</View>
搜索栏我没用 TextInput,而是用 TouchableOpacity 做成了一个假的输入框。点击后跳转到专门的搜索页面。
为什么不直接用输入框?
主要是考虑到搜索页面可能有搜索历史、热门搜索这些功能,放在首页太挤了。而且用户点击搜索框的时候,键盘弹出来会把首页内容顶上去,体验不太好。
消息图标右上角的小红点用 unreadCount > 0 来控制显示。这个数字是从全局状态里拿的,任何地方消息被读了,这里都会自动更新。
Banner 轮播
用 ScrollView 的水平滚动来实现简易轮播:
<ScrollView
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
style={styles.bannerScroll}
>
{banners.map(banner => (
<Image key={banner.id} source={{uri: banner.image}} style={styles.banner} />
))}
</ScrollView>
pagingEnabled 这个属性很关键,加上它之后滑动会有"吸附"效果,每次正好停在一张图片上。showsHorizontalScrollIndicator={false} 把底部的滚动条藏起来,不然挺丑的。
想做自动轮播?
可以用
setInterval配合scrollTo方法实现。不过要注意在组件卸载时清除定时器,不然会内存泄漏。如果需求复杂的话,建议直接用react-native-snap-carousel这类库。
快捷入口
Banner 下面是四个快捷入口:
<View style={styles.quickActions}>
{[
{icon: '📦', label: '分类', screen: 'category'},
{icon: '🎫', label: '优惠券', screen: 'couponList'},
{icon: '❤️', label: '收藏', screen: 'favorites'},
{icon: '📋', label: '订单', screen: 'orderList'},
].map(item => (
<TouchableOpacity
key={item.label}
style={styles.quickItem}
onPress={() => navigate(item.screen as any)}
>
<Text style={styles.quickIcon}>{item.icon}</Text>
<Text style={styles.quickLabel}>{item.label}</Text>
</TouchableOpacity>
))}
</View>
用数组配置的方式来写,后面要加减入口改数组就行,不用动 JSX 结构。
Emoji 当图标靠谱吗?
开发阶段用 Emoji 很方便,但正式上线建议换成图片或者 iconfont。因为不同系统的 Emoji 长得不一样,可能会影响视觉一致性。
商品列表
终于到重头戏了,商品列表:
<View style={styles.section}>
<Text style={styles.sectionTitle}>热门商品</Text>
{loading ? (
<ActivityIndicator size="large" color="#3498db" style={styles.loader} />
) : (
<View style={styles.productGrid}>
{products.map(item => (
<ProductCard
key={item.id}
product={item}
onPress={() => navigate('productDetail', {product: item})}
/>
))}
</View>
)}
</View>
加载中显示菊花图,加载完显示商品网格。商品卡片抽成了单独的 ProductCard 组件,首页代码就不会太臃肿。
点击商品的时候,我把整个商品对象都传给了详情页。这样详情页打开就能直接显示内容,不用再等接口返回。
传对象还是传 ID?
传对象的好处是详情页秒开,用户体验好。坏处是如果商品信息更新了,详情页显示的可能是旧数据。看业务需求吧,对实时性要求高的话还是传 ID 让详情页自己请求。
样式部分
挑几个关键的样式说一下:
const styles = StyleSheet.create({
container: {flex: 1, backgroundColor: '#f5f5f5'},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingTop: 50,
paddingBottom: 12,
backgroundColor: '#3498db',
},
paddingTop: 50 是给状态栏留的空间。不同机型状态栏高度不一样,正式项目建议用 react-native-safe-area-context 来动态获取。
searchBar: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff',
borderRadius: 20,
paddingHorizontal: 16,
paddingVertical: 10,
},
搜索栏用 flex: 1 占据剩余空间,borderRadius: 20 做成胶囊形状。
productGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between'
},
商品网格用 flexWrap: 'wrap' 实现自动换行。justifyContent: 'space-between' 让两列商品左右分开,中间自动留出间距。
为什么不用 FlatList?
首页商品数量不多的话,用
map渲染问题不大。如果商品很多(比如上百个),建议换成FlatList,它有虚拟列表优化,性能更好。
最后别忘了 TabBar
<TabBar />
底部导航栏是个公共组件,首页、分类、购物车、我的这四个页面都要用。
总结一下
首页看着内容多,其实拆开来每个部分都不复杂。关键是要想清楚数据怎么流动、组件怎么拆分。
几个值得注意的点:
- API 封装一下,别到处写 fetch
- 加载状态要处理,别让用户干等着
- 搜索框做成假的,跳转到专门的搜索页
- 商品卡片抽成组件,保持首页代码简洁
- 样式注意适配状态栏高度
下一篇写分类页面,敬请期待。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐




所有评论(0)