likes
comments
collection
share

基于Vue3+Typescript封装的useRequest网络请求hook封装背景 在如今前后端分离的开发模式中,我们

作者站长头像
站长
· 阅读数 31

封装背景

在如今前后端分离的开发模式中,我们有很多需要通过异步请求获取数据的场景,在此过程中会可能存在有很多的处理,如 loading、错误捕获、数据处理、请求前校验等。

在一般的情况下,我们实现基本的异步请求逻辑,往往会包含数据、后端接口、错误处理等,如下代码所示:

 const list1 = ref<Item1[]>([]);
 const list2 = ref<Item2[]>([]);
 const userInfo = ref<Record<string, any>>({});
 ​
 async function getList1() {
   try {
     const result1 = await getList1Api();
     list1.value = result1;
   } catch (err) {
     // ... 错误处理
   }
 }
 ​
 async function getList2() {
   try {
     const result2 = await getList2Api();
     list2.value = result2;
   } catch (err) {
     // ... 错误处理
   }
 }
 ​
 async function getUserInfo() {
   try {
     const result3 = await getUserInfoApi({id:3});
     userInfo.value = result3;
   } catch (err) {
     // ... 错误处理
   }
 }
 ​
 getList1();
 getList2();
 getUserInfo();

这样写确实没什么问题,毕竟是能完成业务需求的(人和代码有一个能跑就行)。但是这样的代码看似没重复,但好像又全是重复,毕竟从宏观的角度上看,无非就是三件事

  1. 声明初始数据
  2. 声明请求函数
  3. 调用函数发起请求,更新数据

基本流程

作为一名(cv战士)爱敲代码的前端,而且用的是vue3 + Typescript,我还是想结合响应式 API 封装成一个 hook 进行使用,以达到减少编写重复代码的目的

 // 接口函数
 type ApiFn = (...params: any[]) => Promise<any>;
 ​
 /**
  * 用于网络请求的hook
  * @param apiFn 获取数据的接口
  * @returns [接口的数据, 异步请求函数]
  */
 function useRequest(apiFn: ApiFn): [Ref<any>, (params?: any) => Promise<any>] {
   const data: Ref<any> = ref();
 ​
   async function request(params?: any) {
     try {
       const result = await apiFn(params);
       data.value = result;
     } catch (error) {}
   }
 ​
   // 以数组的形式返回
   return [data, request];
 }

那么现在就可以在 .vue 文件中进行使用了, 使用方式如下所示:

 import useRequest from './hooks/useRequest';
 ​
 // 这是使用数组是因为可以防止命名冲突
 const [list1, getList1] = useRequest(getList1Api);
 const [list2, getList2] = useRequest(getList2Api);
 const [userInfo, getUserInfo] = useRequest(getUserInfoApi);
 ​
 getList1();
 getList2();
 getUserInfo({id:3});

现在的代码看起来就简洁多了,而且每个数据以及该数据相关的请求函数都组合在了一起。

初始化数据

目前 data 的初始值的 undefined,如果我直接将 data 放在 <template> 中使用,在某些情况下会出问题

基于Vue3+Typescript封装的useRequest网络请求hook封装背景 在如今前后端分离的开发模式中,我们

 <template>
   <el-table v-if="list1.length > 0" :data="list1">
     <el-table-column></el-table-column>
     <!-- ... -->
   </el-table>
 </template>

例如上面这种情况,需要访问 list1.length 属性时,由于 list1 初始时是 undefined,这就会造成访问了undefined.length,毫无疑问肯定是会报错的

基于这种情况,我给 useRequest 这个 hook 假如第二个参数 options,其中包含一项是 defaultData,用于确定数据的初始值

 // useRequest的返回值类型
 type UseRequestReturn = [Ref<any>, (params?: any) => Promise<any>];
 // useRequest的配置项
 interface UseRequestOption {
   defaultData?: any;
 }
 ​
 ​
 function useRequest(
   apiFn: ApiFn,
   { defaultData }: UseRequestOption = {}
 ): UseRequestReturn {
   // 将传入的defaultData赋值给data
   const data = ref<any>(defaultData as any) as Ref<any>;
   ...
   return [data, request];
 }

