分页请求策略
use hook
在使用扩展 hooks 前,确保你已熟悉了 alova 的基本使用。
为分页场景下设计的 hook,它可以帮助你自动管理分页数据,数据预加载,减少不必要的数据刷新,流畅性提高 300%,编码难度降低 50%。你可以在下拉加载和页码翻页两种分页场景下使用它,此 hook 提供了丰富的特性,助你的应用打造性能更好,使用更便捷的分页功能。
特性
- 丰富全面的分页状态;
- 丰富全面的分页事件;
- 更改 page、pageSize 自动获取指定分页数据;
- 数据缓存,无需重复请求相同参数的列表数据;
- 前后页预加载,翻页不再等待;
- 搜索条件监听自动重新获取页数;
- 支持列表数据的新增、编辑、删除;
- 支持刷新指定页的数据,无需重置;
- 请求级搜索防抖,无需自行维护;
使用
渲染列表数据
- vue
- react
- svelte
- solid
<template>
<div
v-for="item in data"
:key="item.id">
<span>{{ item.name }}</span>
</div>
<button @click="handlePrevPage">上一页</button>
<button @click="handleNextPage">下一页</button>
<button @click="handleSetPageSize">设置每页数量</button>
<span>共有{{ pageCount }}页</span>
<span>共有{{ total }}条数据</span>
</template>
<script setup>
import { queryStudents } from './api.js';
import { usePagination } from 'alova/client';
const {
// 加载状态
loading,
// 列表数据
data,
// 是否为最后一页
// 下拉加载时可通过此参数判断是否还需要加载
isLastPage,
// 当前页码,改变此页码将自动触发请求
page,
// 每页数据条数
pageSize,
// 分页页数
pageCount,
// 总数据量
total
} = usePagination(
// Method实例获取函数,它将接收page和pageSize,并返回一个Method实例
(page, pageSize) => queryStudents(page, pageSize),
{
// 请求前的初始数据(接口返回的数据格式)
initialData: {
total: 0,
data: []
},
initialPage: 1, // 初始页码,默认为1
initialPageSize: 10 // 初始每页数据条数,默认为10
}
);
// 翻到上一页,page值更改后将自动发送请求
const handlePrevPage = () => {
page.value--;
};
// 翻到下一页,page值更改后将自动发送请求
const handleNextPage = () => {
page.value++;
};
// 更改每页数量,pageSize值更改后将自动发送请求
const handleSetPageSize = () => {
pageSize.value = 20;
};
</script>
import { queryStudents } from './api.js';
import { usePagination } from 'alova/client';
const App = () => {
const {
// 加载状态
loading,
// 列表数据
data,
// 是否为最后一页
// 下拉加载时可通过此参数判断是否还需要加载
isLastPage,
// 当前页码,改变此页码将自动触发请求
page,
// 每页数据条数
pageSize,
// 分页页数
pageCount,
// 总数据量
total,
// 更新状态
update
} = usePagination(
// Method实例获取函数,它将接收page和pageSize,并返回一个Method实例
(page, pageSize) => queryStudents(page, pageSize),
{
// 请求前的初始数据(接口返回的数据格式)
initialData: {
total: 0,
data: []
},
initialPage: 1, // 初始页码,默认为1
initialPageSize: 10 // 初始每页数据条数,默认为10
}
);
// 翻到上一页,page值更改后将自动发送请求
const handlePrevPage = () => {
update({
page: page - 1
});
};
// 翻到下一页,page值更改后将自动发送请求
const handleNextPage = () => {
update({
page: page + 1
});
};
// 更改每页数量,pageSize值更改后将自动发送请求
const handleSetPageSize = () => {
update({
pageSize: 20
});
};
return (
<div>
{data.map(item => (
<div key={item.id}>
<span>{item.name}</span>
</div>
))}
<button onClick={handlePrevPage}>上一页</button>
<button onClick={handleNextPage}>下一页</button>
<button onClick={handleSetPageSize}>设置每页数量</button>
<span>共有{pageCount}页</span>
<span>共有{total}条数据</span>
</div>
);
};
<script>
import { queryStudents } from './api.js';
import { usePagination } from 'alova/client';
const {
// 加载状态
loading,
// 列表数据
data,
// 是否为最后一页
// 下拉加载时可通过此参数判断是否还需要加载
isLastPage,
// 当前页码,改变此页码将自动触发请求
page,
// 每页数据条数
pageSize,
// 分页页数
pageCount,
// 总数据量
total
} = usePagination(
// Method实例获取函数,它将接收page和pageSize,并返回一个Method实例
(page, pageSize) => queryStudents(page, pageSize),
{
// 请求前的初始数据(接口返回的数据格式)
initialData: {
total: 0,
data: []
},
initialPage: 1, // 初始页码,默认为1
initialPageSize: 10 // 初始每页数据条数,默认为10
}
);
// 翻到上一页,page值更改后将自动发送请求
const handlePrevPage = () => {
$page--;
};
// 翻到下一页,page值更改后将自动发送请求
const handleNextPage = () => {
$page++;
};
// 更改每页数量,pageSize值更改后将自动发送请求
const handleSetPageSize = () => {
$pageSize = 20;
};
</script>
{#each $data as item}
<div>
<span>{item.name}</span>
</div>
{/each}
<button on:click="{handlePrevPage}">上一页</button>
<button on:click="{handleNextPage}">下一页</button>
<button on:click="{handleSetPageSize}">设置每页数量</button>
<span>共有{pageCount}页</span>
<span>共有{total}条数据</span>
import { queryStudents } from './api.js';
import { usePagination } from 'alova/client';
const App = () => {
const {
// 加载状态
loading,
// 列表数据
data,
// 是否为最后一页
// 下拉加载时可通过此参数判断是否还需要加载
isLastPage,
// 当前页码,改变此页码将自动触发请求
page,
// 每页数据条数
pageSize,
// 分页页数
pageCount,
// 总数据量
total,
// 更新状态
update
} = usePagination(
// Method实例获取函数,它将接收page和pageSize,并返回一个Method实例
(page, pageSize) => queryStudents(page, pageSize),
{
// 请求前的初始数据(接口返回的数据格式)
initialData: {
total: 0,
data: []
},
initialPage: 1, // 初始 页码,默认为1
initialPageSize: 10 // 初始每页数据条数,默认为10
}
);
// 翻到上一页,page值更改后将自动发送请求
const handlePrevPage = () => {
update({
page: page() - 1
});
};
// 翻到下一页,page值更改后将自动发送请求
const handleNextPage = () => {
update({
page: page() + 1
});
};
// 更改每页数量,pageSize值更改后将自动发送请求
const handleSetPageSize = () => {
update({
pageSize: 20
});
};
return (
<div>
{data().map(item => (
<div key={item.id}>
<span>{item.name}</span>
</div>
))}
<button onClick={handlePrevPage}>上一页</button>
<button onClick={handleNextPage}>下一页</button>
<button onClick={handleSetPageSize}>设置每页数量</button>
<span>共有{pageCount()}页</span>
<span>共有{total()}条数据</span>
</div>
);
};
指定分页数据
每个分页数据接口返回的数据结构各不相同,因此我们需要分别告诉usePagination
列表数据与总条数,从而帮助我们管理分页数据。
假如你的分页接口返回的数据格式是这样的:
interface PaginationData {
totalNumber: number;
list: any[];
}
此时你需要通过函数的形式返回列表数据与总条数。
usePagination((page, pageSize) => queryStudents(page, pageSize), {
// ...
total: response => response.totalNumber,
data: response => response.list
});
如果不指定 total 和 data 回调函数,它们将默认通过以下方式获取数据。
const total = response => response.total;
const data = response => response.data;
data 回调函数必须返回一个列表数据,表示分页中所使用的数据集合,而 total 主要用于计算当前页数,在 total 回调函数中如果未返回数字,将会通过请求的列表数量是否少于 pageSize 值来判断当前是否为最后一页,这一般用于下拉加载时使用。
开启追加模式
默认情况下,翻页时会替换原有的列表数据,而追加模式是在翻页时会将下一页的数据追加到当前列表底部,常见的使用场景是下拉加载更多。
usePagination((page, pageSize) => queryStudents(page, pageSize), {
// ...
append: true
});
预加载相邻页数据
为了让分页提供更好的体验,在当前页的上一页和下一页满足条件时将会自动预加载,这样在用户翻页时可直接显示数据而不需要等待,这是默认的行为。如果你不希望预加载相邻页的数据,可通过以下方式关闭。
usePagination((page, pageSize) => queryStudents(page, pageSize), {
// ...
preloadPreviousPage: false, // 关闭预加载上一页数据
preloadNextPage: false // 关闭预加载下一页数据
});
在开启预加载时,并不会一味地加载下一页,需要满足以下两个条件:
- 预加载是基于缓存的,用于分页加载的 Method 实例必须开启缓存,默认情况下 get 请求会有 5 分钟的 memory 缓存,如果是非 get 请求或者全局关闭了缓存,你还需要在这个 Method 实例中单独设置
cacheFor
开启缓存。 - 根据
total
和pageSize
参数判断出下一页还有数据。
除了onSuccess、onError、onComplete
请求事件外,在触发了预加载时,你还可以通过fetching
来获知预加载状态,还可以通过onFetchSuccess、onFetchError、onFetchComplete
来监听预加载请求的事件。
const {
// 预加载状态
fetching,
// 预加载成功事件绑定函数
onFetchSuccess,
// 预加载错误事件绑定函数
onFetchError,
// 预加载完成事件绑定函数
onFetchComplete
} = usePagination((page, pageSize) => queryStudents(page, pageSize), {
// ...
});
监听筛选条件
很多时候列表需要通过条件进行筛选,此时可以通过usePagination
的状态监听来触发重新请求,这与 alova 提供的useWatcher
是一样的。
例如通过学生姓名、学生年级进行筛选。
- vue
- react
- svelte
- solid
<template>
<input v-model="studentName" />
<select v-model="clsName">
<option value="1">Class 1</option>
<option value="2">Class 2</option>
<option value="3">Class 3</option>
</select>
<!-- ... -->
</template>
<script setup>
import { ref } from 'vue';
import { queryStudents } from './api.js';
import { usePagination } from 'alova/client';
// 搜索条件状态
const studentName = ref('');
const clsName = ref('');
const {
// ...
} = usePagination(
(page, pageSize) => queryStudents(page, pageSize, studentName.value, clsName.value),
{
// ...
watchingStates: [studentName, clsName]
}
);
</script>
import { queryStudents } from './api.js';
import { usePagination } from 'alova/client';
import { useState } from 'react';
const App = () => {
// 搜索条件状态
const [studentName, setStudentName] = useState('');
const [clsName, setClsName] = useState('');
const {
// ...
} = usePagination(
(page, pageSize) => queryStudents(page, pageSize, studentName, clsName),
{
// ...
watchingStates: [studentName, clsName]
}
);
return (
<input value={studentName} onChange={({ target }) => setStudentName(target.value)} />
<select value={clsName} onChange={({ target }) => setClsName(target.value)}>
<option value="1">Class 1</option>
<option value="2">Class 2</option>
<option value="3">Class 3</option>
</select>
// ...
);
};
<script>
import { queryStudents } from './api.js';
import { usePagination } from 'alova/client';
import { writable } from 'svelte/store';
// 搜索条件状态
const studentName = writable('');
const clsName = writable('');
const {
// ...
} = usePagination((page, pageSize) => queryStudents(page, pageSize, $studentName, $clsName), {
// ...
watchingStates: [studentName, clsName]
});
</script>
<input bind:value="{studentName}" />
<select bind:value="{clsName}">
<option value="1">Class 1</option>
<option value="2">Class 2</option>
<option value="3">Class 3</option>
</select>
<!-- ... -->
import { queryStudents } from './api.js';
import { usePagination } from 'alova/client';
import { createSignal } from 'solid-js';
const App = () => {
// 搜索条件状态
const [studentName, setStudentName] = createSignal('');
const [clsName, setClsName] = createSignal('');
const {
// ...
} = usePagination(
(page, pageSize) => queryStudents(page, pageSize, studentName(), clsName()),
{
// ...
watchingStates: [studentName, clsName]
}
);
return (
<input value={studentName()} onChange={({ target }) => setStudentName(target.value)} />
<select value={clsName()} onChange={({ target }) => setClsName(target.value)}>
<option value="1">Class 1</option>
<option value="2">Class 2</option>
<option value="3">Class 3</option>
</select>
// ...
);
};
与useWatcher
相同,你也可以通过指定debounce
来实现请求防抖,具体可参考useWatcher 的 debounce 参数设置。
usePagination((page, pageSize) => queryStudents(page, pageSize, studentName, clsName), {
// ...
debounce: 300 // 防抖参数,单位为毫秒数,也可以设置为数组对watchingStates单独设置防抖时间
});
需要注意的是,debounce
是通过 useWatcher 中的请求防抖实现的。监听状态末尾分别还有 page 和 pageSize 两个隐藏的监听状态,也可以通过 debounce 来设置。
举例来说,当watchingStates
设置了[studentName, clsName]
,内部将会监听[studentName, clsName, page, pageSize]
,因此如果需要对 page 和 pageSize 设置防抖时,可以指定为[0, 0, 500, 500]
。
关闭初始化请求
默认情况下,usePagination
会在初始化时发起请求,但你也可以使用immediate
关闭它,并在后续通过send
函数,或者改变page
或pageSize
,以及watchingStates
等监听状态来发起请求。
usePagination((page, pageSize) => queryStudents(page, pageSize, studentName, clsName), {
// ...
immediate: false
});
列表操作函数
usePagination 提供了功能完善的列表操作函数,它可以在不重新请求列表的情况下,做到与重新请求列表一致的效果,大大提高了页面的交互体验,具体的函数说明继续往下看吧!
插入列表项
你可以用它插入列表项到列表任意位置,它将会在插入之后去掉末尾的一项,来保证和重新请求当前页数据一致的效果。
/**
* 插入一条数据
* 如果未传入index,将默认插入到最前面
* 如果传入一个列表项,将插入到这个列表项的后面,如果列表项未在列表数据中将会抛出错误
* @param item 插入项
* @param indexOrItem 插入位置(索引)
*/
declare function insert(item: LD[number], indexOrItem?: number | LD[number]): void;
以下为非 append 模式下(页码翻页场景),返回第一页再插入列表项的示例:
page.value = 1;
nextTick(() => {
insert(newItem, 0);
});
以下为在append 模式下(下拉加载场景),插入列表项后滚动到最顶部的示例:
insert(newItem, 0);
nextTick(() => {
window.scrollTo(0, {});
});
你也可以将insert
的第二个参数指定为列表项,当查找到这个列表项的相同引用时,插入项将插入到这个列表项的后面。
insert(newItem, afterItem);
为了让数据正确,insert 函数调用会清除全部缓存。
移除列表项
在下一页有缓存的情况下,它将会在移除一项后使用下一页的缓存补充到列表项尾部,来保证和重新请求当前页数据一致的效果,在append 模式和非 append 模式下表现相同。
/**
* 移除一条数据
* 如果传入的是列表项,将移除此列表项,如果列表项未在列表数据中将会抛出错误
* @param position 移除的索引或列表项
*/
declare function remove(position: number | LD[number]): void;
你也可以将remove
的第二个参数指定为列表项,当查找到这个列表项的相同引用时,将会移除此列表项。
但在以下两种情况下,它将重新发起请求刷新对应页的数据:
- 下一页没有缓存
- 同步连续调用了超过下一页缓存列表项的数据,缓存数据已经不够补充到当前页列表了。
为了让数据正确,remove 函数调用会清除全部缓存。
更新数据项
当你想要更新列表项时,使用此函数实现。
/**
* 替换一条数据
* 当position传入数字时表示替换索引,负数表示从末尾算起,当 position 传入的是列表项,将替换此列表项,如果列表项未在列表数据中将会抛出错误
* @param item 替换项
* @param position 替换位置(索引)或列表项
*/
declare function replace(
item: LD extends any[] ? LD[number] : any,
position: number | LD[number]
): void;
你也可以将replace
的第二个参数指定为列表项,当查找到这个列表项的相同引用时,将会替换此列表项。
刷新指定页的数据
当你在数据操作后不希望本地更新列表项,而是重新请求服务端的数据,你可以用 refresh 刷新任意页的数据,而不需要重置列表数据让用户又从第一页开始浏览。
/**
* 刷新指定页码数据,此函数将忽略缓存强制发送请求
* 如果未传入页码则会刷新当前页
* 如果传入一个列表项,将会刷新此列表项所在页
* @param pageOrItemPage 刷新的页码或列表项
* @returns [v3.1.0+]包含响应数据的promise
*/
declare function refresh(pageOrItemPage?: number | LD[number]): Promise<AG['Responded']>;
在 append 模式下,你可以将refresh
的参数指定为列表项,当查找到这个列表项的相同引用时,刷新此列表项所在页数的数据。