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

Mark wiens

发布时间:2022-05-31

5月21日消息,据外媒报道,苹果公司近日陆续发布了其多款平台操作系统的更新,分别将它们升级到iOS 13.5、iPadOS 13.5、twOS 13.4.5和iOS 12.4.7版,这次更新主要包括与新型冠状病毒“暴露9se9se,tunespotting,小鸭子图片,

web前端培训Vue3 TypeScript 如何实现useRequest9se9se,tunespotting,小鸭子图片,

起因

自从 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能满足【关注尚硅谷,轻松学IT】咱们的使用需求以及配合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使用起来,【关注尚硅谷,轻松学IT】能让我们感到快乐,拿上面的基础示例和queryKey示例来看,大家可以参考一下VueRequest或者aHooks的用法,我是看了他们的用法来构思我的设计的。

比如一个普通的请求,我希望简单的使用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;

// 失败回调

?: (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;

// 失败回调

?: (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.)) {

options.(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中添加对应www.atguigu.com的逻辑即可,比如使用lodash包裹run函数,这里只是将最基本的功能实现搞定了,一部分小问题以及扩展性的东西没有过分纠结。

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

推荐阅读:

web前端培训Vue3 setup() 启动函数的原理

web前端培训使用 Vue3来实现文章目录功能

web前端培训:Vue3 调度系统的深度剖析

web前端培训:vue3源码中细节知多少

喜羊羊图片,嵇康四弄,苦月亮, http://www.webgnss.com/fangchanzixun/loushikuaixun/42763.html

9se9se,tunespotting,小鸭子图片, 晚上十点多,妈妈对儿子说:“你爸爸喝酒去了,你到楼下接接他。”正玩游戏的儿子道:“他天天喝酒都不用接,干吗今天要去接。”妈妈:“你没看见今天楼下那棵树被园林局的人锯掉了吗?”哈哈! 周日

免责声明:本站所有信息均搜集自互联网,并不代表本站观点,本站不对其真实合法性负责。如有信息侵犯了您的权益,请告知,本站将立刻处理。联系QQ:1640731186