在使用 hook 时传入初始化的数据,这样直接在模板使用时访问其方法就不会出现报错了

 const [list1, getList1] = useRequest(getList1Api, { defaultData: [] });
 const [list2, getList2] = useRequest(getList2Api, { defaultData: [] });
 const [userInfo, getUserInfo] = useRequest(getUserInfoApi, { defaultData: {} });
 ​
 getList1();
 getList2();
 getUserInfo({id:3});

这里可能有人会问怎么全使用 any,因为我( 使用 anyScript )想先把功能实现,所有先使用 any 避免类型报错,在我后面会补全这些类型。

自动执行以及默认参数

目前请求的基本流程已经实现了,接下来需要对其功能进行扩展,那么扩展基本是围绕着 hook 中的 request 函数来进行

基于Vue3+Typescript封装的useRequest网络请求hook封装背景 在如今前后端分离的开发模式中,我们

首先我想在调用 hook 时,可以在其内部自动调用一次 request 函数,那么在组件中就不需要再手动调用,而是等到某个事件(如 click,change 等)触发时再手动调用

那么我在 options 配置中加入一个属性 automatic ,如果该属性为 true,则在 hook 内部调用一次

 function useRequest(
   apiFn: ApiFn,
   { defaultData, automatic }: UseRequestOption = {}
 ): UseRequestReturn {
   const data = ref<any>(defaultData as any) as Ref<any>;
 ​
   async function request(params?: any) {
       ...
   }
 ​
   // 这里如果automatic为true,则调用一次request
   if (automatic) {
     request();
   }
 ​
   return [data, request];
 }

那么首次调用时需要携带参数该怎么办?例如上面的 getUserInfo 函数,类似这种情况,我在配置项中多加一个defaultParams 属性,用于第一次调用时传递的参数

 function useRequest(
   apiFn: ApiFn,
   { automatic, defaultData, defaultParams }: UseRequestOption = {}
 ): UseRequestReturn {
   const data = ref<any>(defaultData as any) as Ref<any>;
 ​
   async function request(params?: any) {
     ...
   }
 ​
   if (automatic) {
     request(defaultParams);
   }
 ​
   return [data, request];
 }

在使用该 hook 时,可以通过配置进行首次自动调用以及传参

 const [list1, getList1] = useRequest(getList1Api, { defaultData: [], automatic: true });
 const [list2, getList2] = useRequest(getList2Api, { defaultData: [], automatic: true });
 const [userInfo, getUserInfo] = useRequest(getUserInfoApi, {
   defaultData: {},
   automatic: true,
   defaultParams: { id: 3 }
 });

数据处理配置

在大多数的情况下,我们从接口拿到的数据是不能直接放到页面上使用的,需要对接口返回的数据做进一步处理

 // 拿到的数组需要对某项进行单独处理:
 async function getList1() {
   const result1 = await getList1Api();
   list1.value =
     result1?.map((item) => ({
       ...item,
       total: item.count1 + item.count2
     })) || [];
 }
 ​
 // 需要的数据在接口返回数据的某一项中
 async function getList2() {
   const result2 = await getList2Api();
   list2.value = result2.list || []
   // ... 
 }

在这种情况下,我给 useRequest 这个 hooks 加入一个配置项 formatter,该配置项是一个函数,并把从接口拿到的数据以参数形式传入,在 formatter 函数中处理完成后进行 return

 interface UseRequestOption {
   defaultData?: any;
   automatic?: boolean;
   defaultParams?: any;
   formatter?: (data: any) => any; // 处理数据的配置项
 }
 ​
 function useRequest(
   apiFn: ApiFn,
   { automatic, defaultData, defaultParams, formatter }: UseRequestOption = {}
 ): UseRequestReturn {
   const data = ref<any>(defaultData as any) as Ref<any>;
 ​
   async function request(params?: any) {
     try {
       const result = await apiFn(params);
       // 这里如果有formatter函数,则调用,并把接口数据当参数传入
       // 在formatter中处理完成后,将处理好的数据返回
       // 这样就可以将处理后的数据赋值给data
       data.value = formatter?.(result) ?? result;
     } catch (error) {}
   }
 ​
   if (automatic) {
     request(defaultParams);
   }
 ​
   return [data, request];
 }

