Vue3 TypeScript 实现useRequest详情

目录
  • 前言:
  • 效果展示
  • 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内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • ahooks useRequest源码精读解析

    目录 前言 架构图 源码解析 Fetch onBefore onRequest onSuccess onFinally onError 其它 API 小结 plugins usePollingPlugin useRetryPlugin 小结 useRequest 对自定义 hook 的思考 总结 前言 自从 React v16.8 推出了 Hooks API,前端框架圈并开启了新的逻辑复用的时代,不再需要在意 HOC 的无限套娃导致性能差的问题,也解决了 mixin 的可阅读性差的问题.当然对于

  • ahooks正式发布React Hooks工具库

    目录 起因 解法 共建 项目目标 品牌升级 社区开源 API 规范 示例演示 开发迭代 下一步 起因 从 React Hooks 正式发布到现在,越来越多的项目正在使用 Function Component 替代 Class Component,Hooks 这一新特性也逐渐被广泛的使用. 然而在实践的过程中,我们发现在很多常见的场景下,大部分逻辑是重复且可被复用的,如对数据请求的逻辑处理,对防抖节流的逻辑处理等,同样的代码经常会在同一个或不同的项目中被重复的编写 . 另一方面,由于 Hooks

  • JavaScript中ahooks 处理 DOM 的方法

    目录 前言 DOM 类 Hooks 使用规范 getTargetElement useEffectWithTarget 思考与总结 前言 目标主要有以下几点: 加深对 React hooks 的理解. 学习如何抽象自定义 hooks.构建属于自己的 React hooks 工具库. 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择. 本篇文章探讨一下 ahooks 对 DOM 类 Hooks 使用规范,以及源码中是如何去做处理的. DOM 类 Hooks 使用规范 这一章节,大部分参考官

  • ahooks整体架构及React工具库源码解读

    目录 引言 React hooks utils 库 ahooks 简介 特点 hooks 种类 ahooks 整体架构 项目启动 整体结构 hooks 总结 引言 本文是深入浅出 ahooks 源码系列文章的第一篇,这个系列的目标主要有以下几点: 加深对 React hooks 的理解. 学习如何抽象自定义 hooks.构建属于自己的 React hooks 工具库. 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择. 注:本系列对 ahooks 的源码解析是基于 v3.3.13.自己

  • Vue3 TypeScript 实现useRequest详情

    目录 前言: 效果展示 Axios interface axios的简单封装 useRequest 如何使用 添加泛型支持 queryKey的问题 完整代码 结语 前言: 自从 Vue3 更新之后,算是投入了比较大的精力写了一个较为完善的Vue3.2 + Vite2 + Pinia + Naive UI的B端模版,在做到网络请求这一块的时候,最初使用的是VueRequest的useRequest,但是因为VueRequest的useRequest的cancel关闭请求并不是真正的关闭,对我个人来

  • vue3+typescript实现图片懒加载插件

    github项目地址: github.com/murongg/vue- 求star 与 issues 我文采不好,可能写的文章不咋样,有什么问题可以在留言区评论,我会尽力解答 本项目已经发布到npm 安装: $ npm i vue3-lazyload # or $ yarn add vue3-lazyload 需求分析 支持自定义 loading 图片,图片加载状态时使用此图片 支持自定义 error 图片,图片加载失败后使用此图片 支持 lifecycle hooks,类似于 vue 的生命周

  • vue3+typeScript穿梭框的实现示例

    前言 实现功能:模仿element穿梭框的简单功能 每周分享一个vue3+typeScript的小组件,我只想分享下自己的实现思路,楼主是个菜鸡前端,记录下实现过程,说不定对你有帮助. 效果展示 预览地址 github地址 开发过程 思路:用两个数组分别记录左右框框里面的值,根据复选框选中状态来实现删除增加即可 html部分 <div class="shuttle"> <!-- 左边列表 --> <div class="shuttle-box&q

  • Vue3+TypeScript封装axios并进行请求调用的实现

    不是吧,不是吧,原来真的有人都2021年了,连TypeScript都没听说过吧?在项目中使用TypeScript虽然短期内会增加一些开发成本,但是对于其需要长期维护的项目,TypeScript能够减少其维护成本,使用TypeScript增加了代码的可读性和可维护性,且拥有较为活跃的社区,当居为大前端的趋势所在,那就开始淦起来吧~ 使用TypeScript封装基础axios库 代码如下: // http.ts import axios, { AxiosRequestConfig, AxiosRes

  • Vue3+TypeScript实现递归菜单组件的完整实例

    目录 前言 需求 实现 首次渲染 点击菜单项 样式区分 默认高亮 数据源变动引发的 bug 完整代码 App.vue 总结 前言 小伙伴们好久不见,最近刚入职新公司,需求排的很满,平常是实在没时间写文章了,更新频率会变得比较慢. 周末在家闲着无聊,突然小弟过来紧急求助,说是面试腾讯的时候,对方给了个 Vue 的递归菜单要求实现,回来找我复盘. 正好这周是小周,没想着出去玩,就在家写写代码吧,我看了一下需求,确实是比较复杂,需要利用好递归组件,正好趁着这 个机会总结一篇 Vue3 + TS 实现递

  • Vue3 + TypeScript 开发总结

    目录 Vue3 + TypeScript 学习 一, 环境配置 1.1 安装最新 Vue 脚手架 1.2 创建Vue3 项目 二, 进击Vue3 2. 1 Vue 2 局限性 2.2 Vue 3 如何解决Vue 2 局限 三,Vue3 Composition Ap i 3.1 关于 Composition Api 3.2 什么时候使用Composition Api 四,Composition Api 必备基础 4.2 ref 创建响应式变量 4.3 生命周期 4.4 watch 4.5 comp

  • Vue3+TypeScript+Vite使用require动态引入图片等静态资源

    问题:Vue3+TypeScript+Vite的项目中如何使用require动态引入类似于图片等静态资源! 描述:今天在开发项目时(项目框架为Vue3+TypeScript+Vite)需要 动态引入静态资源,也就是img标签的src属性值为动态获取,按照以往的做法直接是require引入即可,如下代码: <img class="demo" :src="require(`../../../assets/image/${item.img}`)" /> 写上后

  • 基于Vue3+TypeScript的全局对象的注入和使用详解

    目录 1.Vue2的全局挂载 2.Vue3+TypeScript的全局挂载 3.Vue3+TypeScript的全局挂载的对象接口定义 刚完成一些前端项目的开发,腾出精力来总结一些前端开发的技术点,以及继续完善基于SqlSugar的开发框架循序渐进介绍的系列文章,本篇随笔主要介绍一下基于Vue3+TypeScript的全局对象的注入和使用.我们知道在Vue2中全局注入一个全局变量使用protoType的方式,很方便的就注入了,而Vue3则不能通过这种方式直接使用,而是显得复杂一些,不过全局变量的

  • 使用Vite+Vue3+TypeScript 搭建开发脚手架的详细过程

    目录 Vite前端开发与构建工具 Vue3 与 Vue2区别 TypeScript 使用Vite创建脚手架 1.创建项目文件夹 2.选择Vue 3.选择TypeScript 4.完成后可以看到项目文件夹(my-vue-app) 配置文件引用别名 alias 修改 vite.config.ts 文件配置(此时:会报错 path 未定义,接下来定义path) 定义path,修改tsconfig.json 安装css处理器插件scss 配置全局scss样式(在src/assets 下创建style 文

  • vue3封装京东商品详情页放大镜效果组件

    本文实例为大家分享了vue3封装类似京东商品详情页放大镜效果组件的具体代码,供大家参考,具体内容如下 首先先完成基本布局 完成图片的切换效果,通过 mouseenter 事件切换图片 落地代码 <template> <div class="goods-image"> <!-- 预览大图 --> <div class="large" v-show="show" :style="[{ backgro

随机推荐