使用鸿蒙 ArkUI 搞电商首页有多香?你还在堆“死板静态页”吗?
大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~
本文目录:
前言
说实话,现在要是做个电商 App 首页还写成“纯静态大图 + 一堆死列表”,真的会被用户一眼看出——“这应用有点土啊”。而且产品经理十有八九会在评审会上来一句:“能不能更灵动一点?有点氛围感那种。”
好嘛,氛围感三个字,直接把前端和客户端全安排明白了:布局要丰富、滑动要顺、加载要优雅、动画要丝滑、骨架屏要到位,性能还不能崩,最好网络不好也要撑得住。
那问题来了——用鸿蒙 ArkUI,怎么从 0 到 1 搭一套“像样”的电商首页?而不是简单堆几个 Column + List 就算完事?
这次就一步步把:Banner + 秒杀 + 商品卡片布局、瀑布流/Grid 视图、下拉刷新 & 分页、动画与骨架屏、性能优化全部串起来,做一套真正能上线的电商首页雏形。
一、整体思路先搞清:电商首页到底长啥样?
先别急着写代码,我们先来画个脑内草图。典型电商首页大概会是这样一个结构:
- 顶部:搜索栏 + 消息/购物车入口
- 轮播 Banner:运营最爱的广告位
- 秒杀/活动区:横向滑动、倒计时、小标签
- 商品列表:常见是宫格 / 瀑布流
- 下拉刷新:拉一下重新请求
- 上拉加载更多:分页加载 + 底部加载指示
在 ArkUI 里,我们可以这么拆:
- 使用
Navigation/Scaffold管一整个页面框架 - 使用
Column做整体结构 Swiper负责 BannerRow+List/Grid实现秒杀区域Grid/WaterFlow做商品流展示Refresh+ 滚动监听 做刷新 + 分页加载
先上一个“粗糙但完整”的首页骨架代码感受一下。
// eTS 示例(ArkUI 声明式范式)
@Entry
@Component
struct ECommerceHomePage {
@State bannerList: string[] = []
@State seckillList: Goods[] = []
@State goodsList: Goods[] = []
@State isRefreshing: boolean = false
@State isLoadingMore: boolean = false
@State page: number = 1
build() {
Column() {
this.buildHeader()
this.buildContent()
}
.backgroundColor('#F6F6F6')
.height('100%')
.width('100%')
}
private buildHeader() {
Row() {
Text("商城")
.fontSize(20)
.fontWeight(FontWeight.Bold)
Blank()
Image($r('app.media.icon_cart'))
.width(24)
.height(24)
.margin({ right: 12 })
}
.padding({ left: 16, right: 16, top: 12, bottom: 8 })
.backgroundColor('#FFFFFF')
}
private buildContent() {
// 这里稍后用 Refresh + Scroll/CustomScroll 实现整体内容区域
}
}
先放个壳子,后面我们往里面塞:Banner、秒杀、商品瀑布流、刷新、分页、动画、骨架屏,一步步补完。
二、Banner + 秒杀 + 商品卡片:先把“脸面工程”搭起来
1. Banner 区:Swiper 走一走
Banner 在电商首页的地位,基本等于“客厅的电视机”——不显眼还不行。
ArkUI 通常会用类似 Swiper(或轮播组件)来实现 Banner 轮播。
private buildBannerArea() {
if (this.bannerList.length === 0) {
// 简单占位
Rect()
.width('100%')
.height(160)
.backgroundColor('#EDEDED')
.borderRadius(12)
.margin({ top: 8, bottom: 8, left: 16, right: 16 })
return
}
Swiper() {
ForEach(this.bannerList, (url: string) => {
Image(url)
.objectFit(ImageFit.Cover)
.width('100%')
.height(160)
.borderRadius(12)
})
}
.index(0)
.autoPlay(true)
.interval(3000)
.indicator(true)
.margin({ top: 8, bottom: 8, left: 16, right: 16 })
}
这里 Banner 只是一个“模块”,待会儿我们会把它塞进整体内容中。
2. 秒杀区:横向滑动 + 倒计时 + 价格标签
秒杀区的特点:
- 内容少但很抢眼
- 通常是横向滑动
- 必须有“时间紧迫感”:倒计时 + 红色价格
我们可以用一个 Row + List/Scroll 来实现横向滑动。
private buildSeckillArea() {
Column() {
// 标题行
Row() {
Text("限时秒杀")
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#FF3B30')
Text(" 距结束 00:12:35")
.fontSize(12)
.fontColor('#999999')
Blank()
Text("查看更多 >")
.fontSize(12)
.fontColor('#666666')
}
.margin({ left: 16, right: 16, bottom: 8 })
// 横向商品滚动
Scroll() {
Row() {
ForEach(this.seckillList, (item: Goods) => {
this.buildSeckillItem(item)
}, (item: Goods) => item.id.toString())
}
}
.scrollable(ScrollDirection.Horizontal)
}
.padding({ top: 8, bottom: 8 })
.backgroundColor('#FFFFFF')
}
单个秒杀商品卡片:
private buildSeckillItem(item: Goods) {
Column() {
Image(item.image)
.width(80)
.height(80)
.borderRadius(8)
Text(item.title)
.fontSize(12)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width(80)
.margin({ top: 4 })
Row() {
Text(`¥${item.seckillPrice}`)
.fontSize(14)
.fontColor('#FF3B30')
Text(`¥${item.originPrice}`)
.fontSize(10)
.fontColor('#999999')
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 4 })
}
.margin({ top: 4 })
}
.margin({ left: 16, right: 8 })
}
到这一步,我们已经有:Banner + 秒杀 两大“运营位”。接下来进入重头戏——商品卡片 + Grid/瀑布流。
三、商品瀑布流 / Grid 视图:别再满屏一个一个长列表了
电商首页最核心的视觉部分,就是商品内容区。用户翻来覆去看的,也是这一块。
常见有两种展示方式:
- 规则宫格 Grid:高宽统一,整齐干净
- 瀑布流 Waterfall:高度不同,信息密度更高
我们先来一个 规则 Grid 商品展示,再说如何做“类瀑布流”。
1. Grid 商品卡片布局
我们假设用一个两列 Grid:
private buildGoodsGrid() {
Grid() {
ForEach(this.goodsList, (item: Goods) => {
this.buildGoodsCard(item)
}, (item: Goods) => item.id.toString())
}
.columnsTemplate("1fr 1fr") // 两列平分
.rowsGap(8)
.columnsGap(8)
.padding({ left: 16, right: 16, bottom: 16 })
}
商品卡片组件:
private buildGoodsCard(item: Goods) {
Column() {
Image(item.image)
.width('100%')
.aspectRatio(1) // 正方形图
.borderRadius(8)
Text(item.title)
.fontSize(14)
.maxLines(2)
.margin({ top: 4 })
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(`¥${item.price}`)
.fontSize(16)
.fontColor('#FF3B30')
.fontWeight(FontWeight.Medium)
Blank()
if (item.soldCount > 0) {
Text(`已售 ${item.soldCount}`)
.fontSize(10)
.fontColor('#999999')
}
}
.margin({ top: 4 })
}
.backgroundColor('#FFFFFF')
.borderRadius(10)
.padding(8)
}
这样一个最基础的 Grid 商品展示就完成了,干净利落。
2. 类瀑布流的实现思路
瀑布流的难点不是布局,而是每个卡片高度不同的情况下仍然保证视觉平衡。
如果框架直接提供 WaterFlow/Masonry 那是最省事的;如果没有,我们可以从两种思路入手:
- 两列 Column 手动分配数据:根据预估高度分配到左右列
- 带高度字段的数据结构:后端提供图片比例和文案长度,提前估一个高度
伪代码示意:
@State leftColumn: Goods[] = []
@State rightColumn: Goods[] = []
distributeGoodsToColumns(goods: Goods[]) {
let leftHeight = 0
let rightHeight = 0
goods.forEach(item => {
// 简单估算高度:图片高度 + 文本长度 * 系数
const estHeight = 200 + item.title.length * 2
if (leftHeight <= rightHeight) {
this.leftColumn.push(item)
leftHeight += estHeight
} else {
this.rightColumn.push(item)
rightHeight += estHeight
}
})
}
private buildWaterfall() {
Row() {
// 左列
Column() {
ForEach(this.leftColumn, (item: Goods) => {
this.buildGoodsCard(item)
})
}
.layoutWeight(1)
// 右列
Column() {
ForEach(this.rightColumn, (item: Goods) => {
this.buildGoodsCard(item)
})
}
.layoutWeight(1)
}
.padding({ left: 16, right: 16 })
}
当然,真正上线时你肯定会根据 ArkUI 当前版本内置组件情况调整实现方式,但思路是通用的:数据预分配 + 两列布局。
四、下拉刷新 & 分页加载:没有这一套就别说是“首页”
电商页面如果不能下拉刷新、不能上拉加载,用户基本会觉得你还是个 MVP。
我们来把:整体区域用 Refresh 包裹,下拉触发刷新,滚动到底触发分页加载。
1. 下拉刷新:Refresh + 状态控制
整体内容区域可以这么写:
private buildContent() {
Refresh({
refreshing: this.isRefreshing,
onRefresh: () => {
this.handleRefresh()
}
}) {
Scroll() {
Column() {
this.buildBannerArea()
this.buildSeckillArea()
this.buildGoodsGrid()
this.buildLoadMoreFooter()
}
}
}
}
刷新逻辑:
private handleRefresh() {
this.isRefreshing = true
this.page = 1
// 模拟网络请求
setTimeout(() => {
// 重新拉取数据
this.bannerList = mockBanners()
this.seckillList = mockSeckill()
this.goodsList = mockGoodsPage(1)
this.isRefreshing = false
}, 1000)
}
2. 分页加载:监听滚动到底 + 加载标记
分页加载一般有两种方式:
- 用户滑到接近底部时触发
loadMore - 点击“加载更多”按钮
我们用滑动到底自动加载 + 底部 Loading 提示。
思路:在 Scroll 上监听滚动事件,根据偏移判断何时触底。
private buildContent() {
Refresh({
refreshing: this.isRefreshing,
onRefresh: () => this.handleRefresh()
}) {
Scroll() {
Column() {
this.buildBannerArea()
this.buildSeckillArea()
this.buildGoodsGrid()
this.buildLoadMoreFooter()
}
}
.onScroll((offset: ScrollOffset) => {
this.handleScroll(offset)
})
}
}
private handleScroll(offset: ScrollOffset) {
// 伪逻辑:距离底部小于某个值就触发加载更多
if (!this.isLoadingMore && offset.end - offset.current < 200) {
this.loadMore()
}
}
private loadMore() {
this.isLoadingMore = true
this.page += 1
setTimeout(() => {
const newPageData = mockGoodsPage(this.page)
this.goodsList = this.goodsList.concat(newPageData)
this.isLoadingMore = false
}, 1200)
}
底部加载提示 UI:
private buildLoadMoreFooter() {
if (!this.isLoadingMore) {
return
}
Row() {
LoadingProgress()
.width(20)
.height(20)
.margin({ right: 8 })
Text("正在加载更多…")
.fontSize(12)
.fontColor('#888888')
}
.margin({ top: 8, bottom: 16 })
}
至此,我们已经有了一个功能完整的电商首页交互逻辑:下拉刷新 + 上拉分页。
五、动画与骨架屏:加载的时候不能“死给用户看”
你是不是也遇到过这种体验:点进一个页面,空白一大片,过两秒东西突然蹦出来——
这就是典型的“没有骨架屏 + 没配动画”的直男式加载。
我们要做的是:
- 首屏加载给出“骨架屏”占位
- 真数据回来后平滑过渡展示(淡入/位移动画)
- 异常情况也不能空
1. 骨架屏:假装数据已经在路上
简单搞一版列表骨架:灰色块 + 模拟图文结构。
@State isFirstLoading: boolean = true
private buildGoodsAreaWrapper() {
if (this.isFirstLoading) {
return this.buildSkeletonList()
} else {
return this.buildGoodsGrid() // 真正数据
}
}
private buildSkeletonList() {
Grid() {
ForEach(new Array(6).fill(0), (_, index: number) => {
this.buildSkeletonCard(index)
})
}
.columnsTemplate("1fr 1fr")
.rowsGap(8)
.columnsGap(8)
.padding({ left: 16, right: 16, bottom: 16 })
}
private buildSkeletonCard(index: number) {
Column() {
Rect()
.width('100%')
.aspectRatio(1)
.backgroundColor('#E5E5E5')
.borderRadius(8)
Rect()
.width('80%')
.height(14)
.backgroundColor('#E5E5E5')
.borderRadius(4)
.margin({ top: 8 })
Rect()
.width('60%')
.height(12)
.backgroundColor('#E5E5E5')
.borderRadius(4)
.margin({ top: 4 })
}
.backgroundColor('#FFFFFF')
.borderRadius(10)
.padding(8)
}
在数据请求完成后切换状态:
private fetchHomeData() {
this.isFirstLoading = true
// 模拟接口
setTimeout(() => {
this.bannerList = mockBanners()
this.seckillList = mockSeckill()
this.goodsList = mockGoodsPage(1)
this.isFirstLoading = false
}, 1500)
}
2. 动画加持:数据进来要“轻轻地长”出来
完全静态从骨架切成内容,会给人一种“画面突然换了一张”的割裂感。
我们可以在 goodsList 展示时配合淡入 + 轻微位移动画,让 UI 更有“呼吸感”。
一个简单思路:为商品卡片添加 opacity 和 translateY,在数据就绪时对整个区块做一次 animateTo。
@State goodsOpacity: number = 0
@State goodsTranslateY: number = 20
private onGoodsLoaded() {
// 数据已经填充 goodsList 后调用
animateTo({ duration: 400, curve: Curve.EaseOut }, () => {
this.goodsOpacity = 1
this.goodsTranslateY = 0
})
}
private buildGoodsAreaWithAnimation() {
Column() {
this.buildGoodsGrid()
}
.opacity(this.goodsOpacity)
.translate({ x: 0, y: this.goodsTranslateY })
}
这样第一屏加载的时候,商品会轻轻地从下方浮上来,视觉体验会明显更舒服。
六、性能处理与加载优化:让首页“看起来重,但跑得飞快”
电商首页是最容易变成性能黑洞的页面:大图、轮播、瀑布流、动画、骨架、刷新……
如果不注意性能,很容易就出现:
- 滑动掉帧、卡顿
- 内存暴涨
- 图片闪烁、占位不稳
下面我们从几个点讲怎么优化。
1. 图片加载与占位优化
策略:
- 所有图片都给定明确尺寸(宽/高或者宽 + 比例)
- 使用压缩后的缩略图(后端配合)
- 需要时支持渐进式加载(先小图后大图)
示例:
Image(item.image)
.width('100%')
.aspectRatio(1) // 固定比例,避免布局抖动
.borderRadius(8)
.backgroundColor('#F0F0F0') // 当图片未加载完成时的底色
2. 状态粒度控制:不要动不动就全页面刷新
很多人一开始喜欢写一个大组件,@State 全写在页面顶上,但只要一个小字段变化就会触发整棵树重 build。
对电商首页来说,这简直是性能自杀。
建议:
- 把 Banner、秒杀区、商品列表拆成独立子组件
- 哪块数据变,就只刷新那块
- 对长列使用
ForEach+ key,确保最小重建
例子:把商品卡片拆成单独组件,并用 @Prop 接入数据。
@Component
struct GoodsCard {
@Prop item: Goods
build() {
// 这里就是之前 buildGoodsCard 的内容
}
}
在 Grid 中使用:
Grid() {
ForEach(this.goodsList, (item: Goods) => {
GoodsCard({ item })
}, (item: Goods) => item.id.toString())
}
3. 分页策略:不要一次性把 200 条全塞进来
电商首页常见错误之一:
“既然首页很重要,那我干脆一口气拉 100 条商品,用户想看就滑。”
然后滑动体验就直接“去世”。
实战推荐:
- 每页 10–20 条
- 优先保证首屏加载速度
- 用户滑得快时可以“预加载”下一页
伪逻辑:
if (!this.isLoadingMore && offset.end - offset.current < 400) {
// 提前触发下一页加载
this.loadMore()
}
4. 动画适度节制:有节奏,但别乱炫技
动画确实能让 UI 看起来更高级,但“动画过度”也是一大性能/体验杀手:
- 每个元素都动
- 动画时长太长
- 动画曲线过于夸张
经验法则:
- 动画只给关键区域:首屏商品、重要入口
- 避免对大列表里的每个 item 独立执行复杂动画
- 尽量使用简单的 opacity/translate,而不是复杂滤镜
七、组合起来:一个可用的电商首页雏形
把上面所有零散的模块缝在一起,你会发现:
- 页面结构:清晰
- 用户体验:完整
- 细节体验:有动画、有骨架、不突兀
- 开发上:易维护、易扩展
你已经拥有了一个这样的首页:
- 打开 App:看到骨架屏,页面不空
- 1 秒后:Banner 渐出,秒杀区出现,商品从下方轻轻浮现
- 下拉刷新:整个页面数据更新,有动画反馈
- 往下滑:更多商品按页加载,底部有 Loading 提示
- 网络不好:至少有骨架和旧数据,不至于惨白一片
这,才像一个真正“活着”的电商首页。
结语:你是真的在做“电商首页”,还是只是“商品列表页”?
很多人写电商首页,只是换了一套皮的“商品列表”。
但对于用户来说,首页应该是:氛围、节奏、信息密度、反馈、性能全部综合后的体验结果。
用鸿蒙 ArkUI,我们可以把这些拆解成:
- 布局:Banner + 秒杀 + 商品 Grid / 瀑布流
- 交互:下拉刷新 & 分页
- 体验:动画过渡 & 骨架屏
- 工程:性能优化 & 状态拆分
如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~
更多推荐



所有评论(0)