例如上述的 list1 和 list2 需要额外处理,那么通过 formatter 配置项进行处理即可

 const [list1, getList1] = useRequest(getList1Api, {
   defaultData: [],
   automatic: true,
   formatter: (result: any) => 
     result.map((item) => ({ ...item, total: item.count1 + item.count2 }))
 });
 ​
 const [list2, getList2] = useRequest(getList2Api, {
   defaultData: [],
   automatic: true,
   formatter: (result: any) => result.list
 });

加入 loading

在一些表格或者某些特定区域的数据请求中,在请求未完成之前,往往需要展示一个 loading 效果,这个loading效果可能是全屏的,有可能是局部的。

那么我就在这个 hook 中多加一个记录 loading 状态的数据进行暴露,并且在配置中决定是展示全屏 loading 还是局部 loading

 type UseRequestReturn = [Ref<any>, (params?: any) => Promise<any>, Ref<boolean>];
 ​
 interface UseRequestOption {
   defaultData?: any;
   automatic?: boolean;
   defaultParams?: any;
   fullScreenLoading?: boolean; // 控制是全屏loading还是局部loading
   formatter?: (data: any) => any;
 }
 ​
 function useRequest(
   apiFn: ApiFn,
   { automatic, defaultData, defaultParams, formatter, fullScreenLoading }: UseRequestOption = {}
 ): UseRequestReturn {
   // useLoading是基于element封装的一个loading Hook
   const [showLoading, closeLoading] = useLoading();
   const data = ref<any>(defaultData as any) as Ref<any>;
   // 多加一个loading状态
   const loading = ref(false);
 ​
   async function request(params?: any) {
     // 如果fullScreenLoading为true,则开启全屏loading
     // 否则使用局部loading
     fullScreenLoading ? showLoading() : (loading.value = true);
     try {
       const result = await apiFn(params);
       data.value = formatter?.(result) || result;
     } catch (error) {
     } finally {
       // 重置loading状态
       fullScreenLoading ? closeLoading() : (loading.value = false);
     }
   }
 ​
   if (automatic) {
     request(defaultParams);
   }
 ​
   return [data, request, loading];
 }

请求周期钩子

一个基本的请求一般包含两个阶段:

  1. 请求前数据校验
  2. 请求成功/失败后的处理

在这个 hook 中,我依然通过配置项传入对应的处理函数,如 beforeRequest、onSuccess、onError

  • 请求前有可能需要进行某些校验,如参数校验,所以在 beforeRequest 中会将参数传入
  • 在请求成功后,可能需要拿到接口的的响应信息作进一步逻辑处理,或者拿到处理后的数据作处理,所以在 onSuccess 中会有两个参数:原始数据和处理后的数据
 interface UseRequestOption {
   defaultData?: any;
   automatic?: boolean;
   defaultParams?: any;
   fullScreenLoading?: boolean;
   formatter?: (data: any) => any;
   beforeRequest?: (params?:any) => boolean | void; // 请求前的钩子
   onSuccess?: (data: any, formatData:any) => void; // 请求成功的钩子
   onError?: (err: any) => void; // 请求失败的钩子
 }
 function useRequest(
   apiFn: ApiFn,
   {
     automatic,
     defaultData,
     defaultParams,
     formatter,
     fullScreenLoading,
     beforeRequest,
     onSuccess,
     onError
   }: UseRequestOption = {}
 ): UseRequestReturn {
   ...
   async function request(params?: any) {
     fullScreenLoading ? showLoading() : (loading.value = true);
     // 请求前的处理,如果传入了beforeRequest配置并且返回false
     // 那么该请求就不发出
     const isRequest = beforeRequest?.(params) ?? true;
     if (!isRequest) return;
     
     try {
       const result = await apiFn(params);
       data.value = formatter?.(result) || result;
       // 调用传入的onSuccess函数
       // 并把数据通过参数形式传入
       onSuccess?.(result, data.value);
     } catch (error: any) {
       // 错误处理
       onError?.(error);
     } finally {
       fullScreenLoading ? closeLoading() : (loading.value = false);
     }
   }
   ...
   return [data, request, loading];
 }

