前言:

自从 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内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

Vue3 TypeScript 实现useRequest详情的更多相关文章

  1. vue3获取当前路由地址

    本文详细讲解了vue3获取当前路由地址的方法,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  2. 十分钟带你快速上手Vue3过渡动画

    在开发中我们想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验,下面这篇文章主要给大家介绍了关于如何快速上手Vue3过渡动画的相关资料,需要的朋友可以参考下

  3. 用vue3封装一个符合思维且简单实用的弹出层

    最近新项目中需要一个弹窗组件,所以我就做了一个,下面这篇文章主要给大家介绍了关于如何利用vue3封装一个符合思维且简单实用的弹出层,文中通过实例代码介绍的非常详细,需要的朋友可以参考下

  4. 使用Vite+Vue3+Vant全家桶快速构建项目步骤详解

    这篇文章主要为大家介绍了使用Vite+Vue3+Vant全家桶快速构建项目步骤详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  5. vue3中$attrs的变化与inheritAttrs的使用详解

    $attrs现在包括class和style属性。 也就是说在vue3中$listeners不存在了,vue2中$listeners是单独存在的,在vue3 $attrs包括class和style属性, vue2中 $attrs 不包含class和style属性,这篇文章主要介绍了vue3中$attrs的变化与inheritAttrs的使用 ,需要的朋友可以参考下

  6. vue3中proxy的基本用法说明

    这篇文章主要介绍了vue3中proxy的基本用法说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  7. 使用typescript类型实现ThreeSum

    这篇文章主要介绍了使用typescript类型实现ThreeSum,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以一下,希望对你学习又是帮助

  8.  typeScript入门基础介绍

    这篇文章主要介绍了 typeScript入门基础,TypeScript 是由微软开发的开源、跨平台的编程语言,是 javaScript 的超集,最终被编译为 javaScript代码。常常被简称为TS支持JS、ES语法,下文将继续其他基础介绍,需要的朋友可以参考一下

  9. typescript返回值类型和参数类型的具体使用

    本文主要介绍了typescript返回值类型和参数类型的具体使用文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  10. 如何利用vue3实现一个俄罗斯方块

    俄罗斯方块这个游戏相信大家都玩过,下面这篇文章主要给大家介绍了关于如何利用vue3实现一个俄罗斯方块的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部