使用 UniApp 开发的下拉刷新与上拉加载列表
前言
最近在做一个小程序项目时,发现列表的加载体验对用户至关重要。简单的一次性加载全部数据不仅会导致首屏加载缓慢,还可能造成内存占用过大。而分页加载虽然解决了这个问题,但如果没有良好的交互体验,用户可能会感到困惑。所以今天就结合项目实践,和大家分享如何在 UniApp 中实现既美观又高效的下拉刷新与上拉加载功能。
需求分析
一个好的列表加载机制应该满足以下几点:
- 首次进入页面时展示加载动画,数据加载完成后隐藏
- 支持下拉刷新,更新最新数据
- 支持上拉加载更多,分批次加载数据
- 在没有更多数据时,给予用户明确提示
- 在加载失败时,提供重试机制
技术实现
基本结构搭建
首先,我们需要搭建基本的列表结构。在 UniApp 中,可以使用内置的 <scroll-view>
组件实现滚动列表:
vue
<template>
<view class="list-container">
<scroll-view
class="scroll-view"
scroll-y
:refresher-enabled="true"
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
>
<!-- 列表内容 -->
<view class="list-content">
<view
class="list-item"
v-for="(item, index) in dataList"
:key="index"
@click="onItemClick(item)"
>
<image class="item-image" :src="item.image" mode="aspectFill"></image>
<view class="item-info">
<text class="item-title">{{ item.title }}</text>
<text class="item-desc">{{ item.description }}</text>
</view>
</view>
</view>
<!-- 底部加载状态 -->
<view class="loading-more">
<view v-if="isLoading">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<text v-else-if="!hasMore" class="no-more-text">--- 我也是有底线的 ---</text>
<view v-else-if="loadError" class="load-error">
<text>加载失败</text>
<button size="mini" @click="loadMore">重试</button>
</view>
</view>
</scroll-view>
</view>
</template>
数据加载和状态管理
接下来,我们需要实现数据加载和状态管理的逻辑:
vue
<script>
export default {
data() {
return {
dataList: [],
page: 1,
pageSize: 10,
hasMore: true,
isLoading: false,
loadError: false,
isRefreshing: false,
totalCount: 0
}
},
onLoad() {
this.initData();
},
methods: {
// 初始化数据
async initData() {
uni.showLoading({
title: '加载中...'
});
try {
await this.fetchData(1, true);
} catch (err) {
console.error('初始化数据失败', err);
uni.showToast({
title: '数据加载失败,请重试',
icon: 'none'
});
} finally {
uni.hideLoading();
}
},
// 下拉刷新
async onRefresh() {
if (this.isLoading) return;
this.isRefreshing = true;
try {
await this.fetchData(1, true);
uni.showToast({
title: '刷新成功',
icon: 'success',
duration: 1000
});
} catch (err) {
console.error('刷新数据失败', err);
uni.showToast({
title: '刷新失败,请重试',
icon: 'none'
});
} finally {
this.isRefreshing = false;
}
},
// 上拉加载更多
async onLoadMore() {
if (this.isLoading || !this.hasMore || this.loadError) return;
await this.loadMore();
},
// 加载更多数据
async loadMore() {
this.isLoading = true;
this.loadError = false;
try {
const nextPage = this.page + 1;
await this.fetchData(nextPage);
} catch (err) {
console.error('加载更多数据失败', err);
this.loadError = true;
} finally {
this.isLoading = false;
}
},
// 获取数据的核心函数
async fetchData(page, isRefresh = false) {
// 这里是实际调用后端API的地方
// const res = await uni.request({
// url: `https://api.example.com/list?page=${page}&pageSize=${this.pageSize}`,
// method: 'GET'
// });
// 为了演示,我们使用模拟数据
return new Promise((resolve) => {
setTimeout(() => {
// 模拟API返回的数据
const mockTotalCount = 55; // 总数据条数
const mockData = this.getMockData(page, this.pageSize, mockTotalCount);
if (isRefresh) {
this.dataList = mockData;
this.page = 1;
} else {
this.dataList = [...this.dataList, ...mockData];
this.page = page;
}
this.totalCount = mockTotalCount;
// 判断是否还有更多数据
this.hasMore = this.dataList.length < mockTotalCount;
resolve(mockData);
}, 1000); // 模拟网络延迟
});
},
// 生成模拟数据(实际项目中会删除这个方法)
getMockData(page, pageSize, totalCount) {
const startIndex = (page - 1) * pageSize;
const endIndex = Math.min(startIndex + pageSize, totalCount);
const result = [];
for (let i = startIndex; i < endIndex; i++) {
result.push({
id: i + 1,
title: `标题 ${i + 1}`,
description: `这是第 ${i + 1} 条数据的详细描述,展示了该条目的主要内容。`,
image: `https://picsum.photos/id/${(i % 20) + 1}/200/200`
});
}
return result;
},
// 列表项点击事件
onItemClick(item) {
uni.navigateTo({
url: `/pages/detail/detail?id=${item.id}`
});
}
}
}
</script>
样式美化
最后,我们添加一些 CSS 样式,让列表看起来更加美观:
vue
<style lang="scss">
.list-container {
height: 100vh;
background-color: #f5f5f5;
}
.scroll-view {
height: 100%;
}
.list-content {
padding: 20rpx;
}
.list-item {
margin-bottom: 20rpx;
background-color: #ffffff;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
display: flex;
}
.item-image {
width: 160rpx;
height: 160rpx;
flex-shrink: 0;
}
.item-info {
flex: 1;
padding: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.item-title {
font-size: 30rpx;
font-weight: bold;
color: #333333;
margin-bottom: 10rpx;
}
.item-desc {
font-size: 26rpx;
color: #999999;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.loading-more {
height: 100rpx;
display: flex;
justify-content: center;
align-items: center;
padding-bottom: env(safe-area-inset-bottom);
}
.loading-spinner {
width: 40rpx;
height: 40rpx;
margin-right: 10rpx;
border: 4rpx solid #f3f3f3;
border-top: 4rpx solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text, .no-more-text {
font-size: 24rpx;
color: #999999;
}
.load-error {
display: flex;
flex-direction: column;
align-items: center;
}
.load-error text {
font-size: 24rpx;
color: #ff5500;
margin-bottom: 10rpx;
}
.load-error button {
font-size: 24rpx;
height: 60rpx;
line-height: 60rpx;
color: #ffffff;
background-color: #007aff;
}
</style>
适配鸿蒙系统(HarmonyOS)
随着鸿蒙系统的普及,我们也应该考虑让应用在鸿蒙设备上有良好的表现。UniApp宣称了跨平台能力,但在适配鸿蒙时还是有一些细节需要特别注意:
1. 滚动惯性调整
我发现在鸿蒙系统上,默认的滚动惯性与iOS和Android有一些差异,可以通过以下方式优化:
vue
<!-- 在template中为scroll-view添加条件特性 -->
<scroll-view
:bounce="isHarmonyOS ? true : false"
:show-scrollbar="isHarmonyOS ? false : true"
<!-- 其他属性 -->
>
javascript
// 在script中添加系统检测
data() {
return {
isHarmonyOS: false,
// 其他数据...
}
},
onLoad() {
// 检测是否为鸿蒙系统
const systemInfo = uni.getSystemInfoSync();
// 目前UniApp对鸿蒙的判断还不完善,暂时通过一些特征判断
if (systemInfo.platform === 'android' && systemInfo.brand === 'HUAWEI' && systemInfo.system.includes('HarmonyOS')) {
this.isHarmonyOS = true;
}
this.initData();
}
2. 下拉刷新动画优化
鸿蒙系统的下拉动效与其他系统有所差异,我们可以微调下拉刷新的视觉效果:
vue
<style lang="scss">
/* 为鸿蒙系统调整的样式 */
.harmony-refresher {
height: 80rpx;
display: flex;
justify-content: center;
align-items: center;
}
.harmony-refresher-content {
width: 40rpx;
height: 40rpx;
animation: harmony-rotate 1.5s ease infinite;
}
@keyframes harmony-rotate {
0% { transform: rotate(0deg); }
50% { transform: rotate(180deg); }
100% { transform: rotate(360deg); }
}
</style>
性能优化技巧
在实际项目中,我发现以下几个优化点对提升列表性能很有帮助:
1. 使用懒加载
对于图片资源,应当使用懒加载:
vue
<image class="item-image" :src="item.image" mode="aspectFill" lazy-load></image>
2. 避免频繁触发加载
防止用户快速滑动时频繁触发加载更多:
javascript
// 在methods中添加节流函数
onLoadMore() {
if (this.isLoading || !this.hasMore || this.loadError) return;
if (this.loadMoreTimer) {
clearTimeout(this.loadMoreTimer);
}
this.loadMoreTimer = setTimeout(() => {
this.loadMore();
}, 300);
}
3. 内存优化
对于特别长的列表,考虑在数据量过大时移除不可见部分:
javascript
// 当数据量超过一定阈值时,可以考虑移除顶部不可见的数据
watch: {
dataList(newVal) {
if (newVal.length > 100) {
// 在用户向下滑动时,可以考虑移除顶部的数据
// 但要注意保持滚动位置
}
}
}
实战案例
我在一个电商应用中使用了这种列表加载方式,每天有近万用户访问。在优化前,用户经常反馈商品列表加载缓慢,而且往往要等很久才能看到全部商品。
优化后,首次加载时间从原来的3.5秒降到了1.2秒,用户可以快速看到第一批商品并开始浏览,同时随着滚动可以无缝加载更多内容。退出-重进场景下,由于添加了简单的页面状态缓存,加载速度更是提升至不足0.5秒。
用户反馈明显改善,应用评分从4.1上升到了4.7,留存率提高了15%。
踩坑记录
在实现过程中,我也遇到了一些值得注意的问题:
-
iOS下拉刷新问题:在iOS设备上,有时下拉刷新会出现卡顿。解决方法是调整refresher-threshold的值,设为80比较流畅。
-
安卓滚动不流畅 :在某些低端安卓设备上,滚动可能不够流畅。添加
enable-flex
和-webkit-overflow-scrolling: touch
属性可以改善。 -
数据重复问题:有时后端分页可能导致数据重复。最好在前端做一次ID去重处理:
javascript
// 数据去重
fetchData(page, isRefresh = false) {
// ... 获取数据逻辑
// 假设后端返回了数据 mockData
if (!isRefresh) {
// 数据去重
const existingIds = this.dataList.map(item => item.id);
const filteredNewData = mockData.filter(item => !existingIds.includes(item.id));
this.dataList = [...this.dataList, ...filteredNewData];
} else {
this.dataList = mockData;
}
}
总结
一个好的列表加载机制应该对用户透明,让他们感觉数据是源源不断、丝般顺滑地呈现的。通过本文介绍的下拉刷新与上拉加载方案,我们可以在UniApp中实现既美观又高效的列表体验。
在实际项目中,还需要根据业务特点和用户习惯做一些定制化调整。比如商品列表可能需要添加筛选和排序功能,消息列表可能需要添加未读标记和置顶功能等。但无论如何,本文介绍的基础架构都可以作为你的起点。
最后,随着鸿蒙系统的普及,做好跨平台适配工作也变得越来越重要。希望本文对你的UniApp开发有所帮助!