下面是使用该 hook 的方式

 const [list, getList] = useRequest(getListApi, {
   defaultData: [],
   automatic: true,
   beforeRequest: (params:any) => {
     if (xxx) {
       // 其他处理
       // 返回false,请求不会继续进行
       return false;
     }
   },
   formatter: (result: any) => result.list,
   onSuccess: (result: any, formatResult: any) => {
     /* 成功处理 */
   },
   onError: (err: any) => {
     /* 错误处理 */
   }
 });

现在看起来使用封装的 hook 进行异步请求,就能很明确请求的每个阶段该处理的事情,而不是把所有代码都糅合到一个函数里,可能看起来就更直观。

加入类型校验

先前为了把功能实现,于是 hook 内部的类型都使用了 any 进行替代,那么既然用上了 typescript,那就尽量避免 any 的存在,让其他人使用 hook 时具有对应的类型提示和检查。

在整个请求过程,其实就是往接口传入一些参数,得到服务器返回的数据,那么则有以下公式

 data = await fetch(params)

那么我的想法是:首先通过传入的 Api 函数,自动推断出需要传入的参数类型和返回值类型,所以我定义了 3 个泛型。

  • A:传入的 Api 函数类型
  • D:需要的数据类型,默认是接口响应的数据类型
  • P:传参类型,默认是接口的参数类型

现在函数签名如下:

 // 我们约定Api函数的参数使用对象形式传递,所以只有一个参数
 type ApiFn = (params?: any) => Promise<any>;
 type ApiReturn<A extends ApiFn> = Awaited<ReturnType<A>>;
 type UseRequestReturn<D, P> = [Ref<D>, (params?: P) => Promise<any>, Ref<boolean>];
 ​
 interface UseRequestOption<A extends ApiFn, D, P> {
   defaultData?: D; // 默认的数据类型为D
   automatic?: boolean;
   defaultParams?: P; // 默认的参数类型为P
   fullScreenLoading?: boolean;
   formatter?: (data: ApiReturn<A>) => D; // 通过接口响应的数据,处理成指定的数据类型
   beforeRequest?: (params?: P) => boolean | void;
   // 第一个参数是接口响应的数据,第二个参数处理后的数据
   onSuccess?: (data: ApiReturn<A>, formatData: D) => void; 
   onError?: (err: any) => void;
 }
 ​
 function useRequest<A extends ApiFn, D = ApiReturn<A>, P = Parameters<A>[0]>(
   apiFn: A,
   options: UseRequestOption<A, D, P> = {}
 ): UseRequestReturn<D, P> {}
  • 那么当使用这个 hook,并传入 Api 函数时,会自动推导类型
 export function getUserInfoApi(params: GetUserInfoParams) {
   return http.post<LoginResponse>({ url: Api.GetUserInfo, data: params });
 }

基于Vue3+Typescript封装的useRequest网络请求hook封装背景 在如今前后端分离的开发模式中,我们

  • hook中返回的数据和包装后的请求函数也会自动推导类型

基于Vue3+Typescript封装的useRequest网络请求hook封装背景 在如今前后端分离的开发模式中,我们

  • 当传入默认数据类型时,也会自动推导 info 的类型;传入默认参数时,也会自动推导 params 的类型

基于Vue3+Typescript封装的useRequest网络请求hook封装背景 在如今前后端分离的开发模式中,我们

  • 同理,通过 formatter 配置项处理后返回的数据也可以自动推导出 info 的数据类型,并且有相应的类型提示

基于Vue3+Typescript封装的useRequest网络请求hook封装背景 在如今前后端分离的开发模式中,我们

  • 如果需要限制数据的类型,而不是根据推导,则需要显示传入泛型进行限制
 interface UserInfo {
   id: number;
   name: string;
 }
 ​
 type GetUserInfoApi = typeof getUserInfoApi;
 const [info, getUserInfo] = useRequest<GetUserInfoApi, UserInfo, GetUserInfoParams>(getUserInfoApi);

基于Vue3+Typescript封装的useRequest网络请求hook封装背景 在如今前后端分离的开发模式中,我们

加入防抖和节流

