Tips:
下面gif图2.7MB左右,网络不好可能加载有问题(无法打开请点击此图片链接单独查看)
上拉加载更多的细节:
- 触底: 监测触底事件在触底之后执行一系列动作
- 加载数据: 在触底后需要向服务器请求数据,如果已经请求到了所有数据,应该不再发送请求。
- 加载状态: 请求数据的等待时间,需要更新状态为加载中,数据渲染完成后取消该状态的显示
- 数据渲染: 将请求到的数据显示在视图中
- 没有更多数据的提示
优化项
- 防止连续的多次请求
- 封装:如何在多个页面应用同一套实现代码
数据结构来源7七月老师的(风袖API文档)
{
"total":1,
"count":10,
"page":0,
"total_page":1,
"items":[
{
"id":8,
"title":"ins复古翠绿NoteBook",
"subtitle":"林白默默的掏出小本本,将她说的话一次不漏的记了下来。",
"img":"",
"for_theme_img":"",
"price":"29.99",
"discount_price":"27.8",
"description":null,
"tags":"林白推荐",
"sketch_spec_id":"1",
"max_purchase_quantity":null,
"min_purchase_quantity":null
}
]
}
// /model/Products.js
class Products {
static store = [
{
id: 'P001',
title: '人间值得',
subtitle: '愿你遍历山河,仍觉人间值得!',
img: '/images/人间值得.png',
price: "49.90",
discount_price: "46.30",
labels: ['人间值得', '恒子奶奶'],
for_theme_img: "",
},
// .....本文这里给出一条数据,其余的省略
]
constructor() {
this.total = Products.store.length;
}
async getPorductList({ count = 5, page = 1 }) {
this.count = count;
this.page = page;
this.total_page = Math.ceil(this.total / this.count);
const start = (this.page - 1) * this.count;
const end = this.page * this.count;
this.items = Products.store.slice(start, end);
return new Promise((resolve) => {
resolve(this._getDataTemplate())
})
}
_getDataTemplate() {
return {
total: this.total,
count: this.count,
total_page: this.total_page,
page: this.page,
items: this.items
}
}
}
通过构造一个Products类,模拟数据库以及对数据库的请求。
- 静态属性
store
代表数据库中的数据, _getDataTemplate
对数据格式进行组装,模拟后端对数据的处理,getPorductList
方法模拟请求后端数据,每次请求默认5条数据,可以配置请求数据条数与请求页数,最终将数据进行包装后返回一个promise。
一个项目的loading风格是统一的,这里选择了易用性而舍弃了灵活性。
<view class="loading-container" wx:if="{{show}}">
<view class="loading" wx:if="{{loading}}">
<image class="loading-img" src="/images/loading.gif"></image>
<text class="loading-text">加载中</text>
</view>
<view class="done" wx:else>
我也是有底线的~
</view>
</view>
通过设置show
属性来显示或隐藏loading组件,通过设置loading
属性来选择显示loading的状态
data: {
loadingStatus: true, // loading状态(加载中/无数据)的控制
loadingShow: false, // loading组件的显示控制
products: [], // 展示的数据
productModel: null, // Products类创建的对象模型
currentPage: 1, // 当请求页的设置
pageCount: 5 // 每页请求数据的数量
},
async onLoad (options) {
const productModel = new Products();
const products = await productModel.getPorductList({
count: this.data.pageCount,
page: this.data.currentPage
})
this.setData({
productModel,
products: products,
});
this.renderWaterFlow();
},
renderWaterFlow() {
wx.lin.renderWaterFlow(this.data.products.items, false, () => {
this.setData({
loadingShow: false,
})
})
},
进入页面在没有触发触底事件时,应当加载一组数据进行正常的显示。所以选择在onLoad
生命周期中进行。
这里创建了Products
类的实例productModel
方便后续向后端发送请求获取数据。紧接着调用该实例的getPorductList
方法,并传入请求页与每页显示数据条数获取第一组数据,并将其更新到data中。
最后调用lin-ui
提供的瀑布流组件进行数据的渲染。
onReachBottom: function () {
console.log('触底')
if(!this.data.loadingShow) {
console.log('请求')
if (this.data.currentPage >= this.data.productModel.total_page) {
this.setData({
loadingShow: true,
loadingStatus: false
})
} else {
this.setData({
loadingShow: true,
currentPage: this.data.currentPage + 1,
})
setTimeout(() => {
this.getPorductList()
}, 3000)
}
}
},
async getPorductList() {
const products = await this.data.productModel.getPorductList({
count: this.data.pageCount,
page: this.data.currentPage
})
this.setData({
products,
})
this.renderWaterFlow();
},
onReachBottom
是小程序提供的触底事件处理方法,我们可以将触底后需要做的操作放在此函数中运行。
在这个函数中先忽略最外层的if
语句,剩余代码判断了当前展示的数据是不是最后一页的数据:
- 如果是的话就不再进行数据的请求,并将loading组件显示出来,loading状态设为false,进行
没有更多数据
提示的相关展示。 - 如果当前展示的数据没有到最后一页,则应请求下一页数据,并将loading组件加载出来,loading状态为加载状态。这里使用setTimeout模拟了发送和接收请求这段等待的时间。
getPorductList
方法里对(模拟的)后端进行了请求并做了数据设置,之后调用renderWaterFlow
进行瀑布流的展示,在lin-ui
瀑布流函数的回调中,可以设置将loadingShow
为false
隐藏loading组件。
连续请求的优化
上面提到先忽略onReachBottom
最外层的if语句,这里来看看这个if语句解决了什么问题,上面代码中可以看到有两个console
打印语句,一个是触底
一个是请求
,当网络稍微差的时候,我们可以在没有接收到请求数据的时候触发多次触底事件,这是不合理的,所以加了这个if语句,判断是否在loading了,如果在loading,则表明正在请求数据,就不应该再发送请求,否则再继续进行请求的逻辑。
上面的功能已经完成,但还可以做很多优化,比如最起码在onLoad
和onReachBottom
写很多代码看起来让人很不舒服。
但更重要的并不是这个问题,而是我们这个触底请求数据可能要在多个页面中用到,如何只写一份代码就能让不同的页面使用这个功能就显得很重要了。
在接触这个作业时对提及的封装自己感觉并没有什么地方值得封装,因为事实上代码量并不是很多,对量不是很多的代码不能为了封装而封装吧,想了想它的需求,才明白应该提取公用的部分,将其封装起来。
这里的方式是使用behaviors
,behavior
就类似vue
中的mixin
(自己没写过小程序,看了文档后感觉这两种东西作用非常相似,将两者做类比可以更方便自己对它的理解)
behavior
不能在Page
中使用Page
有自己的用处,Component
不能替代它,如触底函数在Component中是没有的behavior
与Component
的生命周期函数不同于Page
虽然只列举了这几个问题,可能对于开发过小程序的人还不是坑,但是对自己来说就算坑了,踩坑和解决也花了不少功夫。
behavior的封装
// /behaviors/loadmore.js
import { Products } from '../model/ProductsTest.js';
module.exports = Behavior({
behaviors: [],
data: {
loadingStatus: true,
loadingShow: false,
products: [],
productModel: null,
currentPage: 1,
pageCount: 5
},
async attached() {
this.initData()
},
methods: {
async initData() {
const productModel = new Products();
const products = await productModel.getPorductList({
count: this.data.pageCount,
page: this.data.currentPage
})
this.setData({
productModel,
products: products,
});
this.renderWaterFlow();
},
renderWaterFlow() {
wx.lin.renderWaterFlow(this.data.products.items, false, () => {
this.setData({
loadingShow: false,
})
})
},
handleReachBottom() {
if (!this.data.loadingShow) {
if (this.data.currentPage >= this.data.productModel.total_page) {
this.setData({
loadingShow: true,
loadingStatus: false
})
} else {
this.setData({
loadingShow: true,
currentPage: this.data.currentPage + 1,
})
setTimeout(() => {
this.getPorductList()
}, 3000)
}
}
},
async getPorductList() {
const products = await this.data.productModel.getPorductList({
count: this.data.pageCount,
page: this.data.currentPage
})
this.setData({
products,
})
this.renderWaterFlow();
}
},
})
这里的封装其实就是将之前页面中的函数进行移植,首先将原来的数据可以完全剪切过来,之前页面的renderWaterFlow
和getPorductList
方法复制到behavior
的methods
字段中,onLoad中的代码可以完全提取出来写一个initData()
方法,onReachBottom
中的代码提取出来写成一个handleReachBottom()
方法,将这两个方法也复制到behavior
中的methods
字段。到这里代码的移植工作就做完大部分了。
然后看看获取第一组数据的执行时机,在Page
中的时候是放在onLoad
方法中的,现在应该放到attached
方法中,attached
是behavior
的一个生命周期方法,Component
中也有这个方法。
然后比较重要的一件事就出现了,之前我们的代码都放到了behavior
中,但是在Page中无法使用behavior
,如果直接将Page变成Component
,这将导致我们无法监听触底事件,所以只能创建一个Compoent
,将Page中的组件复制过去。然后更改页面js的逻辑,传播触底事件
<f-loadmore reachBottom="{{reachBottom}}"></f-loadmore>
// /views/waterflow/waterflow.js
data: {
reachBottom: false
},
onReachBottom: function () {
this.setData({
reachBottom: true
})
},
// /components/loadmore/index.js
properties: {
reachBottom: Boolean
},
observers: {
'reachBottom': function(val) {
console.log(val)
if(val) {
this.handleReachBottom();
}
}
},
说来也巧,微信中的数据的更新与Vue不同,当属性reachBottom
更改为true
之后,再次触底触发reachBottom
重新setData,仍然设置为true
,在组件监听reachBottom
属性变化时仍然能够监听到。
所以第一次设置reachBottom
为false,虽然会触发组件中事件的监听,但由于触底时候会设置reachBottom
为true
,我们就可以将这次false
过滤掉,如果触发过来的都是true
,我们就认为触发了触底事件,然后执行handleReachBottom
。
这里可以直接在Component
中执行引入过来的behavior
,引入过来的behavior
一旦被注册到当前组件,其中的各配置将都会与组件的配置项进行合并,所以可以直接使用。
当前的做法只是一个简单的例子,有时候我们会很多地方需要调用不同的接口,但以上例子中的做法并不支持,你可以根据自己需要,将这个封装做得更加灵活,以达到适应自己项目的目的。