写点什么

web 前端培训 Vue3 TypeScript 如何实现 useRequest

作者:@零度
  • 2022 年 5 月 31 日
  • 本文字数:4711 字

    阅读完需:约 15 分钟

​起因


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


效果展示


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


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


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


Axios


既然咱们使用 TypeScript 和 axios,为了使 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 的用法,我是看了他们的用法来构思我的设计的_web前端培训


比如一个普通的请求,我希望简单的使用 data、loading、err 等来接受数据,比如


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 的形式传入到 request 的 instance 中执行。


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 = {


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>>>({


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(querisedefaultQuerise)


};


}


复制代码


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


文章来源于程序员成长指北


用户头像

@零度

关注

关注尚硅谷,轻松学IT 2021.11.23 加入

IT培训 www.atguigu.com

评论

发布
暂无评论
web前端培训Vue3 TypeScript 如何实现useRequest_Vue_@零度_InfoQ写作社区