第22集:uni-app 列表开发
章节概览
在本章节中,我们将学习 uni-app 列表开发的核心知识点和最佳实践。列表是移动应用中最常见的界面元素之一,用于展示大量数据。通过本章节的学习,您将掌握列表组件的使用、下拉刷新和上拉加载的实现、数据分页加载的方法,以及如何构建高性能的列表界面。
核心知识点
1. 列表组件使用
uni-app 提供了多种列表相关的组件,包括:
- view:基础容器组件,可用于构建自定义列表
- scroll-view:可滚动视图组件,支持横向和纵向滚动
- list:高性能列表组件,适用于长列表
- recycle-view:回收式列表组件,优化内存使用
- cell:列表项组件,与 list 组件配合使用
2. 下拉刷新
下拉刷新是列表开发中的常用功能,用于刷新列表数据,包括:
- onPullDownRefresh:页面生命周期函数,用于监听下拉刷新事件
- uni.startPullDownRefresh:主动触发下拉刷新
- uni.stopPullDownRefresh:停止下拉刷新
- 自定义下拉刷新:实现自定义的下拉刷新动画和逻辑
3. 上拉加载
上拉加载是列表开发中的常用功能,用于加载更多数据,包括:
- onReachBottom:页面生命周期函数,用于监听触底事件
- 上拉加载状态管理:处理加载中、加载完成、加载失败等状态
- 数据分页:实现数据的分页加载和管理
- 节流处理:避免频繁触发上拉加载事件
4. 列表性能优化
列表性能优化是确保列表流畅运行的重要环节,包括:
- 虚拟列表:只渲染可视区域内的列表项,优化内存使用
- 列表项复用:复用列表项组件,减少 DOM 操作
- 数据缓存:缓存列表数据,减少重复请求
- 图片懒加载:延迟加载可视区域外的图片
- 避免复杂计算:减少列表渲染时的复杂计算
实用案例
实现商品列表页
我们将实现一个完整的商品列表页,包括:
- 商品列表展示:使用自定义列表项展示商品信息
- 下拉刷新:刷新商品列表数据
- 上拉加载:加载更多商品数据
- 性能优化:实现图片懒加载和列表项复用
- 交互体验:添加加载状态和错误提示
代码示例
1. 基础列表实现
使用 view 组件构建列表
<template>
<view class="basic-list">
<view v-for="(item, index) in listData" :key="item.id" class="list-item">
<image :src="item.image" :alt="item.title" class="item-image" />
<view class="item-content">
<text class="item-title">{{ item.title }}</text>
<text class="item-desc">{{ item.description }}</text>
<text class="item-price">¥{{ item.price }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
listData: [
{
id: 1,
title: '商品1',
description: '这是商品1的描述',
price: 99.9,
image: 'https://via.placeholder.com/100'
},
{
id: 2,
title: '商品2',
description: '这是商品2的描述',
price: 199.9,
image: 'https://via.placeholder.com/100'
},
{
id: 3,
title: '商品3',
description: '这是商品3的描述',
price: 299.9,
image: 'https://via.placeholder.com/100'
}
]
}
}
}
</script>
<style scoped>
.basic-list {
padding: 20rpx;
}
.list-item {
display: flex;
padding: 20rpx;
margin-bottom: 20rpx;
background-color: #fff;
border-radius: 10rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.item-image {
width: 150rpx;
height: 150rpx;
border-radius: 5rpx;
}
.item-content {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.item-title {
font-size: 32rpx;
font-weight: 500;
margin-bottom: 10rpx;
}
.item-desc {
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-price {
font-size: 30rpx;
font-weight: bold;
color: #ff4d4f;
}
</style>使用 scroll-view 组件实现可滚动列表
<template>
<scroll-view
class="scroll-list"
scroll-y
:scroll-top="scrollTop"
@scroll="handleScroll"
@scrolltolower="handleScrollToLower"
>
<view v-for="(item, index) in listData" :key="item.id" class="list-item">
<image :src="item.image" :alt="item.title" class="item-image" />
<view class="item-content">
<text class="item-title">{{ item.title }}</text>
<text class="item-desc">{{ item.description }}</text>
<text class="item-price">¥{{ item.price }}</text>
</view>
</view>
<view v-if="loading" class="loading">加载中...</view>
<view v-else-if="!hasMore" class="no-more">没有更多数据了</view>
</scroll-view>
</template>
<script>
export default {
data() {
return {
listData: [],
scrollTop: 0,
page: 1,
pageSize: 10,
loading: false,
hasMore: true
}
},
onLoad() {
this.loadData()
},
methods: {
// 加载数据
async loadData() {
if (this.loading) return
this.loading = true
try {
// 模拟网络请求
await new Promise(resolve => setTimeout(resolve, 1000))
const newData = Array.from({ length: this.pageSize }, (_, i) => ({
id: (this.page - 1) * this.pageSize + i + 1,
title: `商品${(this.page - 1) * this.pageSize + i + 1}`,
description: `这是商品${(this.page - 1) * this.pageSize + i + 1}的描述`,
price: Math.random() * 1000,
image: 'https://via.placeholder.com/100'
}))
if (this.page === 1) {
this.listData = newData
} else {
this.listData = [...this.listData, ...newData]
}
// 模拟没有更多数据
if (this.page >= 3) {
this.hasMore = false
}
this.page++
} catch (error) {
console.error('加载数据失败:', error)
} finally {
this.loading = false
}
},
// 滚动事件
handleScroll(e) {
this.scrollTop = e.detail.scrollTop
},
// 滚动到底部
handleScrollToLower() {
if (!this.loading && this.hasMore) {
this.loadData()
}
}
}
}
</script>
<style scoped>
.scroll-list {
height: 100vh;
padding: 20rpx;
}
.list-item {
display: flex;
padding: 20rpx;
margin-bottom: 20rpx;
background-color: #fff;
border-radius: 10rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.item-image {
width: 150rpx;
height: 150rpx;
border-radius: 5rpx;
}
.item-content {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.item-title {
font-size: 32rpx;
font-weight: 500;
margin-bottom: 10rpx;
}
.item-desc {
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-price {
font-size: 30rpx;
font-weight: bold;
color: #ff4d4f;
}
.loading,
.no-more {
text-align: center;
padding: 20rpx;
color: #999;
font-size: 28rpx;
}
</style>2. 下拉刷新和上拉加载
实现下拉刷新
<template>
<view class="pull-refresh-list">
<view v-for="(item, index) in listData" :key="item.id" class="list-item">
<image :src="item.image" :alt="item.title" class="item-image" />
<view class="item-content">
<text class="item-title">{{ item.title }}</text>
<text class="item-desc">{{ item.description }}</text>
<text class="item-price">¥{{ item.price }}</text>
</view>
</view>
<view v-if="loading" class="loading">加载中...</view>
<view v-else-if="!hasMore" class="no-more">没有更多数据了</view>
</view>
</template>
<script>
export default {
data() {
return {
listData: [],
page: 1,
pageSize: 10,
loading: false,
hasMore: true
}
},
onLoad() {
this.loadData()
},
// 下拉刷新
onPullDownRefresh() {
console.log('触发下拉刷新')
this.page = 1
this.hasMore = true
this.loadData().finally(() => {
uni.stopPullDownRefresh()
})
},
// 上拉加载
onReachBottom() {
console.log('触发上拉加载')
if (!this.loading && this.hasMore) {
this.loadData()
}
},
methods: {
// 加载数据
async loadData() {
if (this.loading) return
this.loading = true
try {
// 模拟网络请求
await new Promise(resolve => setTimeout(resolve, 1000))
const newData = Array.from({ length: this.pageSize }, (_, i) => ({
id: (this.page - 1) * this.pageSize + i + 1,
title: `商品${(this.page - 1) * this.pageSize + i + 1}`,
description: `这是商品${(this.page - 1) * this.pageSize + i + 1}的描述`,
price: Math.random() * 1000,
image: 'https://via.placeholder.com/100'
}))
if (this.page === 1) {
this.listData = newData
} else {
this.listData = [...this.listData, ...newData]
}
// 模拟没有更多数据
if (this.page >= 3) {
this.hasMore = false
}
this.page++
} catch (error) {
console.error('加载数据失败:', error)
uni.showToast({
title: '加载失败,请稍后重试',
icon: 'none'
})
} finally {
this.loading = false
}
}
}
}
</script>
<style scoped>
.pull-refresh-list {
padding: 20rpx;
}
.list-item {
display: flex;
padding: 20rpx;
margin-bottom: 20rpx;
background-color: #fff;
border-radius: 10rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.item-image {
width: 150rpx;
height: 150rpx;
border-radius: 5rpx;
}
.item-content {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.item-title {
font-size: 32rpx;
font-weight: 500;
margin-bottom: 10rpx;
}
.item-desc {
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-price {
font-size: 30rpx;
font-weight: bold;
color: #ff4d4f;
}
.loading,
.no-more {
text-align: center;
padding: 20rpx;
color: #999;
font-size: 28rpx;
}
</style>实现上拉加载更多
<template>
<view class="load-more-list">
<view v-for="(item, index) in listData" :key="item.id" class="list-item">
<image :src="item.image" :alt="item.title" class="item-image" />
<view class="item-content">
<text class="item-title">{{ item.title }}</text>
<text class="item-desc">{{ item.description }}</text>
<text class="item-price">¥{{ item.price }}</text>
</view>
</view>
<view v-if="loading" class="loading">
<uni-icons type="spinner" size="24" color="#007aff" animation="spin"></uni-icons>
<text>加载中...</text>
</view>
<view v-else-if="!hasMore" class="no-more">没有更多数据了</view>
<view v-else-if="loadError" class="load-error" @click="loadData">
<text>加载失败,点击重试</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
listData: [],
page: 1,
pageSize: 10,
loading: false,
loadError: false,
hasMore: true
}
},
onLoad() {
this.loadData()
},
onReachBottom() {
if (!this.loading && this.hasMore && !this.loadError) {
this.loadData()
}
},
methods: {
// 加载数据
async loadData() {
if (this.loading) return
this.loading = true
this.loadError = false
try {
// 模拟网络请求
await new Promise(resolve => setTimeout(resolve, 1000))
// 模拟加载失败
if (Math.random() > 0.8) {
throw new Error('网络错误')
}
const newData = Array.from({ length: this.pageSize }, (_, i) => ({
id: (this.page - 1) * this.pageSize + i + 1,
title: `商品${(this.page - 1) * this.pageSize + i + 1}`,
description: `这是商品${(this.page - 1) * this.pageSize + i + 1}的描述`,
price: Math.random() * 1000,
image: 'https://via.placeholder.com/100'
}))
if (this.page === 1) {
this.listData = newData
} else {
this.listData = [...this.listData, ...newData]
}
// 模拟没有更多数据
if (this.page >= 3) {
this.hasMore = false
}
this.page++
} catch (error) {
console.error('加载数据失败:', error)
this.loadError = true
} finally {
this.loading = false
}
}
}
}
</script>
<style scoped>
.load-more-list {
padding: 20rpx;
}
.list-item {
display: flex;
padding: 20rpx;
margin-bottom: 20rpx;
background-color: #fff;
border-radius: 10rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.item-image {
width: 150rpx;
height: 150rpx;
border-radius: 5rpx;
}
.item-content {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.item-title {
font-size: 32rpx;
font-weight: 500;
margin-bottom: 10rpx;
}
.item-desc {
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-price {
font-size: 30rpx;
font-weight: bold;
color: #ff4d4f;
}
.loading,
.no-more,
.load-error {
text-align: center;
padding: 20rpx;
color: #999;
font-size: 28rpx;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
}
.load-error {
color: #ff4d4f;
}
.load-error:active {
opacity: 0.8;
}
</style>3. 商品列表页实战
完整的商品列表页
<template>
<view class="goods-list-page">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-input">
<uni-icons type="search" size="24" color="#999"></uni-icons>
<input type="text" v-model="searchKeyword" placeholder="搜索商品" @confirm="handleSearch" />
</view>
<button @click="handleSearch" class="search-btn">搜索</button>
</view>
<!-- 分类筛选 -->
<view class="category-filter">
<scroll-view scroll-x enable-flex>
<view
v-for="category in categories"
:key="category.id"
class="category-item"
:class="{ active: selectedCategory === category.id }"
@click="selectCategory(category.id)"
>
{{ category.name }}
</view>
</scroll-view>
</view>
<!-- 商品列表 -->
<view class="goods-list">
<view
v-for="(goods, index) in goodsList"
:key="goods.id"
class="goods-item"
@click="navigateToDetail(goods.id)"
>
<image
:src="goods.image"
:alt="goods.name"
class="goods-image"
lazy-load
/>
<view class="goods-info">
<text class="goods-name">{{ goods.name }}</text>
<text class="goods-desc">{{ goods.description }}</text>
<view class="goods-bottom">
<text class="goods-price">¥{{ goods.price }}</text>
<text class="goods-sales">销量 {{ goods.sales }}</text>
</view>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading">
<uni-icons type="spinner" size="24" color="#007aff" animation="spin"></uni-icons>
<text>加载中...</text>
</view>
<view v-else-if="!hasMore" class="no-more">没有更多商品了</view>
<view v-else-if="loadError" class="load-error" @click="loadGoods">
<text>加载失败,点击重试</text>
</view>
<view v-else-if="goodsList.length === 0" class="empty">
<uni-icons type="empty" size="64" color="#999"></uni-icons>
<text>暂无商品</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
// 搜索和筛选
searchKeyword: '',
categories: [
{ id: 0, name: '全部' },
{ id: 1, name: '手机' },
{ id: 2, name: '电脑' },
{ id: 3, name: '平板' },
{ id: 4, name: '耳机' },
{ id: 5, name: '手表' }
],
selectedCategory: 0,
// 商品列表
goodsList: [],
page: 1,
pageSize: 10,
loading: false,
loadError: false,
hasMore: true
}
},
onLoad() {
this.loadGoods()
},
onPullDownRefresh() {
this.page = 1
this.hasMore = true
this.loadGoods().finally(() => {
uni.stopPullDownRefresh()
})
},
onReachBottom() {
if (!this.loading && this.hasMore && !this.loadError) {
this.loadGoods()
}
},
methods: {
// 加载商品数据
async loadGoods() {
if (this.loading) return
this.loading = true
this.loadError = false
try {
// 模拟网络请求
await new Promise(resolve => setTimeout(resolve, 1000))
// 模拟根据分类和关键词筛选
const filteredGoods = Array.from({ length: this.pageSize }, (_, i) => {
const id = (this.page - 1) * this.pageSize + i + 1
return {
id,
name: `${this.getSelectedCategoryName()}商品${id}${this.searchKeyword ? `_${this.searchKeyword}` : ''}`,
description: `这是${this.getSelectedCategoryName()}商品${id}的详细描述,包含商品的各种特性和优势`,
price: Math.random() * 1000 + 100,
sales: Math.floor(Math.random() * 10000),
image: `https://via.placeholder.com/200?text=Goods${id}`
}
})
if (this.page === 1) {
this.goodsList = filteredGoods
} else {
this.goodsList = [...this.goodsList, ...filteredGoods]
}
// 模拟没有更多数据
if (this.page >= 3) {
this.hasMore = false
}
this.page++
} catch (error) {
console.error('加载商品失败:', error)
this.loadError = true
uni.showToast({
title: '加载失败,请稍后重试',
icon: 'none'
})
} finally {
this.loading = false
}
},
// 搜索商品
handleSearch() {
this.page = 1
this.hasMore = true
this.loadGoods()
},
// 选择分类
selectCategory(categoryId) {
this.selectedCategory = categoryId
this.page = 1
this.hasMore = true
this.loadGoods()
},
// 获取选中的分类名称
getSelectedCategoryName() {
const category = this.categories.find(c => c.id === this.selectedCategory)
return category ? category.name : ''
},
// 跳转到商品详情
navigateToDetail(goodsId) {
uni.navigateTo({
url: `/pages/goods/detail?id=${goodsId}`
})
}
}
}
</script>
<style scoped>
.goods-list-page {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 搜索栏 */
.search-bar {
display: flex;
padding: 10rpx 20rpx;
background-color: #fff;
align-items: center;
gap: 10rpx;
}
.search-input {
flex: 1;
display: flex;
align-items: center;
background-color: #f0f0f0;
border-radius: 20rpx;
padding: 0 20rpx;
height: 60rpx;
}
.search-input input {
flex: 1;
margin-left: 10rpx;
font-size: 28rpx;
}
.search-btn {
height: 60rpx;
padding: 0 30rpx;
background-color: #007aff;
color: #fff;
border-radius: 20rpx;
font-size: 28rpx;
}
/* 分类筛选 */
.category-filter {
background-color: #fff;
margin-bottom: 10rpx;
}
.category-filter scroll-view {
display: flex;
padding: 15rpx 0;
}
.category-item {
padding: 0 30rpx;
font-size: 28rpx;
color: #666;
position: relative;
}
.category-item.active {
color: #007aff;
font-weight: 500;
}
.category-item.active::after {
content: '';
position: absolute;
bottom: -15rpx;
left: 50%;
transform: translateX(-50%);
width: 20rpx;
height: 4rpx;
background-color: #007aff;
border-radius: 2rpx;
}
/* 商品列表 */
.goods-list {
padding: 10rpx;
}
.goods-item {
background-color: #fff;
border-radius: 10rpx;
padding: 20rpx;
margin-bottom: 10rpx;
display: flex;
gap: 20rpx;
}
.goods-item:active {
opacity: 0.8;
}
.goods-image {
width: 160rpx;
height: 160rpx;
border-radius: 5rpx;
flex-shrink: 0;
}
.goods-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.goods-name {
font-size: 32rpx;
font-weight: 500;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.goods-desc {
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.goods-bottom {
display: flex;
justify-content: space-between;
align-items: center;
}
.goods-price {
font-size: 30rpx;
font-weight: bold;
color: #ff4d4f;
}
.goods-sales {
font-size: 24rpx;
color: #999;
}
/* 加载状态 */
.loading,
.no-more,
.load-error,
.empty {
text-align: center;
padding: 30rpx;
color: #999;
font-size: 28rpx;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
}
.load-error {
color: #ff4d4f;
}
.load-error:active {
opacity: 0.8;
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
padding: 100rpx 0;
}
</style>4. 列表性能优化
虚拟列表实现
<template>
<view class="virtual-list" :style="{ height: containerHeight + 'px' }">
<view
class="virtual-list-content"
:style="{ transform: `translateY(${offsetTop}px)` }"
>
<view
v-for="(item, index) in visibleItems"
:key="item.id"
class="list-item"
:style="{ height: itemHeight + 'px' }"
>
<text>{{ item.content }}</text>
</view>
</view>
<scroll-view
class="scroll-container"
scroll-y
:scroll-top="scrollTop"
@scroll="handleScroll"
>
<view class="scroll-placeholder" :style="{ height: totalHeight + 'px' }"></view>
</scroll-view>
</view>
</template>
<script>
export default {
props: {
// 列表数据
data: {
type: Array,
default: () => []
},
// 列表项高度
itemHeight: {
type: Number,
default: 100
},
// 容器高度
containerHeight: {
type: Number,
default: 500
}
},
data() {
return {
scrollTop: 0,
offsetTop: 0,
startIndex: 0,
endIndex: 0
}
},
computed: {
// 总高度
totalHeight() {
return this.data.length * this.itemHeight
},
// 可见项数量
visibleCount() {
return Math.ceil(this.containerHeight / this.itemHeight) + 2 // 额外渲染2个以确保滚动流畅
},
// 可见项
visibleItems() {
return this.data.slice(this.startIndex, this.endIndex)
}
},
watch: {
// 监听滚动位置
scrollTop: {
handler(newVal) {
// 计算起始索引
this.startIndex = Math.floor(newVal / this.itemHeight)
// 计算结束索引
this.endIndex = Math.min(this.startIndex + this.visibleCount, this.data.length)
// 计算偏移量
this.offsetTop = this.startIndex * this.itemHeight
},
immediate: true
},
// 监听数据变化
data() {
this.updateVisibleRange()
}
},
methods: {
// 处理滚动事件
handleScroll(e) {
this.scrollTop = e.detail.scrollTop
},
// 更新可见范围
updateVisibleRange() {
this.startIndex = Math.floor(this.scrollTop / this.itemHeight)
this.endIndex = Math.min(this.startIndex + this.visibleCount, this.data.length)
this.offsetTop = this.startIndex * this.itemHeight
}
}
}
</script>
<style scoped>
.virtual-list {
position: relative;
overflow: hidden;
}
.virtual-list-content {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.list-item {
display: flex;
align-items: center;
padding: 0 20rpx;
border-bottom: 1rpx solid #e5e5e5;
background-color: #fff;
}
.scroll-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.scroll-placeholder {
width: 100%;
}
</style>图片懒加载
<template>
<view class="lazy-load-list">
<view v-for="(item, index) in listData" :key="item.id" class="list-item">
<image
:src="item.image"
:alt="item.title"
class="item-image"
lazy-load
@load="handleImageLoad"
@error="handleImageError"
/>
<view class="item-content">
<text class="item-title">{{ item.title }}</text>
<text class="item-desc">{{ item.description }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
listData: Array.from({ length: 50 }, (_, i) => ({
id: i + 1,
title: `Item ${i + 1}`,
description: `This is the description for item ${i + 1}`,
image: `https://via.placeholder.com/200?text=Image${i + 1}`,
imageLoaded: false
}))
}
},
methods: {
// 图片加载成功
handleImageLoad(e) {
console.log('Image loaded:', e.target.dataset.src)
},
// 图片加载失败
handleImageError(e) {
console.log('Image load error:', e.target.dataset.src)
// 可以设置默认图片
e.target.src = 'https://via.placeholder.com/200?text=Error'
}
}
}
</script>
<style scoped>
.lazy-load-list {
padding: 20rpx;
}
.list-item {
display: flex;
padding: 20rpx;
margin-bottom: 20rpx;
background-color: #fff;
border-radius: 10rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.item-image {
width: 150rpx;
height: 150rpx;
border-radius: 5rpx;
}
.item-content {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.item-title {
font-size: 32rpx;
font-weight: 500;
margin-bottom: 10rpx;
}
.item-desc {
font-size: 26rpx;
color: #666;
}
</style>常见问题与解决方案
1. 列表性能问题
问题:长列表渲染卡顿,内存占用过高
解决方案:
- 使用虚拟列表,只渲染可视区域内的列表项
- 实现图片懒加载,延迟加载可视区域外的图片
- 使用 list 和 cell 组件,利用组件复用机制
- 减少列表项内的复杂计算和 DOM 操作
- 避免在列表渲染时使用复杂的过滤器和计算属性
2. 下拉刷新和上拉加载冲突
问题:下拉刷新和上拉加载同时触发,或者触发时机不正确
解决方案:
- 使用状态变量管理加载状态,避免重复触发
- 在上拉加载时禁用下拉刷新,或在下拉刷新时禁用上拉加载
- 调整下拉刷新和上拉加载的触发阈值
- 使用节流函数处理滚动事件,避免频繁触发
3. 数据分页问题
问题:数据分页加载时出现重复数据或数据丢失
解决方案:
- 确保后端 API 正确实现分页逻辑,返回正确的分页数据
- 在前端维护正确的分页状态,包括当前页码、每页大小等
- 在下拉刷新时重置分页状态,确保重新从第一页加载
- 处理边界情况,如最后一页数据不足一页的情况
4. 列表项点击事件问题
问题:列表项点击事件不触发,或触发区域不正确
解决方案:
- 确保列表项使用了正确的点击事件绑定(@click)
- 检查列表项内是否有阻止事件冒泡的元素
- 为列表项添加合适的触摸反馈样式,提高用户体验
- 对于复杂的列表项,考虑使用 catchtap 替代 tap 以避免事件冒泡
学习总结
通过本章节的学习,您已经掌握了以下内容:
- 列表组件使用:学会了使用 view、scroll-view 等组件构建列表,以及如何使用高性能列表组件
- 下拉刷新和上拉加载:掌握了下拉刷新和上拉加载的实现方法,以及如何处理加载状态
- 数据分页加载:学会了如何实现数据的分页加载和管理,包括页码控制和数据合并
- 列表性能优化:掌握了虚拟列表、图片懒加载、列表项复用等性能优化技巧
- 商品列表页实战:实现了一个完整的商品列表页,包括搜索、分类筛选、商品展示等功能
列表开发是移动应用开发中的重要部分,良好的列表设计可以提高用户体验和应用性能。在实际项目中,您应该根据具体的业务需求和数据量大小,选择合适的列表实现方案,并应用性能优化技巧,确保列表流畅运行。
通过本章节的学习,您已经具备了开发各种类型列表的能力,可以应对不同场景下的列表开发需求,为用户提供流畅、高效的列表体验。