工具类-列表请求工具 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);
  };
  ...
}
相关推荐
曼陀罗10 小时前
import 一个js文件,报ts类型错误的解决思路
前端·typescript
lulu_063211 小时前
safari 浏览器输入框 focus时不显示那一闪一闪的图标
前端·css·vue·safari·element-plus
问道飞鱼13 小时前
【GIT知识】git进阶-hooks勾子脚本
git·hooks
RogerLHJ17 小时前
cocos creator 的 widget组件的使用及踩坑
typescript·游戏引擎·游戏程序·cocos2d
Java陈序员18 小时前
一个开源免费中后台模版!
vue.js·typescript·vite
羊小猪~~19 小时前
前端入门之VUE--vue组件化编程
前端·javascript·css·vue.js·vscode·vue·html
DARLING Zero two♡21 小时前
Microi吾码低代码平台:前端源码的本地运行探索
前端·低代码·开源·c#·vue
winfield8211 天前
TypeScript 编程,|| 和 ?? 的区别是什么?
前端·typescript
zzb15801 天前
Exp 智能协同管理系统-部门管理前端页面开发
前端·typescript·前端框架·vue
神仙姐姐QAQ2 天前
vue+mars3d叠加展示arcgis动态服务
arcgis·vue·cesium·mars3d