Vue3 TypeScript 实现useRequest详情

2022-10-14

本文介绍了Vue3 TypeScript实现useRequest详情,useRequest可能是目前社区中最强大,最接地气的请求类 Hooks了。可以覆盖99%的网络请求场景,无论是读还是写,无论是普通请求还是分页请求,无论是缓存还是防抖节流,通通都能支持,关于其介绍需要的小伙伴可以参考一下

目录
  • 前言:
  • 效果展示
  • Axios
    • interface
    • axios的简单封装
  • useRequest
    • 如何使用
    • 添加泛型支持
    • queryKey的问题
  • 完整代码
    • 结语

      前言:

      自从 Vue3 更新之后,算是投入了比较大的精力写了一个较为完善的Vue3.2 + Vite2 + Pinia + Naive UI的B端模版,在做到网络请求这一块的时候,最初使用的是VueRequestuseRequest,但是因为VueRequestuseRequestcancel关闭请求并不是真正的关闭,对我个人来说,还是比较介意,于是在参考aHooksVueRequest的源码之后,差不多弄了一个简易的useRequest,使用体验还算ok,但是因为个人能力以及公司业务的问题,我的版本只支持axios,不支持fetch,算是作为公司私有的库使用,没有考虑功能的大而全,也只按VueRequest的官网,实现了一部分我认为最重要的功能。

      写的比较混乱,中间是一部分思考,可以直接拖到最后看实现,再回来看一下我为什么选择这么做,欢迎讨论。

      效果展示

      一个基础的useRequest示例,支持发起请求 取消请求 请求成功信息 成功回调 错误捕获

      queryKey示例,单个useRequest管理多个相同请求。

      其余还是依赖更新 重复请求关闭 防抖 节流等功能

      Axios

      既然咱们使用TypeScriptaxios,为了使axios能满足咱们的使用需求以及配合TypeScript的编写时使用体验,咱们对axios进行一个简单的封装。

      interface

      // /src/hooks/useRequest/types.ts
      import { AxiosResponse, Canceler } from 'axios';
      import { Ref } from 'vue';
      // 后台返回的数据类型
      export interface Response<T> {
          code: number;
          data: T;
          msg: string;
      }
      // 为了使用方便,对 AxiosResponse 默认添加我们公用的 Response 类型
      export type AppAxiosResponse<T = any> = AxiosResponse<Response<T>>;
      
      // 为了 useRequest 使用封装的类型
      export interface RequestResponse<T> {
          instance: Promise<AppAxiosResponse<T>>;
          cancel: Ref<Canceler | undefined>;
      }

      axios的简单封装

      因为咱们现在没有接入业务,所以axios只需要简单的封装能支持咱们useRequest的需求即可。

      import { ref } from 'vue';
      import { AppAxiosResponse, RequestResponse } from './types';
      import axios, { AxiosRequestConfig, Canceler } from 'axios';
      const instance = axios.create({
        timeout: 30 * 1000,
        baseURL: '/api'
      });
      export function request<T>(config: AxiosRequestConfig): RequestResponse<T> {
        const cancel = ref<Canceler>();
        return {
          instance: instance({
            ...config,
            cancelToken: new axios.CancelToken((c) => {
              cancel.value = c;
            })
          }),
          cancel
        };
      }

      例:

      import { IUser } from '@/interface/User';
      export function getUserInfo(id: number) {
        return request<IUser>({
          url: '/getUserInfo',
          method: 'get',
          params: {
            id
          }
        });
      }

      需要注意的是,示例中的错误信息经过了统一性的封装,如果希望错误有一致性的表现,可以封装一个类型接收错误,建议与后台返回的数据结构一致。

      现在,咱们使用这个request函数,传入对应的泛型,就可以享受到对应的类型提示

      useRequest

      如何使用

      想要设计useRequest,那现在思考一下,什么样的useRequest使用起来,能让我们感到快乐,拿上面的基础示例queryKey示例来看,大家可以参考一下VueRequest或者aHooks的用法,我是看了他们的用法来构思我的设计的。

      比如一个普通的请求,我希望简单的使用dataloadingerr等来接受数据,比如:

      const { run, data, loading, cancel, err } = useRequest(getUserInfo, {
          manual: true
      })

      那 useRequest 的简单模型好像是这样的

      export function useRequest(service, options) {
          return {
              data,
              run,
              loading,
              cancel,
              err
          }
      }

      传入一个请求函数配置信息,请求交由useRequest内部接管,最后将data loading等信息返回即可。

      那加上queryKey

      const { run, querise } = useRequest(getUserInfo, {
          manual: true,
          queryKey: (id) => String(id) 
      })

      似乎还要返回一个querise,于是变成了

      export function useRequest(service, options) {
          return {
              data,
              run,
              loading,
              cancel,
              err,
              querise
          }
      }

      对应的querise[key]选项,还要额外维护data loading等属性,这样对于useRequest内部来说是不是太割裂了呢,大家可以尝试一下,因为我就是一开始做简单版本之后再来考虑queryKey功能的,代码是十分难看的。

      添加泛型支持

      上面的伪代码我们都没有添加泛型支持,那我们需要添加哪些泛型,上面request的例子其实比较明显了

      import { IUser } from '@/interface/User';
      export function getUserInfo(id: number) {
        return request<IUser>({
          url: '/getUserInfo',
          method: 'get',
          params: {
            id
          }
        });
      }

      对于id,作为请求参数,我们每一个请求都不确定,这里肯定是需要一个泛型的,IUser作为返回类型的泛型,需要被useRequest正确识别,必然也是需要一个泛型的。

      其中,请求参数的泛型,为了使用的方便,我们定义其extends any[],必须是一个数组,使用...args的形式传入到requestinstance中执行。

      service的类型需要与request类型保持一致, options的类型按需要实现的功能参数添加,于是,我们得到了如下一个useRequest

      // /src/hooks/useRequest/types.ts
      export type Service<T, P extends any[]> = (...args: P) => RequestResponse<T>;
      // 可按对应的配置项需求扩展
      export interface Options<T, P extnds any> {
        // 是否手动发起请求
        manual?: boolean;
        // 当 manual 为false时,自动执行的默认参数
        defaultParams?: P;
        // 依赖项更新
        refreshDeps?: WatchSource<any>[];
        refreshDepsParams?: ComputedRef<P>;
        // 是否关闭重复请求,当queryKey存在时,该字段无效
        repeatCancel?: boolean;
        // 并发请求
        queryKey?: (...args: P) => string;
        // 成功回调
        onSuccess?: (response: AxiosResponse<Response<T>>, params: P) => void;
        // 失败回调
        onError?: (err: ErrorData, params: P) => void;
      }
      // /src/hooks/useRequest/index.ts
      export function useRequest<T, P extends any[]>(
          service: Service<T, P>,
          options: Options<T, P> = {}
      ){
          return {
              data, // data 类型为T
              run,
              loading,
              cancel,
              err,
              querise
          }
      }

      queryKey的问题

      上面我们提到了,queryKey请求普通请求如果单独维护,不仅割裂,而且代码还很混乱,那有没有什么办法来解决这个问题呢,用js的思想来看这个问题,假设我现在有一个对象querise,我需要将不同请求参数的请求相关数据维护到querise中,比如run(1),那么querise应该为

      const querise = {
        1: {
            data: null,
            loading: false
            ...
        }
      }

      这是在queryKey的情况下,那没有queryKey呢?很简单,维护到default对象呗,即

      const querise = {
        default: {
            data: null,
            loading: false
            ...
        }
      }

      为了确保默认key值的唯一性,我们引入Symbol,即

      const defaultQuerise = Symbol('default');
      const querise = {
        [defaultQuerise]: {
            data: null,
            loading: false
            ...
        }
      }

      因为我们会使用reactive包裹querise,所以想要满足非queryKey请求时,使用默认导出的data loading err等数据,只需要

      return {
          run,
          querise,
          ...toRefs(querise[defaulrQuerise])
      }

      好了,需要讨论的问题完了,我们来写代码

      完整代码

      // /src/hooks/useRequest/types.ts
      import { Canceler, AxiosResponse } from 'axios';
      import { ComputedRef, WatchSource, Ref } from 'vue';
      export interface Response<T> {
        code: number;
        data: T;
        msg: string;
      }
      export type AppAxiosResponse<T = any> = AxiosResponse<Response<T>>;
      export interface RequestResponse<T>{
        instance: Promise<AppAxiosResponse<T>>;
        cancel: Ref<Canceler | undefined>
      }
      export type Service<T, P extends any[]> = (...args: P) => RequestResponse<T>;
      export interface Options<T, P extends any[]> {
        // 是否手动发起请求
        manual?: boolean;
        // 当 manual 为false时,自动执行的默认参数
        defaultParams?: P;
        // 依赖项更新
        refreshDeps?: WatchSource<any>[];
        refreshDepsParams?: ComputedRef<P>;
        // 是否关闭重复请求,当queryKey存在时,该字段无效
        repeatCancel?: boolean;
        // 重试次数
        retryCount?: number;
        // 重试间隔时间
        retryInterval?: number;
        // 并发请求
        queryKey?: (...args: P) => string;
        // 成功回调
        onSuccess?: (response: AxiosResponse<Response<T>>, params: P) => void;
      
        // 失败回调
        onError?: (err: ErrorData, params: P) => void;
      }
      export interface IRequestResult<T> {
        data: T | null;
        loading: boolean;
        cancel: Canceler;
        err?: ErrorData;
      }
      export interface ErrorData<T = any> {
        code: number | string;
        data: T;
        msg: string;
      }
      // /src/hooks/useRequest/axios.ts
      import { ref } from 'vue';
      import { AppAxiosResponse, RequestResponse } from './types';
      import axios, { AxiosRequestConfig, Canceler } from 'axios';
      const instance = axios.create({
        timeout: 30 * 1000,
        baseURL: '/api'
      });
      
      instance.interceptors.request.use(undefined, (err) => {
        console.log('request-error', err);
      });
      
      instance.interceptors.response.use((res: AppAxiosResponse) => {
        if(res.data.code !== 200) {
          return Promise.reject(res.data);
        }
        return res;
      }, (err) => {
        if(axios.isCancel(err)) {
          return Promise.reject({
            code: 10000,
            msg: 'Cancel',
            data: null
          });
        }
        if(err.code === 'ECONNABORTED') {
          return Promise.reject({
            code: 10001,
            msg: '超时',
            data: null
          });
        }
        console.log('response-error', err.toJSON());
        return Promise.reject(err);
      });
      export function request<T>(config: AxiosRequestConfig): RequestResponse<T> {
        const cancel = ref<Canceler>();
        return {
          instance: instance({
            ...config,
            cancelToken: new axios.CancelToken((c) => {
              cancel.value = c;
            })
          }),
          cancel
        };
      }
      import { isFunction } from 'lodash';
      import { reactive, toRefs, watch } from 'vue';
      import { IRequestResult, Options, Service, ErrorData } from './types';
      const defaultQuerise = Symbol('default');
      export function useRequest<T, P extends any[]>(
        service: Service<T, P>,
        options: Options<T, P> = {}
      ) {
        const {
          manual = false,
          defaultParams = [] as unknown as P,
          repeatCancel = false,
          refreshDeps = null,
          refreshDepsParams = null,
          queryKey = null
        } = options;
      
        const querise = reactive<Record<string | symbol, IRequestResult<T>>>({
          [defaultQuerise]: {
            data: null,
            loading: false,
            cancel: () => null,
            err: undefined
          }
        });
        const serviceFn = async (...args: P) => {
          const key = queryKey ? queryKey(...args) : defaultQuerise;
          if (!querise[key]) {
            querise[key] = {} as any;
          }
          if (!queryKey && repeatCancel) {
            querise[key].cancel();
          }
          querise[key].loading = true;
          const { instance, cancel } = service(...args);
          querise[key].cancel = cancel as any;
          instance
            .then((res) => {
              querise[key].data = res.data.data;
              querise[key].err = undefined;
              if (isFunction(options.onSuccess)) {
                options.onSuccess(res, args);
              }
            })
            .catch((err: ErrorData) => {
              querise[key].err = err;
              if (isFunction(options.onError)) {
                options.onError(err, args);
              }
            })
            .finally(() => {
              querise[key].loading = false;
            });
        };
      
        const run = serviceFn;
        // 依赖更新
        if (refreshDeps) {
          watch(
            refreshDeps,
            () => {
              run(...(refreshDepsParams?.value || ([] as unknown as P)));
            },
            { deep: true }
          );
        }
      
        if (!manual) {
          run(...defaultParams);
        }
      
        return {
          run,
          querise,
          ...toRefs(querise[defaultQuerise])
        };
      }

      需要防抖 节流 错误重试等功能,仅需要扩展Options类型,在useRequest中添加对应的逻辑即可,比如使用lodash包裹run函数,这里只是将最基本的功能实现搞定了,一部分小问题以及扩展性的东西没有过分纠结。

      结语

      到此这篇关于Vue3 TypeScript 实现useRequest详情的文章就介绍到这了,更多相关TypeScript实现 useRequest内容请搜索北冥有鱼以前的文章或继续浏览下面的相关文章希望大家以后多多支持北冥有鱼!

      您可能感兴趣的文章:

      • 关于Vue3&TypeScript的踩坑汇总
      • Vue3+TypeScript+Vite使用require动态引入图片等静态资源
      • vue3+Pinia+TypeScript 实现封装轮播图组件
      • TypeScript Pinia实战分享(Vuex和Pinia对比梳理总结)
      • TypeScript在Vuex4中使用TS实战分享
      • 关于Vue新搭档TypeScript快速入门实践

      《Vue3 TypeScript 实现useRequest详情.doc》

      下载本文的Word格式文档,以方便收藏与打印。