web 前端培训 Vue3 TypeScript 如何实现 useRequest
起因
自从 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 函数,这里只是将最基本的功能实现搞定了,一部分小问题以及扩展性的东西没有过分纠结。
文章来源于程序员成长指北
评论