0. 需求
接上一篇文章,我们已经适配好了alovajs
。现在需要一个分页策略。alova
自带的请求策略并不支持原生微信小程序,所以需要结合源码自行开发。
1.官方例子
ts
// vue3中
const {
// 加载状态
loading,
// 列表数据
data,
// 是否为最后一页
// 下拉加载时可通过此参数判断是否还需要加载
isLastPage,
// 当前页码,改变此页码将自动触发请求
page,
// 每页数据条数
pageSize,
// 分页页数
pageCount,
// 总数据量
total
} = usePagination(
// Method实例获取函数,它将接收page和pageSize,并返回一个Method实例
(page, pageSize) => queryStudents(page, pageSize),
{
initialPage: 1, // 初始页码,默认为1
initialPageSize: 10 // 初始每页数据条数,默认为10
....
}
);
// 翻到上一页,page值更改后将自动发送请求
const handlePrevPage = () => {
page.value--;
};
简单的来说usePagination
返回一系列状态,这些状态对于ui
库来说是可识别的状态,当你修改了page
值,也会触发重新请求。
2. 实现自己的版本
官方的代码很多,主要是因为功能比较全,涉及到预加载上下页、翻页和追加两种模式。我的主要是实现了基础的分页请求,预加载我直接舍弃了。所以下面的代码只是思路,如果你需要预加载可以在此基础上改造。
注意:代码中同样使用了mobx
作为状态管理,怎么接入可以参考上一篇文章
ts
// 自己的usePagination
export default function (
// 请求对象
handle,
// 配置
config,
) {
const {
// 如何获取总量值
// 自行根据项目实际情况
total: totalGetter = (res: any) => res.data.total,
// 如何获取响应数据
data: dataGetter = (res: any) => res.data.records,
// 是否是追加模式
// 移动端肯定优先追加
append = true,
// 初始页码
initialPage = 1,
// 每页的size
initialPageSize = 10,
// 外部状态,通常是筛选条件
watchingStates = [],
// 初始数据
initialData,
// 是否立即请求
immediate = true,
// 请求中间件
middleware = () => {},
// 由于没有预请求功能,这里强制请求直接false
force = () => false,
...rest
} = config || {};
///////////////创建状态///////////////
....
....
//////////////////监听变化/////////////////
....
....
//////////////////请求处理/////////////////
...
...
//////////////////导出操作函数/////////////////
...
...
return {
...
}
这里为了方便讲解,我删掉了ts
类型。,如果你想要有类型提示,那你得做好抠脑壳的准备。alova
为了能自动推断类型,有大量的泛型参数,怎么正确的传参需要研究一下。
2.1. 创建状态
ts
///////////////创建状态///////////////
// 用于控制是否重置
const isReset = useRef(false);
// 重置期间请求的次数,为了防止重置时重复请求,使用此参数限制请求
const requestCountInReseting = useRef(0);
const page = useState(initialPage);
const pageSize = useState(initialPageSize);
// 请求错误
const isError = useState<boolean>(false);
// 当前页,或者下拉加载下的所有数据
const data = useState(initialData ? dataGetter(initialData) || [] : []);
// 本次请求的数据
const listDataGetter = (rawData: any) => dataGetter(rawData) || rawData;
// 发送请求
const getHandlerMethod = (refreshPage = page.get()) => {
const pageSizeVal = pageSize.get();
const handlerMethod = handler(refreshPage, pageSizeVal);
return handlerMethod;
};
// 计算data、total、isLastPage参数
const total = useState<number>(initialData ? totalGetter(initialData) : undefined);
const pageCount = computed(() => {
const totalVal = total.get();
return totalVal !== undefined ? Math.ceil(totalVal / pageSize.get()) : undefined;
});
// 如果返回的数据小于pageSize了,则认定为最后一页了
const isLastPage = computed(() => {
const dataRaw = states.data.get();
if (!dataRaw) {
return true;
}
const statesDataVal = listDataGetter(dataRaw);
const pageVal = page.get();
const pageCountVal = pageCount.get();
const dataLen = Array.isArray(statesDataVal) ? statesDataVal.length : 0;
return pageCountVal ? pageVal >= pageCountVal : dataLen < pageSize.get();
});
基本上就是抄的源码,删减了部分代码。下面解释一下。
-
useRef
ts/** * 模拟useRef,使得闭包也能获取最新的值 */ const useRef = <T>(value: T) => ({ current: value });
-
useState
ts/** * 生成mobx可监听的值 */ const useState = <T>(value: T, deep: boolean = false) => observable.box(value, { deep });
-
有大量
getter
如dataGetter
、listDataGetter
等等,这些函数可供外部自定义操作方法。 -
getHandlerMethod
的作用是pageSize
在外部被改变的时候能够获取到最新的值,而且能通过传递refreshPage
调整请求的页码。 -
pageCount
我沿用的源码,通过计算得出的。其实如果后端有返回全部页码可以直接设置。
2.2. 监听变化
ts
// 监听外部输入的状态,重置page为1
reaction(
() => watchingStates.map(i => i.get()),
(value, prevValue) => {
page.set(initialPage);
isReset.current = true;
},
);
// 监听分页参数变化重新请求
// 请求状态
const states = useWatcher<S, E, R, T, RC, RE, RH>(
getHandlerMethod,
[...watchingStates, page, pageSize] as any,
{
immediate,
initialData,
middleware(ctx, next) {
// 监听值改变时将会重置为第一页,此时会触发两次请求,在这边过滤掉一次请求
let requestPromise: Promise<any> = Promise.resolve();
if (!isReset.current) {
requestPromise = next();
} else if (requestCountInReseting.current === 0) {
requestCountInReseting.current++;
requestPromise = next();
}
return requestPromise;
},
// false
force,
// 如果上一个请求没结束,不强制结束上一个请求
// 如果上拉加载第二页把第一页的请求结束了
// 那么数据肯定就错乱了
abortLast: false,
...rest,
},
);
const { send } = states;
- 第一个
reaction
目的是监听外部状态,一般就是请求参数变化,如果变化了就重置页码为1 useWatcher
是监听导出的状态变化如请求参数变化、page
改变就重新请求。middleware
中的代码就是重置和刷新的逻辑
2.3. 请求处理
ts
states.onSuccess(({ data: rawData, sendArgs: [refreshPage, isRefresh] }) => {
runInAction(() => {
isError.set(false);
// 更新总数
const newTotal = totalGetter(rawData);
if (total.get() !== newTotal) {
total.set(newTotal);
}
const pageSizeVal = pageSize.get();
const listData = listDataGetter(rawData); // 获取数据数组
// 如果追加数据,才更新data
if (append) {
// 如果是reset则先清空数据
isReset.current && data.set([]);
const cacheData: any[] = data.get();
if (refreshPage === undefined) {
data.set([...cacheData, ...listData]);
} else if (refreshPage) {
// 如果是刷新页面,则是替换那一页的数据
cacheData.splice((refreshPage - 1) * pageSizeVal, pageSizeVal, ...listData);
// 直接设置cacheData无效的,mobx内部有判断是否变化
data.set(Array.prototype.concat.apply([], cacheData));
}
} else {
data.set(listData);
}
});
});
states.onError(({ error }) => {
runInAction(() => {
isError.set(true);
});
});
// 请求成功与否,都要重置它们
states.onComplete(() => {
isReset.current = false;
requestCountInReseting.current = 0;
// 取消下拉刷新
wx.stopPullDownRefresh();
});
onSuccess
中就是对返回的data
做操作,如果是追加模式就组合在一起,翻页就直接替换。onError
就是设置error
。这个回调我没有发现官方代码有使用,好像没有对错误做操作。onComplete
复原状态,这里我加了一个停止下拉刷新。
2.4. 导出操作函数
这里有点和官方源码不一样,小程序的状态并不是双向的,更像是react
是单向的。所以直接修改page
达到翻页会有点繁琐。
因为你得先把这个hook
返回的page
放入小程序的data
中,然后setData
修改page
值,然后在修改page
的同时修改hook
返回的page
。这样才能视图和hook
的状态一致。
所以我将翻页的方法改成调用函数。
ts
const getItemIndex = (item: any) => data.get().findIndex((i: any) => i.id === item.id);
/**
* 加载下一页
*/
const nextPage = () => {
runInAction(() => {
if (isError.get()) {
// 上次请求失败,重发
refresh();
} else if (!isLastPage.get()) {
page.set(page.get() + 1);
}
});
};
/**
* 刷新指定页码数据,此函数将忽略缓存强制发送请求
* 如果未传入页码则会刷新当前页
* 如果传入一个列表项,将会刷新此列表项所在页,只对append模式有效
* @param pageOrItemPage 刷新的页码或列表项
*/
const refresh = async (pageOrItemPage = page.get()) => {
let refreshPage = pageOrItemPage;
if (append) {
if (isNaN(pageOrItemPage)) {
const itemIndex = getItemIndex(pageOrItemPage);
refreshPage = Math.floor(itemIndex / pageSize.get()) + 1;
}
// 更新当前页数据
await send(refreshPage, true);
} else {
// 整页翻页模式不支持传列表项
if (!isNaN(refreshPage)) {
// 页数相等,则刷新当前页,否则fetch数据
if (refreshPage === page.get()) {
await send(undefined, true);
} else {
await fetch(getHandlerMethod(refreshPage), true);
}
}
}
};
/**
* 从第${initialPage}页开始重新加载列表,并清空缓存
*/
const reload = () => {
runInAction(() => {
isReset.current = true;
page.get() === initialPage ? send() : page.set(initialPage);
});
};
- 我只实现了常用的函数,其他的函数根据这个思路自行实现
nextPage
主要是实现翻页请求refresh
总体是赋值的源码,所以除了刷新当前页,还有根据传入的数据项,刷新数据项所在的页面(我没测试过)。reload
重置页面
2.5. return
ts
return {
...state,
page,
pageSize,
data,
pageCount,
total,
isLastPage,
refresh,
isError,
reload,
nextPage,
}
3. 使用
用上一篇文章封装的mapRequestHook
,使用这个hook
ts
const priceRange = observable.box<undefined | number>(undefined);
Component({
behaviors: [
mapRequestHook(() => ({
productListPage: usePagination(
(page, size) => getProductList({ page, size, priceRange: priceRange.get() }),
{
initialPageSize: 10,
// 有个可变参数priceRange
watchingStates: [priceRange],
},
),
})),
],
onPullDownRefresh() {
// 下拉刷新
this.data.productListPage.reload();
},
onReachBottom() {
// 触底下一页
this.data.productListPage.nextPage();
},
})
html
<view
wx:for="{{productListPage.data}}"
></view>
- 这里注意一下,
watchingStates
中的值必须是observable.box
后的数据,也就是mobx
可以监听的数据。