在日常的开发中,对某个按钮加入防抖,或者某个功能加上节流还是很常见的,所以我在该 hook 中继续加上了防抖和节流的配置项

 interface UseRequestOption<A extends ApiFn, D, P> {
   throttleTime?: number; // 节流时间 ms
   debounceTime?: number; // 防抖时间 ms
   ...
 }
  
 function useRequest<A extends ApiFn, D = ApiReturn<A>, P = Parameters<A>[0]>(
   apiFn: A,
   {
     throttleTime,
     debounceTime,
     ...
   }: UseRequestOption<A, D, P> = {}
 ): UseRequestReturn<D, P> {
 ​
   async function request(params?: P) {
     ...
   }
 ​
   let run = request;
 ​
   // 如果传入了节流时间,则对request进行节流
   if (throttleTime) {
     const throttleRun = throttle(request, throttleTime);
     run = async (params?: P) => throttleRun(params);
   }
     
   // 如果传入了防抖时间,则对request进行防抖
   if (debounceTime) {
     const debounceRun = debounce(request, debounceTime);
     run = async (params?: P) => debounceRun(params);
   }
 ​
   ...
 ​
   return [data, run, loading];
 }

总结

因为这是我根据日常开发所封装的 hook,所以在功能方面还有不足,但已经能满足开发中的大部分场景,如果后续遇到相应的场景进行扩展,目前已经在项目中使用,并没有什么问题。

封装该 hook 主要是想进一步掌握 typescript 的使用,并且提高自身的编程思维,并且省去重复的逻辑编写,令请求函数的每个部分更加直观。

这里有不足之处就是,这个 hook 已经在使用,如果后面要扩展功能,还有考虑兼容原有的逻辑。而且所有代码在一个 hook 中,随着功能的扩展会导致代码越来越多。

同时也恳请各位大佬可以留言给个建议,因为在 typescript 类型这方面,我总觉得有更好的方式去做类型限制,但目前想到的就是这种方式,谢谢!

参考

源码

 import { throttle, debounce } from "lodash-es";
 import { ref, reactive, type Ref } from "vue";
 // 这个全屏loading是基于elementPlus封装
 import useLoading from "./useLoading";
 ​
 type ApiFn = (params?: any) => Promise<any>;
 type ApiReturn<A extends ApiFn> = Awaited<ReturnType<A>>;
 ​
 interface UseRequestOption<A extends ApiFn, D, P> {
   defaultData?: D;
   automatic?: boolean;
   defaultParams?: P;
   fullScreenLoading?: boolean;
   throttleTime?: number;
   debounceTime?: number;
   formatter?: (data: ApiReturn<A>) => D;
   beforeRequest?: (params?: P) => boolean | void;
   onSuccess?: (data: ApiReturn<A>, formatData: D) => void;
   onError?: (err: any) => void;
 }
 ​
 type UseRequestReturn<D, P> = [Ref<D>, (params?: P) => Promise<any>, Ref<boolean>];
 /**
  * @description 用于网络请求的hook
  */
 function useRequest<A extends ApiFn, D = ApiReturn<A>, P = Parameters<A>[0]>(
   apiFn: A,
   {
     automatic,
     defaultData,
     defaultParams,
     throttleTime,
     debounceTime,
     formatter,
     fullScreenLoading,
     beforeRequest,
     onSuccess,
     onError
   }: UseRequestOption<A, D, P> = {}
 ): UseRequestReturn<D, P> {
   const { showLoading, closeLoading } = useLoading();
 ​
   const data: Ref<D> = ref(defaultData) as Ref<D>;
   const loading = ref<boolean>(false);
 ​
   async function request(params?: P) {
     fullScreenLoading ? showLoading() : (loading.value = true);
     const isRequest = beforeRequest?.(params) ?? true;
     if (!isRequest) return;
     try {
       const result = await apiFn(params);
       data.value = formatter?.(result) ?? result;
       onSuccess?.(result, data.value);
     } catch (error) {
       onError?.(error);
     } finally {
       fullScreenLoading ? closeLoading() : (loading.value = false);
     }
   }
 ​
   let run = request;
 ​
   if (throttleTime) {
     const throttleRun = throttle(request, throttleTime);
     run = async (params?: P) => throttleRun(params);
   }
   if (debounceTime) {
     const debounceRun = debounce(request, debounceTime);
     run = async (params?: P) => debounceRun(params);
   }
 ​
   if (automatic) {
     request(defaultParams);
   }
     
   return [data, run, loading];
 }
 ​
 export default useRequest;
转载自:https://juejin.cn/post/7401408738514010149
评论
请登录