Vue3 TypeScript 实现一个 useRequest
大厂技术 高级前端 Node进阶
点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
起因
自从 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 {
code: number;
data: T;
msg: string;
}
// 为了使用方便,对 AxiosResponse 默认添加我们公用的 Response 类型
export type AppAxiosResponseany> = AxiosResponse>;
// 为了 useRequest 使用封装的类型
export interface RequestResponse {
instance: Promise>;
cancel: Refundefined>;
}
复制代码
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();
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({
url: '/getUserInfo',
method: 'get',
params: {
id
}
});
}
复制代码
需要注意的是,示例中的错误信息经过了统一性的封装,如果希望错误有一致性的表现,可以封装一个类型接收错误,建议与后台返回的数据结构一致。
现在,咱们使用这个request
函数,传入对应的泛型,就可以享受到对应的类型提示
。
useRequest
如何使用
想要设计useRequest
,那现在思考一下,什么样的useRequest
使用起来,能让我们感到快乐
,拿上面的基础示例
和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({
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 Serviceextends any[]> = (...args: P) => RequestResponse;
// 可按对应的配置项需求扩展
export interface Options {
// 是否手动发起请求
manual?: boolean;
// 当 manual 为false时,自动执行的默认参数
defaultParams?: P;
// 依赖项更新
refreshDeps?: WatchSource<any>[];
refreshDepsParams?: ComputedRef;
// 是否关闭重复请求,当queryKey存在时,该字段无效
repeatCancel?: boolean;
// 并发请求
queryKey?: (...args: P) => string;
// 成功回调
onSuccess?: (response: AxiosResponse>, params: P ) => void;
// 失败回调
onError?: (err: ErrorData, params: P) => void;
}
复制代码
// /src/hooks/useRequest/index.ts
export function useRequest<T, P extends any[]>(
service: Service,
options: Options = {}
){
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 {
code: number;
data: T;
msg: string;
}
export type AppAxiosResponseany> = AxiosResponse>;
export interface RequestResponse{
instance: Promise>;
cancel: Refundefined>
}
export type Serviceextends any[]> = (...args: P) => RequestResponse;
export interface Optionsextends any[]> {
// 是否手动发起请求
manual?: boolean;
// 当 manual 为false时,自动执行的默认参数
defaultParams?: P;
// 依赖项更新
refreshDeps?: WatchSource<any>[];
refreshDepsParams?: ComputedRef;
// 是否关闭重复请求,当queryKey存在时,该字段无效
repeatCancel?: boolean;
// 重试次数
retryCount?: number;
// 重试间隔时间
retryInterval?: number;
// 并发请求
queryKey?: (...args: P) => string;
// 成功回调
onSuccess?: (response: AxiosResponse>, params: P ) => void;
// 失败回调
onError?: (err: ErrorData, params: P) => void;
}
export interface IRequestResult {
data: T | null;
loading: boolean;
cancel: Canceler;
err?: ErrorData;
}
export interface ErrorData {
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();
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,
options: Options = {}
) {
const {
manual = false,
defaultParams = [] as unknown as P,
repeatCancel = false,
refreshDeps = null,
refreshDepsParams = null,
queryKey = null
} = options;
const querise = reactivestring | symbol, IRequestResult>>({
[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
函数,这里只是将最基本的功能实现搞定了,一部分小问题以及扩展性的东西没有过分纠结。
结语
当前的 useRequest
还是比较简陋,希望有想法或者建议的朋友可以一起讨论,有什么问题也可以问我,谢谢。
感谢
本次分享到这里就结束了,感谢您的阅读,如果本文对您有什么帮助,别忘了动动手指点个赞 ❤️ 和关注。
Node 社群
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:
1. 点个「在看」,让更多人也能看到这篇文章 2. 订阅官方博客 www.inode.club 让我们一起成长
点赞和在看就是最大的支持
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:
点赞和在看就是最大的支持