工具类-列表请求工具 useList

useList

用于列表请求的基于 vue 3 的 hooks,接收请求函数、请求参数等数据,自动生成请求请求函数,分页信息等

本文有涉及到 http 请求工具和接口返回格式的内容:

  1. http 工具:一个基于 axios 封装的请求工具
  2. 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);
  };
  ...
}
相关推荐
tech_zjf35 分钟前
装饰器:给你的代码穿上品如的衣服
前端·typescript·代码规范
三棵杨树3 小时前
TypeScript从零开始(三):基础类型下
typescript
zoomdong5 小时前
10x 提升!TypeScript 宣布使用 Go 重写
前端·typescript
蒜香拿铁7 小时前
【typescript基础篇】(第三章) 函数
前端·typescript
蒜香拿铁7 小时前
【typescript基础篇】(第三章) 接口
前端·typescript
kangyouwei8 小时前
TS中Omit如何在enum枚举类型上使用
typescript
只会写Bug的程序员9 小时前
面试之《TypeScript泛型》
前端·面试·typescript
青春路上的小蜜蜂19 小时前
鸿蒙——实操开发自定义Hivigor插件并发布插件
typescript·harmonyos·plugin·hvigor·自定义插件
Pro_er20 小时前
Vue3组合式API终极指南:从原理到实战,彻底掌握高效开发!
vue·前端开发
觉醒法师1 天前
HarmonyOS开发 - 电商App实例二( 网络请求http)
前端·http·华为·typescript·harmonyos·ark-ts