最终代码在最后
基本结构
主体采用taro的scrollView组件,并为其设置固定高度来使其滑动生效,在配置一个onScrollToLower事件来触发到底事件。
下拉scroll组件
import React from "react";
import { ScrollView } from "@tarojs/components";
import { LoadMore } from "../loadMore";
const Test = () => {
const onScrollToLower = () => {
// 下拉到底事件
}
return (
<>
<ScrollView
className="test" // 设定一个固定高度
onScrollToLower={onScrollToLower}
/>
<LoadMore>
<>
)
}
LoadMore组件
借用taro-ui的AtLoadMore
import React from "react";
import { AtLoadMore } from "taro-ui/lib";
import { View } from "@tarojs/components";
import { useSelector } from "react-redux";
import { RootState } from "@/models/index";
import "./index.scss";
export const LoadMore = () => {
// 获取model中的数据
const showLoad = useSelector<RootState>((state) => state.smartPanHistory.showLoad);
const loadingStatus = useSelector<RootState>((state) => state.smartPanHistory.loadingStatus);
return (
<View className={`recordloadmore ${showLoad ? "" : "hide"}`}>
<AtLoadMore status={loadingStatus} moreText='上拉加载' />
</View>
);
};
实现拼接数据
如何将新加载的数据拼接到前面的数据后面,这一点可以借用react的刷新方式,将新数据使用数组的concat方法拼到后面即可,react只会对已作出改变的部分进行更新,因此并不会整个页面刷新,而是只将后面的新数据刷新出来。
将起始的数据设置成空数组,这样即使是首次刷新也可以采用拼接数据的方式去刷新,而不用增加新的变量来区分是首次加载还是下拉刷新。
res.items 等同于 []concatres.items
LoadMore控制变量分析
taro-ui的LoadMore组件使用status来控制 more(上拉加载)、 loading(加载中)、 noMore(已无更多)三种状态,同时该组件在上拉时显示上拉加载数据完毕后消失,所以需要一个值来控制它的显示和隐藏,因此我们定义两个变量loadingStatus,showLoad来控制LoadMore的状态和是否显示。
- showLoad逻辑分析
很容易想到的是当到底时该组件显示,状态为上拉加载,随后显示loading,然后开始加载数据,加载数据完成后loading结束,并且组件消失。
所以组件的出现与消失和上拉到底直接相关,因此他应该在上拉到底事件中控制。
他的默认值是false,只有触发下拉到底才会显示。 - loadingStatus逻辑分析
默认状态下显示上拉加载,loadingStatus的默认值为more。
loading的显示时间与调接口的时间长短直接相关,而且调接口之前显示loading,调接口之后关闭loading,然后根据数据是否有剩余来判断组件显示已无更多状态还是继续恢复默认的上拉加载。所以需要在调接口的过程中来更改model中loadingStatus的值,同时添加一个hadOver变量来判断是否还有剩余数据。 - hadOver逻辑分析
这个变量由接口定义,如果接口中有返回表明是否具有剩余数据的属性直接用即可,如果没有可以使用返回的数据长度做判断,这里采用的是返回的数据长度。
于是有了以下的代码:
const onScrollToLower = async () => {
console.log("----------我已经到底了---------");
try {
await dispatch({
type: "smartPanHistory/getHistoryList",
payload: {
beginDate: startTime, // 查询接口的参数
endDate: endTime, // 查询接口的参数
status: current === 1 ? "2" : current === 2 ? "3" : "", // 查询接口的参数
limit: 20, // 查询接口的参数
showLoad: true, // 显示组件
},
});
} catch (err) {
err && Taro.showModal({ content: err?.info, showCancel: false });
}
}
二次触底bug
上面的逻辑看起来没有问题,但是在实际操作中会产生一个bug
当触底事件触发时让组件显示,记录页面滑动的scroll就会被刷新,进而浏览器会认为你又一次触发了到底事件,因此会执行两遍这个函数,并且界面会出现上下弹跳和卡顿的现象。
因此,我们要在页面加载的一开始就判断组件LoadMore是显示或隐藏,而不在触底时在让它显示或隐藏。
showLoad逻辑二次分析
在起始状态是没有数据的,因此我们应当显示无数据的界面,而当数据很多时(超过了手机的纵向显示范围,需要通过上拉才可以看到后面的数据)我们需要上拉,这时无论status是什么状态,都应该显示出来。同理,若只有一条数据或数据没有超过手机界面时则无需上拉该组件则不显示。
因此showLoad的值与接口数据的多少有关系,所以我们应该在请求接口后根据数据长度来对他的值进行更新。而在更新之前即未调接口,他的值应为false即不显示组件。
这样在页面第一次加载时,通过接口返回的数据提前将LoadMore展示或隐藏,触底时不会出现dom元素变动,体验上丝滑很多,不会出现卡顿和执行两次的情况。(这里我的页面中当数据超过3条时会超出范围,我设置为了3,下面请求接口代码中会出现)
loadingStatus逻辑二次分析
loadingStatus的状态默认是more,根据上述分析在调用接口前将其状态更改为loading。
随后根据hadOver的值来判断:
-
hadOver == true
此时还有剩余数据可以继续触发上拉功能,因此在调用接口后将loadingStatus状态设为more,showLoad设置为false,然后将返回的数组进行concat拼接更新到model中。
-
hadOver == false
此时后面的请求接口已经没有数据了,因此将loadingStatus状态设置为noMore,将showLoad设置为true。
组件消失bug
上面的逻辑看上去很正常,但是实际却是并没有显示出已无更多,而是整个组件消失了,查看控制台会发现又调用了一次触底函数。原因就在于此时已经没有数据,但是又触发了触底事件调用了接口,而接口返回的数据为空,根据第一次对showLoad的分析没数据时showLoad为false不显示。
因此,需要在最后一次监测到没有数据后,不触发触底事件,但这明显做不到。可以换一种思路,还是执行触底事件,但是在触底事件中添加一个判断来判断是否调用接口,这样就不会改变组件内部的各种状态,显示出正确的状态,同时不用重复调用接口,而这个判断变量自然是model中hadOver的值。
查询功能
我还有个另外的功能就是有一个查询功能,可以通过更改起始和终止时间以及对应的tabs,来查询当前时间段当前状态下的数据,在这种情况下就不再是简单的concat拼接而是要清空数据进行重刷页面,且每一个tabs都有一个对应数组,调接口时根据传入的status值判断更新其中的哪一个。
当查询时将对应的tab数组更新为空数组,随后在刷新一遍model中对应的数据将其更新为接口返回的数据而不是直接拼接数据,所以添加了一个isQuerry变量来判断是否是查询功能。
下面是完整代码:
触底事件
// 是否剩余变量
const hadOver = useSelector<RootState>((state) => state.smartPanHistory.hadOver);
// 触底函数
const onScrollToLower = async (currentTab) => {
/**
* 这一步是我要在taro的tabs组件(我的是三个tab)下都要设置下拉加载,而这里有一个bug就是
* 在切换tab时也会触发到底事件,后来发现是控制tabs的current没同步,因此在进入前设置了一个
* 校验
*/
if(currentTab !== current) return;
// 通过数据是否剩余来判断是否掉接口,剩余就继续调用进行上拉加载,否则就不掉
if(hadOver) {
try {
await dispatch({
type: "smartPanHistory/getHistoryList",
payload: {
beginDate: startTime,
endDate: endTime,
status: current === 1 ? "2" : current === 2 ? "3" : "", // 当前tabs值
limit: 4, // 请求数据条数
anchorID: anchorID, // 当前的上拉锚点
},
});
} catch (err) {
err && Taro.showModal({ content: err?.info, showCancel: false });
}
}
}
model数据与请求接口
// 在这里对 ModelState 进行数据初始化操作
state: {
allList: [], // tabs0 scrollView的数据存储变量
triggeredList: [], // tabs1 scrollView的数据存储变量
invalidList: [], // tabs2 scrollView的数据存储变量
showLoad: false, // 是否展示LoadMore组件,默认不展示
loadingStatus: "more", // LoadMore组件展示状态,默认more上拉加载
hadOver: true, // 是否还有数据,默认为true执行调用接口,在请求后根据返回值改变
anchorID: "", // 上拉数据锚点
isQuery: false // 是否是查询
},
effects: {
// 查询数据接口
*getHistoryList({ payload }: Action, { call, put, select }: EffectCommand & EffectThrottle) {
// 拿到payload参数
const { beginDate, endDate, limit, status, anchorID, isQuery } = payload!;
// 拿到model中的每个tabs对应的数据列表
const {allList, triggeredList, invalidList } = yield select<any>(state => state.smartPanHistory);
// 调用接口前改变loadMore组件状态为加载中
yield put({
type: "updateState",
payload: {
loadingStatus: "loading"
},
});
// 开始调用接口IF011704数据
let res:any = [];
// 如果传了表明是上拉刷新,将锚点传入
if(anchorID) {
// 如果status传了就表示是tabs == 1 或tabs == 2,如果不传默认为tabs == 0
if (status) {
res = yield call(IF011704, {beginDate, endDate, limit, status, anchorID});
} else {
res = yield call(IF011704, {beginDate, endDate, limit, anchorID});
}
} else {
if (status) {
res = yield call(IF011704, {beginDate, endDate, limit, status});
} else {
res = yield call(IF011704, {beginDate, endDate, limit});
}
}
// 获取接口数组长度
const resLength = res.items.length;
// 对象参数
let parmerObj = Object.create(null); // 未查询状态下需要更新的model数据
let queryParmer = Object.create(null); // 第二次查询时更新的model数据
let lastAnchorID = ""; // 接口中最后一个数据所携带的锚点
if(status) {
// 通过判断status来获取更新哪一个tabs,并设置parmerObj中对应更新的key,value值
if(status === "2") {
// 已触发
parmerObj["triggeredList"] = isQuery ? [] : triggeredList.concat(res.items);
queryParmer["triggeredList"] = res.items;
} else {
// 已失效
parmerObj["invalidList"] = isQuery ? [] : invalidList.concat(res.items);
queryParmer["invalidList"] = res.items;
}
} else {
// 全部
parmerObj["allList"] = isQuery ? [] : allList.concat(res.items);
queryParmer["allList"] = res.items;
}
// 如果一条数据都没有就什么也不做,如果有数据获取到他的锚点
if (resLength !== 0) {
lastAnchorID = res.items[res.items.length - 1].anchorID;
}
// 设置上拉和切换tabs参数
// 更新LoadMore组件初始化状态,这里我用3条数据来控制,超过显示,没超过不显示
parmerObj.showLoad = queryParmer.showLoad = resLength >= 3 ? true : false;
// 更新数据是否有剩余的状态
parmerObj.hadOver = queryParmer.hadOver = resLength >= limit ? true : false;
// 更新LoadMore status状态,有剩余表示继续下拉加载为more,没数据则已无更多noMore
parmerObj.hadOver ? parmerObj.loadingStatus = "more" : parmerObj.loadingStatus = "noMore";
// 锚点
parmerObj.anchorID = lastAnchorID;
// 更新未查询状态时的数据
yield put({
type: "updateState",
payload: parmerObj
});
// 当是查询按钮时将空数组再次更新为items使得数据刷新到顶部
isQuery && (
yield put({
type: "updateState",
payload: queryParmer
})
);
},