useList
用于列表请求的基于 vue 3 的 hooks,接收请求函数、请求参数等数据,自动生成请求请求函数,分页信息等
本文有涉及到 http 请求工具和接口返回格式的内容:
- http 工具:一个基于 axios 封装的请求工具
- ResponseData 接口:定义接口返回数据结构的 interface
ts
interface ResponseData<T = any> {
code: number;
data: T;
message: string;
}
详情可参考文章:工具类-基于 axios 的 http 请求工具 Request
实现一个简单的列表请求
ts
import { ref, Ref } from 'vue';
import { get, cloneDeep } from 'lodash-es';
import { ResponseData } from '@/http/type';
interface UseListConfig<P = any, T = any> {
request: {
/**
* 请求列表方法
*/
api: (params: P) => Promise<ResponseData<T[]>>;
/**
* 请求参数
*/
params?: P;
};
response?: {
/**
* 列表数据 默认 data
* 例: 响应数据为 { data: { list: [] } } 则传递 data.list;
*/
listDataKey?: string;
};
}
export function useList<P extends object = any, T = any>(config: UseListConfig<P, T>) {
const cacheConfig = cloneDeep(config);
const { api } = cacheConfig.request;
const { listDataKey = 'data' } = cacheConfig.response || {};
const params = ref(cloneDeep(cacheConfig.request.params || {}) as P) as Ref<P>;
const list = ref([]) as Ref<T[]>;
const handleSearch = async () => {
const res = await api(params.value as P);
// 更具 listDataKey 获取列表数据
list.value = get(res, listDataKey);
return res;
};
return {
params,
list,
handleSearch,
};
}
useList 定义了两个泛型,其中 P 代表请求的参数类型,T 代表列表数据每一项的类型,接收请求方法和请求参数等数据, 最后返回了请求参数变量 params,列表数据 list,还有发起列表请求的 handlerSearch 方法
在 vue 3 中使用
script
ts
import { useList } from '@/hooks/-useList';
import { ResponseData } from '@/http/type';
// 定义请求参数类型
interface GetListParams {
name: string;
}
// 定义请求项类型
interface ListItem {
id: number;
name: string;
}
// 模拟列表数据和 http 请求
const MOCK_LIST: ListItem[] = Array(100)
.fill(0)
.map((_, index) => ({
id: index + 1,
name: `list-item-${index + 1}`,
}));
const getList = async (params: GetListParams): Promise<ResponseData<ListItem[]>> => {
await new Promise(resolve => setTimeout(resolve, 3 * 1e3));
const list: ListItem[] = [];
if (params.name) list.push(...MOCK_LIST.filter(item => item.name.includes(params.name)));
else list.push(...MOCK_LIST);
return {
code: 200,
data: list,
message: 'success',
};
};
const { params, list, handleSearch } = useList<GetListParams, ListItem>({
request: {
api: getList,
params: {
name: '',
},
},
response: {
listDataKey: 'data',
},
});
template
html
<template>
<div>
<div>
<a-input v-model="params.name" placeholder="请输入名称" />
<a-button type="primary" @click="handleSearch()">查询</a-button>
</div>
<div>
<p v-for="item of list" :key="item.id">{{ item.name }}</p>
</div>
</div>
</template>
使用泛型定义好请求的参数和返回的内容的类型,向 useList 传入请求函数和参数,获得 params,list,以及 handlerSearch,将 params 的字段绑定到搜索的表单元素,点击搜索调用 handlerSearch 即可完成列表的请求
增加 loading
每次列表请求时需要给 列表增加一个加载中的文案或图表,每次手动去声明一个 loading,在 调用 handleSearch 前赋值为 true,调用结束后赋值为 false,可以实现控制列表的加载状态。
但是每个列表都要实现一遍过于麻烦和冗余,可以在 useList 中增加一个 loading 的变量并返回,在请求前后改变 loading 的值,实现加载状态的控制。
loading 的实现使用了 useLoading,可以查阅 工具类-useLoading
ts
export function useList<P extends object = any, T = any>(config: UseListConfig<P, T>) {
...
const { loading, executor } = useLoading();
const handleSearch = async () => {
const res = await executor(async () => api(params.value as P));
list.value = get(res, listDataKey);
return res;
};
return {
...
loading,
...
}
}
使用示例
script
ts
...
const { params, loading, list, handleSearch } = useList<GetListParams, ListItem>({
request: {
api: getList,
params: {
name: '',
},
},
});
template
html
<template>
<div>
<div>
<a-input v-model="params.name" placeholder="请输入名称" />
<a-button type="primary" @click="handleSearch()">查询</a-button>
</div>
<!-- v-loading 是使元素显示加载状态的指令 -->
<div v-loading="loading">
<p v-for="item of list" :key="item.id">{{ item.name }}</p>
</div>
</div>
</template>
处理分页信息
首先增加 UseListConfig 的分页信息类型定义
ts
interface UseListConfig<P = any, T = any> {
request: {
...
/**
* 分页信息-当前页数参数在 params 中的 key
* 默认: page
*/
pageNumKey?: string;
/**
* 分页信息-每页条数参数在 params 中的 key
* 默认: pageSize
*/
pageSizeKey?: string;
};
response?: {
...
/**
* 总条数字段的 key
* 例: 响应数据为 { data: { list: [], total: 0 } } 则传递 data.total;
* 默认 pageInfo.items
*/
listDataKey?: string;
};
}
在 handleSearch 中增加分页控制,增加 handleCurrentChange 和 handleSizeChange 方法
ts
export function useList<P extends object = any T = any>(config: UseListConfig<P, T>) {
...
const total = ref(0);
const handleSearch = async (pageNum = 1) => {
if (pageNumKey in (params.value as object)) {
(params.value as any)[pageNumKey] = pageNum;
}
const res = await executor(async () => api(params.value as P));
list.value = get(res, listDataKey);
total.value = get(res, totalKey);
return res;
};
/**
* 切换当前页码 刷新列表
*/
const handleCurrentChange = async (pageNum: number) => {
await handleSearch(pageNum);
};
/**
* 切换分页大小 刷新列表
*/
const handleSizeChange = async (pageSize: number) => {
if (pageSizeKey in (params.value as object)) {
(params.value as any)[pageSizeKey] = pageSize;
}
// 切换分页大小后,默认回到第一页
await handleSearch(1);
};
return {
...
handleSearch,
handleCurrentChange,
handleSizeChange,
}
}
使用示例
script
ts
import { useList } from '@/hooks/-useList';
import { ResponseData } from '@/http/type';
import Pagination from '@/components/pagination/index.vue'; // 分页的组件
interface GetListParams {
name: string;
page: number;
pageSize: number;
}
interface ListItem {
id: number;
name: string;
}
// 模拟列表数据和 http 请求
const MOCK_LIST: ListItem[] = Array(100)
.fill(0)
.map((_, index) => ({
id: index + 1,
name: `list-item-${index + 1}`,
}));
const getList = async (params: GetListParams): Promise<ResponseData<ListItem[]>> => {
await new Promise(resolve => setTimeout(resolve, 3 * 1e3));
let list: ListItem[] = [];
if (params.name) list = MOCK_LIST.filter(item => item.name.includes(params.name));
else list = MOCK_LIST;
list = list.slice(params.page * params.pageSize - params.pageSize, params.page * params.pageSize);
return {
code: 200,
data: list,
message: 'success',
pageInfo: {
items: MOCK_LIST.length,
},
};
};
const { params, total, loading, list, handleSearch, handleCurrentChange, handleSizeChange } =
useList<GetListParams, ListItem>({
request: {
api: getList,
params: {
name: '',
page: 1,
pageSize: 10,
},
pageNumKey: 'page',
pageSizeKey: 'pageSize',
},
response: {
listDataKey: 'data',
totalKey: 'pageInfo.items',
},
});
template
html
<template>
<div>
<div>
<a-input v-model="params.name" placeholder="请输入名称" />
<a-button type="primary" @click="handleSearch()">查询</a-button>
<pagination
:total="total"
:page-size="params.pageSize"
:current="params.page"
:show-total="true"
@change="handleCurrentChange"
@page-size-change="handleSizeChange"
/>
</div>
<!-- v-loading 是使元素显示加载状态的指令 -->
<div v-loading="loading">
<p v-for="item of list" :key="item.id">{{ item.name }}</p>
</div>
</div>
</template>
增加 reset 方法
在列表请求页中,经常有需要清空或重置搜索条件的需求,可以在 useList 中记录传入的初始 params,增加 handleReset 函数,将 params 变量的值赋值为 初始的 params 值
ts
export type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
export function useList<P extends object = any, T = any>(config: UseListConfig<P, T>) {
...
// 使用 readonly 约束 defaultParams,避免被更改
const defaultParams: DeepReadonly<P> = cloneDeep(cacheConfig.request.params || ({} as P));
const handleReset = () => {
params.value = cloneDeep(defaultParams as P);
handleSearch(); // 重置完立即发起搜索
};
return {
...
handleReset,
}
}
有可能 rest 函数不一定是将 params 变量重置为使用 useList 时传入的值,为了应付这种特殊情况,我们可以增加一个 handleCustomeReset 的参数,将重置的权限暴露出去
ts
interface UseListConfig<P = any, T = any> {
request: {
...
/**
* 自定义重置方法
*/
handleCustomReset?: (params: P, defaultParams: DeepReadonly<P>) => P;;
};
}
export function useList<P extends object = any, T = any>(config: UseListConfig<P, T>) {
...
const defaultParams = cloneDeep(cacheConfig.request.params || ({} as P));
const { handleCustomReset } = cacheConfig.request;
const handleReset = () => {
if (handleCustomReset) params.value = handleCustomReset(params.value, defaultParams);
else params.value = cloneDeep(defaultParams as P);
handleSearch(); // 重置完立即发起搜索
};
return {
...
handleReset,
}
}
增加 handleCustomReset 的可选项, 将当前 params 的值和 defaultParmas 传给 handleCustomReset,如果有传入 handleCustomReset,则重置时使用 handleCustomReset 的返回值赋值给 params 变量,若没有 handleCustomReset,则使用默认的重置方式
增加一些回调钩子
请求前的钩子
- handleValidate: 请求前校验,校验参数是否合理等
- handleParams:请求前处理参数
- resetApi: 重置列表请求方法
请求完成后的钩子
- handleResponseData:处理返回的列表数据
ts
interface UseListConfig<P = any, T = any> {
request: {
...
/**
* 自定义重置方法
*/
handleCustomReset?: (params: P, defaultParams: DeepReadonly<P>) => P;
/**
* 校验函数,校验参数是否合理等
* 返回 false 则不发起请求
*/
handleValidate?: (params: DeepReadonly<P>) => boolean;
/**
* 处理请求参数
*/
handleParams?: (params: DeepReadonly<P>) => P;
/**
* 重置请求方法
*/
resetApi?: (params: DeepReadonly<P>) => (params: P) => Promise<ResponseData<T[]>>;
};
response?: {
...
/**
* 处理响应数据
*/
handleResponseData?: (list: T[]) => T[];
};
}
export function useList<P extends object = any, T = any>(config: UseListConfig<P, T>) {
...
const {
handleValidate,
handleParams,
resetApi,
} = cacheConfig.request || {};
const {
handleResponseData,
} = cacheConfig.response || {};
const handleSearch = async (pageNum = 1) => {
if (pageNumKey in params.value) {
(params.value as any)[pageNumKey] = pageNum;
}
let _params = cloneDeep(params.value);
if (handleValidate && !handleValidate(_params)) return;
if (handleParams) _params = handleParams(_params);
if (resetApi) api = resetApi(_params);
const res = await executor(async () => api(params.value as P));
const _list = get(res, listDataKey);
if (handleResponseData) list.value = handleResponseData(_list);
else list.value = _list;
total.value = get(res, totalKey);
return res;
};
}
使用示例
ts
const getList = async (params: GetListParams): Promise<ResponseData<ListItem[]>> => {
console.log('getList');
await new Promise(resolve => setTimeout(resolve, 3 * 1e3));
...
};
const getListLongTime = async (params: GetListParams): Promise<ResponseData<ListItem[]>> => {
console.log('getListLongTime');
await new Promise(resolve => setTimeout(resolve, 10 * 1e3));
...
};
const { params, total, loading, list, handleSearch, handleCurrentChange, handleSizeChange } =
useList<GetListParams, ListItem>({
request: {
...
handleValidate(params) {
if (params.page <= 0) {
console.error('page 必须大于 0');
return false;
}
return true;
},
handleParams(params) {
return {
...params,
pageSize: Math.min(params.pageSize, 10), // 当 pageSize 小于 10 时,默认设置为 10
};
},
resetApi(params) {
// 根据 params.name 判断调用哪个接口
if (params.name.toLocaleLowerCase() === 'longtime') return getListLongTime;
return getList;
},
},
response: {
...
handleResponseData(list) {
// 将 list 中的 name 转为大写
return list.map(item => ({
...item,
name: item.name.toUpperCase(),
}));
},
},
});
增加防抖
在 UseListConfig 中增加 lazy 字段,接收一个以毫秒为单位的时间值作为搜索时函数的防抖时间
ts
interface UseListConfig<P = any, T = any> {
request: {
...
/**
* 搜索函数防抖延迟时间
* 默认不开启防抖
*/
lazy?: number;
...
};
response?: {
...
};
}
先实现一个用于防抖的函数,类似 lodash 的 debounce 函数
注:为什么不直接用 ladash 的 debounce,因为 debounce 的返回值类型不太符合需求
ts
type AnyFunction = (...args: any[]) => any;
const debounce = <T extends AnyFunction>(fn: T, lazy = 300): ((...args: Parameters<T>) => Promise<ReturnType<T>>) => {
let timer: number | null = null;
return (...args) =>
new Promise(resolve => {
if (timer) clearTimeout(timer);
timer = window.setTimeout(() => {
resolve(fn(...args));
}, lazy);
});
};
ts
export function useList<P extends object = any, T = any>(config: UseListConfig<P, T>) {
...
const {
...
lazy,
} = cacheConfig.request;
...
const _handleSearch = async (pageNum = 1) => {
const res = await api(params.value); // 去掉了 Loading 的 executor 函数执行
...
};
const handleSearch = async (pageNum = 1) => {
const func = lazy ? debounce(_handleSearch, lazy) : _handleSearch;
// 在这里执行 Loading 的 executor 函数,因为在防抖时间内也需要显示 Loading 状态
return executor(func, pageNum);
};
...
}