提醒
本文实例是使用uniapp进行开发演示的。
一、需求场景
在开发商品(SKU)列表页面时,通常有三个需求:
- 页面下拉刷新,第一页展示最新数据;
- 上拉加载更多数据;
- 列表页面可以滚动到指定位置,例如:回到顶部、回到上次浏览位置
二、需求分析
- 接口支持分页加载
- 页面下拉刷新
首先在pages.json页面路由里将enablePullDownRefresh参数值设为true,表示此页面开启下拉刷新;刷新成功后,页面数据更新并回到顶部;- 页面向上滑动时,检测是否有更多数据,加载到新的数据直接显示在当前页面列表数据下方,否则提示用户没有更多数据了;
- 记录当前页面滚动的位置;当触发滚动到指定位置方法时将记录页面上次滚动的位置数值参入即可。
三、技术方案
- uni-app页面生命周期onPullDownRefresh函数,监听用户下拉动作,一般用于下拉刷新;
- uni-app页面生命周期onReachBottom函数,页面滚动到底部的事件(不是scroll-view滚到底),常用于下拉下一页数据。具体见下方注意事项
- uni-app页面生命周期onPageScroll函数,监听页面滚动,参数为Object;通过obj.scrollTop获取滚动距离;
- uni.pageScrollTo()方法实现页面滚动到指定位置
四、技术知识点简介
4.1 下拉刷新(onPullDownRefresh)
小程序开启下拉刷新,在配置页面(pages.json)的 enablePullDownRefresh 属性为 true。
js代码示例
{
"pages": [
{
"path": "pages/list/testPulldownRefreshReachBottom",
"style": {
"navigationBarTitleText": "产品列表",
"enablePullDownRefresh": true,
"app-plus": {
"bounce": "vertical"
}
}
}
]
}
在 App 平台下可以自定义部分下拉刷新的配置 page->style->app-plus->pullToRefresh。
js代码示例
{
"pages": [
{
"path": "pages/index/index", //首页
"style": {
"app-plus": {
"pullToRefresh": {
"support": true,
"color": "#ff3333",
"style": "default",
"contentdown": {
"caption": "下拉可刷新自定义文本"
},
"contentover": {
"caption": "释放可刷新自定义文本"
},
"contentrefresh": {
"caption": "正在刷新自定义文本"
}
}
}
}
}
]
}
下拉刷新使用注意
- enablePullDownRefresh 与 pullToRefresh->support 同时设置时,后者优先级较高。
- 如果期望在 App 和小程序上均开启下拉刷新的话,请配置页面的 enablePullDownRefresh 属性为 true。
- 若仅期望在 App 上开启下拉刷新,则不要配置页面的 enablePullDownRefresh 属性,而是配置 pullToRefresh->support 为 true。
- 开启原生下拉刷新时,页面里不应该使用全屏高的scroll-view,向下拖动内容时,会优先触发下拉刷新而不是scroll-view滚动
- 原生下拉刷新的起始位置在原生导航栏的下方,如果取消原生导航栏,使用自定义导航栏,原生下拉刷新的位置会在屏幕顶部。如果希望在自定义导航栏下方拉动,只能使用circle方式的下拉刷新,并设置offset参数,将circle圈的起始位置调整到自定义导航栏下方。hello uni-app的扩展组件中有示例。
- 如果想在app端实现更多复杂的下拉刷新,比如美团、京东App那种拉下一个特殊图形,可以使用nvue的组件。HBuilderX 2.0.3+起,新建项目选择新闻模板可以体验
- 如果想在vue页面通过web前端技术实现下拉刷新,插件市场有例子,但前端下拉刷新的性能不如原生,复杂长列表会很卡
- iOS上,default模式的下拉刷新和bounce回弹是绑定的,如果设置了bounce:none,会导致无法使用default下拉刷新
4.2 页面滚动到底部的事件(onReachBottom)
可在pages.json里定义具体页面底部的触发距离onReachBottomDistance,
比如设为50,那么滚动页面到距离底部50px时,就会触发onReachBottom事件。
如使用scroll-view导致页面没有滚动,则触底事件不会被触发。scroll-view滚动到底部的事件请参考scroll-view的文档。
onReachBottomDistance 参数说明onReachBottomDistance:Number类型,默认值 50,页面上拉触底事件触发时距页面底部距离,单位只支持px。
4.3 监听页面滚动(onPageScroll)
参数说明
scrollTop:Number类型,页面在垂直方向已滚动的距离(单位px)
js代码示例
onPageScroll : function(e) { //nvue暂不支持滚动监听,可用bindingx代替
console.log("滚动距离为:" + e.scrollTop);
},
监听页面滚动注意
- onPageScroll里不要写交互复杂的js,比如频繁修改页面。因为这个生命周期是在渲染层触发的,在非h5端,js是在逻辑层执行的,两层之间通信是有损耗的。如果在滚动过程中,频发触发两层之间的数据交换,可能会造成卡顿。(uvue在app端无此限制)
- 在webview渲染时,比如app-vue、微信小程序、H5中,也可以使用wxs监听滚动,参考;在app-nvue中,可以使用bindingx监听滚动,参考。
- 如果想实现滚动时标题栏透明渐变,在App和H5下,可在pages.json中配置titleNView下的type为transparent,参考。(uni-app x不支持)
- 如果需要滚动吸顶固定某些元素,推荐使用css的粘性布局,参考插件市场。插件市场也有其他js实现的吸顶插件,但性能不佳,需要时可自行搜索。(uni-app x可自由在uts中设置固定位置)
4.4 uni.pageScrollTo(OBJECT)
将页面滚动到目标位置。可以指定滚动到具体的scrollTop数值,也可以指定滚动到某个元素的位置
OBJECT参数说明
五、实例效果图
1.第一页数据
2.上拉加载更多数据
3.下拉刷新
4.页面滑动超过一页显示 回到顶部按钮
5.上拉加载更多数据后,已经全部加载完了
6.点击回到顶部按钮后的页面和第一页显示的数据一样
六、实例代码
testPulldownRefreshReachBottom.vue文件代码
<template>
<view class="content-root">
<view class="content-wrap">
<view v-if="productList && productList.length > 0">
<view class="list-item" v-for="(item, index) in productList" :key="index">
<text class="itme-name">{{ item.name }}</text>
<text class="itme-desc">{{ item.desc }}</text>
</view>
</view>
<view class="no-data" v-else>暂无数据</view>
<view class="no-more-data" v-if="noMoreData">
<text>没有更多数据了</text>
</view>
</view>
<view class="go-top" v-if="showGoToTop" @click="goToTop">
<text>回到</text>
<text>顶部</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
productType: 1,
currentPage: 1,
lastPage: 1,
lastPostion: 0,
pageSize: 30,
total: 0,
noMoreData: false,
productList: [],
showGoToTop: false
}
},
onLoad(option) {
this.productType = option.productType
this.getProductList(true)
},
onPageScroll(e) {
this.lastPostion = e.scrollTop
console.log(`当前滚动位置 this.lastPostion = ${this.lastPostion} , this.showGoToTop = ${this.showGoToTop}`)
if (this.lastPostion > 300) {
this.showGoToTop = true
} else if (this.showGoToTop) {
this.showGoToTop = false
}
},
onPullDownRefresh() {
this.lastPage = this.currentPage
this.currentPage = 1
this.getProductList(true)
setTimeout(() => {
uni.stopPullDownRefresh()
}, 600);
},
onReachBottom() {
if (this.noMoreData) {
return
}
this.lastPage = this.currentPage
this.currentPage++
this.getProductList(false)
},
methods: {
//回到顶部
goToTop() {
uni.pageScrollTo({
scrollTop: 0,
duration: 0,
})
},
//获取商品列表
async getProductList(refresh) {
//let result = await getProductList() // 网络请求
setTimeout(() => {// 模拟网络请求
if (refresh) {
this.iniLocalData()
return
}
this.loadMoreData()
}, 500);
},
iniLocalData() {
this.noMoreData = false
this.productList = [
{
productId: "1234567890",
name: "西蓝花",
desc: "有机蔬菜,安全健康",
skuType: [
{
id: "1",
price: "9.99",
txt: ""
}
]
},
{
productId: "3234567890",
name: "西红柿",
desc: "有机蔬菜,安全健康;",
skuType: [
{
id: "1",
price: "9.00",
txt: ""
}
]
},
{
productId: "4234567890",
name: "洋葱",
desc: "洋葱可生吃,安全健康;",
skuType: [
{
id: "1",
price: "5.00",
txt: ""
}
]
},
{
productId: "1734567890",
name: "小青菜",
desc: "有机蔬菜,安全健康;",
skuType: [
{
id: "1",
price: "3.00",
txt: ""
}
]
},
{
productId: "1234467890",
name: "上海青",
desc: "蔬菜,安全健康;",
skuType: [
{
id: "1",
price: "3.00",
txt: ""
}
]
},
{
productId: "1234567390",
name: "上海青",
desc: "蔬菜,安全健康;",
skuType: [
{
id: "1",
price: "3.00",
txt: ""
}
]
},
{
productId: "1239567390",
name: "娃娃菜",
desc: "蔬菜,安全健康;",
skuType: [
{
id: "1",
price: "8.00",
txt: ""
}
]
},
{
productId: "1239567390",
name: "胡萝卜",
desc: "有机蔬菜,安全健康;",
skuType: [
{
id: "1",
price: "5.00",
txt: ""
}
]
},
{
productId: "1239567390",
name: "西葫芦",
desc: "蔬菜,安全健康;",
skuType: [
{
id: "1",
price: "8.00",
txt: ""
}
]
},
{
productId: "1239567390",
name: "紫甘蓝",
desc: "蔬菜,安全健康;",
skuType: [
{
id: "1",
price: "5.00",
txt: ""
}
]
},
{
productId: "1239567390",
name: "大白菜",
desc: "蔬菜,安全健康;",
skuType: [
{
id: "1",
price: "3.00",
txt: ""
}
]
}
]
},
loadMoreData() {
if (this.noMoreData) {
return
}
let result = [
{
productId: "2239567390",
name: "牛腱",
desc: "新鲜牛肉;",
skuType: [
{
id: "1",
price: "109.00",
txt: ""
}
]
},
{
productId: "2239567391",
name: "牛腿肉",
desc: "新鲜牛肉;",
skuType: [
{
id: "1",
price: "99.00",
txt: ""
}
]
},
{
productId: "2239567392",
name: "牛腩",
desc: "新鲜牛肉;",
skuType: [
{
id: "1",
price: "89.00",
txt: ""
}
]
},
{
productId: "2339567390",
name: "排骨",
desc: "新鲜猪肉;",
skuType: [
{
id: "1",
price: "29.00",
txt: ""
}
]
},
{
productId: "2339567391",
name: "后退肉",
desc: "新鲜猪肉;",
skuType: [
{
id: "1",
price: "18.00",
txt: ""
}
]
},
{
productId: "2339567390",
name: "五花肉",
desc: "新鲜猪肉;",
skuType: [
{
id: "1",
price: "24.00",
txt: ""
}
]
}
]
this.productList = [...this.productList, ...result]
this.noMoreData = true // 这个逻辑在实际项目开发中以接口返回的总数量为准
}
}
}
</script>
<style scoped>
.content-root {
height: 100%;
background-color: #f5f5f5;
padding: 32rpx;
}
.content-wrap {
background-color: #f5f5f5;
padding-bottom: 80rpx;
}
.list-item {
display: flex;
padding: 16rpx 20rpx;
flex-direction: column;
background-color: #ffffff;
border-radius: 16rpx;
color: #000;
margin-bottom: 32rpx;
}
.itme-name {
font-size: 32rpx;
}
.itme-desc {
font-size: 24rpx;
}
.no-data {
height: 100%;
text-align: center;
margin-top: 400rpx;
color: #000;
font-size: 28rpx;
}
.no-more-data {
width: 100%;
height: 50rpx;
color: #000;
align-items: center;
font-size: 28rpx;
font-family: PingFangSC-Medium, PingFang SC;
text-align: center;
}
.go-top {
z-index: 99;
position: fixed;
display: flex;
flex-flow: column;
right: 0;
bottom: 0;
margin-right: 32rpx;
margin-bottom: 200rpx;
width: 80rpx;
height: 80rpx;
align-items: center;
background-color: #eeeeee;
border-radius: 50%;
font-size: 20rpx;
font-family: PingFangSC-Medium, PingFang SC;
text-align: center;
}
</style>