TypeScript实战之用TS封装Axios

Axios几个常用类型

在使用TypeScript封装Axios之前我们先来看看Axios几个重要的类型。

AxiosRequestConfig

AxiosRequestConfig是我们使用axios发送请求传递参数的类型。当然它也是我们请求拦截器里面的参数类型。

1
axios(config: AxiosRequestConfig)

可以看到,这个config里面的参数还是挺多的。我们常用的有url、method、params、data、headers、baseURL、timeout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
export interface AxiosRequestConfig {
url?: string;
method?: Method;
baseURL?: string;
transformRequest?: AxiosTransformer | AxiosTransformer[];
transformResponse?: AxiosTransformer | AxiosTransformer[];
headers?: any;
params?: any;
paramsSerializer?: (params: any) => string;
data?: any;
timeout?: number;
timeoutErrorMessage?: string;
withCredentials?: boolean;
adapter?: AxiosAdapter;
auth?: AxiosBasicCredentials;
responseType?: ResponseType;
xsrfCookieName?: string;
xsrfHeaderName?: string;
onUploadProgress?: (progressEvent: any) => void;
onDownloadProgress?: (progressEvent: any) => void;
maxContentLength?: number;
validateStatus?: ((status: number) => boolean) | null;
maxBodyLength?: number;
maxRedirects?: number;
socketPath?: string | null;
httpAgent?: any;
httpsAgent?: any;
proxy?: AxiosProxyConfig | false;
cancelToken?: CancelToken;
decompress?: boolean;
transitional?: TransitionalOptions
}

AxiosInstance

AxiosInstance是我们使用axios实例对象类型。

我们使用axios.create(config?: AxiosRequestConfig)创建出来的对象都是AxiosInstance类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export interface AxiosInstance {
(config: AxiosRequestConfig): AxiosPromise;
(url: string, config?: AxiosRequestConfig): AxiosPromise;
defaults: AxiosRequestConfig;
interceptors: {
request: AxiosInterceptorManager<AxiosRequestConfig>;
response: AxiosInterceptorManager<AxiosResponse>;
};
getUri(config?: AxiosRequestConfig): string;
request<T = any, R = AxiosResponse<T>> (config: AxiosRequestConfig): Promise<R>;
get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
delete<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
head<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
options<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
post<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
put<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
patch<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
}

可以发现,我们可以使用axios.create、axios.all、axios.spread方法,但是AxiosInstance 上并没有create、all、spread等方法,那我们的axios到底是什么类型呢?

AxiosStatic

1
2
3
4
5
6
7
8
9
10
11
export interface AxiosStatic extends AxiosInstance {
create(config?: AxiosRequestConfig): AxiosInstance;
Cancel: CancelStatic;
CancelToken: CancelTokenStatic;
isCancel(value: any): boolean;
all<T>(values: (T | Promise<T>)[]): Promise<T[]>;
spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R;
isAxiosError(payload: any): payload is AxiosError;
}

declare const axios: AxiosStatic;

可以发现,axios其实是AxiosStatic类型,并且继承了AxiosInstance类型。所以是两者的结合。

相较axios.create(config?: AxiosRequestConfig)创建出来的实例对象,axios功能是更强大的。

AxiosResponse

AxiosResponse是非常重要的,我们的axios请求返回值类型都是AxiosResponse类型。

并且我们可以发现AxiosResponse是一个接口泛型,这个泛型会应用到后端返回的data上。所以这块我们可以根据后端接口返回定义不同的类型传递进去。

后面笔者在封装常用方法的时候会细说。

1
2
3
4
5
6
7
8
export interface AxiosResponse<T = any>  {
data: T;
status: number;
statusText: string;
headers: any;
config: AxiosRequestConfig;
request?: any;
}

AxiosError

AxiosError这个类型也是我们必须要知道的。在我们响应拦截器里面的错误就是AxiosError类型。

1
2
3
4
5
6
7
8
export interface AxiosError<T = any> extends Error {
config: AxiosRequestConfig;
code?: string;
request?: any;
response?: AxiosResponse<T>;
isAxiosError: boolean;
toJSON: () => object;
}

说完了Axios的几个常用类型,接下来我们正式开始使用TS来封装我们的Axios

基础封装

首先我们实现一个最基本的版本,实例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// index.ts
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'

class Request {
// axios 实例
instance: AxiosInstance
// 基础配置,url和超时时间
baseConfig: AxiosRequestConfig = {baseURL: "/api", timeout: 60000}

constructor(config: AxiosRequestConfig) {
// 使用axios.create创建axios实例
this.instance = axios.create(Object.assign(this.baseConfig, config))
}

// 定义请求方法
public request(config: AxiosRequestConfig): Promise<AxiosResponse> {
return this.instance.request(config)
}
}

export default Request

在实际项目中有了基本的请求方法还是远远不够的,我们还需要封装拦截器和一些常用方法。

拦截器封装

拦截器封装只需要在类中对axios.create()创建的实例调用interceptors下的两个拦截器即可,实例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// index.ts
constructor(config: AxiosRequestConfig) {
this.instance = axios.create(Object.assign(this.baseConfig, config))

this.instance.interceptors.request.use(
(config: AxiosRequestConfig) => {
// 一般会请求拦截里面加token
const token = localStorage.getItem("token") as string
config.headers!.Authorization = token;

return config
},
(err: any) => {
return Promise.reject(err)
},
)

this.instance.interceptors.response.use(
(res: AxiosResponse) => {
// 直接返回res,当然你也可以只返回res.data
// 系统如果有自定义code也可以在这里处理
return res
},
(err: any) => {
// 这里用来处理http常见错误,进行全局提示
let message = "";
switch (err.response.status) {
case 400:
message = "请求错误(400)";
break;
case 401:
message = "未授权,请重新登录(401)";
// 这里可以做清空storage并跳转到登录页的操作
break;
case 403:
message = "拒绝访问(403)";
break;
case 404:
message = "请求出错(404)";
break;
case 408:
message = "请求超时(408)";
break;
case 500:
message = "服务器错误(500)";
break;
case 501:
message = "服务未实现(501)";
break;
case 502:
message = "网络错误(502)";
break;
case 503:
message = "服务不可用(503)";
break;
case 504:
message = "网络超时(504)";
break;
case 505:
message = "HTTP版本不受支持(505)";
break;
default:
message = `连接出错(${err.response.status})!`;
}
// 这里错误消息可以使用全局弹框展示出来
// 比如element plus 可以使用 ElMessage
// ElMessage({
// showClose: true,
// message: `${message},请检查网络或联系管理员!`,
// type: "error",
// });
// 这里是AxiosError类型,所以一般我们只reject我们需要的响应即可
return Promise.reject(err.response)
},
)
}

在这里我们分别对请求拦截器和响应拦截器做了处理。

在请求拦截器我们给请求头添加了token

在响应拦截器,我们返回了整个response对象,当然你也可以只返回后端返回的response.data,这里可以根据个人喜好来处理。其次对http错误进行了全局处理。

常用方法封装

在基础封装的时候我们封装了一个request通用方法,其实我们还可以更具体的封装get、post、put、delete方法,让我们使用更方便。

并且,我们前面分析到,AxiosResponse其实是一个泛型接口,他可以接受一个泛型并应用到我们的data上。所以我们可以在这里再定义一个后端通用返回的数据类型。

比如假设我们某个项目后端接口不管请求成功与失败,返回的结构永远是code、message、results的话我们可以定义一个这样的数据类型。

1
2
3
4
5
type Result<T> = {
code: number,
message: string,
result: T
}

然后传递个各个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public get<T = any>(
url: string,
config?: AxiosRequestConfig
): Promise<AxiosResponse<Result<T>>> {
return this.instance.get(url, config);
}

public post<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig
): Promise<AxiosResponse<Result<T>>> {
return this.instance.post(url, data, config);
}

public put<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig
): Promise<AxiosResponse<Result<T>>> {
return this.instance.put(url, data, config);
}

public delete<T = any>(
url: string,
config?: AxiosRequestConfig
): Promise<AxiosResponse<Result<T>>> {
return this.instance.delete(url, config);
}

这样当我们调用接口的时候就可以看到我们返回的data的类型啦。就是我们定义的Result类型。

image.png

所以我们可以直接得到自动提示

image.png

上面调用接口的时候并没有传递接口数据类型,所以我们的resultany类型,要想要每个接口都有类型提示,我们还需要给方法传递泛型。

我们再改进下,我们再定义一个login接口返回值类型loginType

1
2
3
type loginType = {
token: string;
};

然后再调用方法的地方传递进去,然后我们再看看返回值data的类型。

image.png

可以看到他是Result<loginType>类型,这个loginType就是result的类型。

所以我们的result还可以进一步的得到提示

image.png

当然每个接口都定义返回值类型固然好,但是会大大加大前端的工作量。我们在写请求方法的时候也可以不传递接口返回值类型,这样result的类型就是any。这个可以根据自身项目需求来选择使用。

看到这小伙伴们是不是都弄懂了呢?如还有疑问欢迎留言。

总结

说了这么多,有些小伙伴们可能有点晕了,下面笔者分享下整个axios的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// index.ts
import axios from "axios";
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";

type Result<T> = {
code: number;
message: string;
result: T;
};

class Request {
// axios 实例
instance: AxiosInstance;
// 基础配置,url和超时时间
baseConfig: AxiosRequestConfig = { baseURL: "/api", timeout: 60000 };

constructor(config: AxiosRequestConfig) {
// 使用axios.create创建axios实例
this.instance = axios.create(Object.assign(this.baseConfig, config));

this.instance.interceptors.request.use(
(config: AxiosRequestConfig) => {
// 一般会请求拦截里面加token
const token = localStorage.getItem("token") as string;
config.headers!.Authorization = token;

return config;
},
(err: any) => {
return Promise.reject(err);
}
);

this.instance.interceptors.response.use(
(res: AxiosResponse) => {
// 直接返回res,当然你也可以只返回res.data
return res;
},
(err: any) => {
// 这里用来处理http常见错误,进行全局提示
let message = "";
switch (err.response.status) {
case 400:
message = "请求错误(400)";
break;
case 401:
message = "未授权,请重新登录(401)";
// 这里可以做清空storage并跳转到登录页的操作
break;
case 403:
message = "拒绝访问(403)";
break;
case 404:
message = "请求出错(404)";
break;
case 408:
message = "请求超时(408)";
break;
case 500:
message = "服务器错误(500)";
break;
case 501:
message = "服务未实现(501)";
break;
case 502:
message = "网络错误(502)";
break;
case 503:
message = "服务不可用(503)";
break;
case 504:
message = "网络超时(504)";
break;
case 505:
message = "HTTP版本不受支持(505)";
break;
default:
message = `连接出错(${err.response.status})!`;
}
// 这里错误消息可以使用全局弹框展示出来
// 比如element plus 可以使用 ElMessage
// ElMessage({
// showClose: true,
// message: `${message},请检查网络或联系管理员!`,
// type: "error",
// });
// 这里是AxiosError类型,所以一般我们只reject我们需要的响应即可
return Promise.reject(err.response);
}
);
}

// 定义请求方法
public request(config: AxiosRequestConfig): Promise<AxiosResponse> {
return this.instance.request(config);
}

public get<T = any>(
url: string,
config?: AxiosRequestConfig
): Promise<AxiosResponse<Result<T>>> {
return this.instance.get(url, config);
}

public post<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig
): Promise<AxiosResponse<Result<T>>> {
return this.instance.post(url, data, config);
}

public put<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig
): Promise<AxiosResponse<Result<T>>> {
return this.instance.put(url, data, config);
}

public delete<T = any>(
url: string,
config?: AxiosRequestConfig
): Promise<AxiosResponse<Result<T>>> {
return this.instance.delete(url, config);
}
}

export default Request;

十分钟封装一个好用的axios

通用能力

列一下我想要这个通用请求能达到什么样的效果

  • 正常请求该有的(跨域携带cookie,token,超时设置

  • 请求响应拦截器

    • 请求成功,业务状态码200,解析result给我,我不想一层一层的去判断拿数据

    • http请求200, 业务状态码非200,说明逻辑判断这是不成功的,那就全局message提示服务端的报错

    • http请求非200, 说明http请求都有问题,也全局message提示报错

    • http请求或者业务状态码401都做注销操作

  • 全局的loading配置, 默认开启,可配置关闭(由于后端的问题,经常会让前端加防抖节流或者loading不让用户在界面上疯狂乱点,行吧行吧,你们的问题前端帮你们解决,你的规矩就是规矩是吧)

  • 统一文件下载处理 (不要再去各写各的下载了,你写一个,他写一个,一个项目就是这样整的跟屎一样)

一步一步添加功能实现

正常请求该有的

1
2
3
4
5
6
7
8
9
10
11
12
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";

export const createAxiosByinterceptors = (
config?: AxiosRequestConfig
): AxiosInstance => {
const instance = axios.create({
timeout: 1000, //超时配置
withCredentials: true, //跨域携带cookie
...config, // 自定义配置覆盖基本配置
});
return instance;
};

请求响应拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import { Message } from "element-ui";
import { jumpLogin } from "@/utils";

export const createAxiosByinterceptors = (
config?: AxiosRequestConfig
): AxiosInstance => {
const instance = axios.create({
timeout: 1000, //超时配置
withCredentials: true, //跨域携带cookie
...config, // 自定义配置覆盖基本配置
});

// 添加请求拦截器
instance.interceptors.request.use(
function (config: any) {
// 在发送请求之前做些什么
console.log("config:", config);
// config.headers.Authorization = vm.$Cookies.get("vue_admin_token");
return config;
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);

// 添加响应拦截器
instance.interceptors.response.use(
function (response) {
// 对响应数据做点什么
console.log("response:", response);
const { code, data, message } = response.data;
if (code === 200) return data;
else if (code === 401) {
jumpLogin();
} else {
Message.error(message);
return Promise.reject(response.data);
}
},
function (error) {
// 对响应错误做点什么
console.log("error-response:", error.response);
console.log("error-config:", error.config);
console.log("error-request:", error.request);
if (error.response) {
if (error.response.status === 401) {
jumpLogin();
}
}
Message.error(error?.response?.data?.message || "服务端异常");
return Promise.reject(error);
}
);
return instance;
};

全局的loading配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import { Message } from "element-ui";
import { jumpLogin } from "@/utils";
import { Loading } from "element-ui";
import { ElLoadingComponent } from "element-ui/types/loading";
// import vm from "@/main";

let loadingInstance: ElLoadingComponent | null = null;
let requestNum = 0;

const addLoading = () => {
// 增加loading 如果pending请求数量等于1,弹出loading, 防止重复弹出
requestNum++;
if (requestNum == 1) {
loadingInstance = Loading.service({
text: "正在努力加载中....",
background: "rgba(0, 0, 0, 0)",
});
}
};

const cancelLoading = () => {
// 取消loading 如果pending请求数量等于0,关闭loading
requestNum--;
if (requestNum === 0) loadingInstance?.close();
};

export const createAxiosByinterceptors = (
config?: AxiosRequestConfig
): AxiosInstance => {
const instance = axios.create({
timeout: 1000, //超时配置
withCredentials: true, //跨域携带cookie
...config, // 自定义配置覆盖基本配置
});

// 添加请求拦截器
instance.interceptors.request.use(
function (config: any) {
// 在发送请求之前做些什么
const { loading = true } = config;
console.log("config:", config);
// config.headers.Authorization = vm.$Cookies.get("vue_admin_token");
if (loading) addLoading();
return config;
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);

// 添加响应拦截器
instance.interceptors.response.use(
function (response) {
// 对响应数据做点什么
console.log("response:", response);
const { loading = true } = response.config;
if (loading) cancelLoading();
const { code, data, message } = response.data;
if (code === 200) return data;
else if (code === 401) {
jumpLogin();
} else {
Message.error(message);
return Promise.reject(response.data);
}
},
function (error) {
// 对响应错误做点什么
console.log("error-response:", error.response);
console.log("error-config:", error.config);
console.log("error-request:", error.request);
const { loading = true } = error.config;
if (loading) cancelLoading();
if (error.response) {
if (error.response.status === 401) {
jumpLogin();
}
}
Message.error(error?.response?.data?.message || "服务端异常");
return Promise.reject(error);
}
);
return instance;
};

统一文件下载处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import { Message } from "element-ui";
import { jumpLogin, downloadFile } from "@/utils";
import { Loading } from "element-ui";
import { ElLoadingComponent } from "element-ui/types/loading";
// import vm from "@/main";

let loadingInstance: ElLoadingComponent | null = null;
let requestNum = 0;

const addLoading = () => {
// 增加loading 如果pending请求数量等于1,弹出loading, 防止重复弹出
requestNum++;
if (requestNum == 1) {
loadingInstance = Loading.service({
text: "正在努力加载中....",
background: "rgba(0, 0, 0, 0)",
});
}
};

const cancelLoading = () => {
// 取消loading 如果pending请求数量等于0,关闭loading
requestNum--;
if (requestNum === 0) loadingInstance?.close();
};

export const createAxiosByinterceptors = (
config?: AxiosRequestConfig
): AxiosInstance => {
const instance = axios.create({
timeout: 1000, //超时配置
withCredentials: true, //跨域携带cookie
...config, // 自定义配置覆盖基本配置
});

// 添加请求拦截器
instance.interceptors.request.use(
function (config: any) {
// 在发送请求之前做些什么
const { loading = true } = config;
console.log("config:", config);
// config.headers.Authorization = vm.$Cookies.get("vue_admin_token");
if (loading) addLoading();
return config;
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);

// 添加响应拦截器
instance.interceptors.response.use(
function (response) {
// 对响应数据做点什么
console.log("response:", response);
const { loading = true } = response.config;
if (loading) cancelLoading();
const { code, data, message } = response.data;
// config设置responseType为blob 处理文件下载
if (response.data instanceof Blob) {
return downloadFile(response);
} else {
if (code === 200) return data;
else if (code === 401) {
jumpLogin();
} else {
Message.error(message);
return Promise.reject(response.data);
}
}
},
function (error) {
// 对响应错误做点什么
console.log("error-response:", error.response);
console.log("error-config:", error.config);
console.log("error-request:", error.request);
const { loading = true } = error.config;
if (loading) cancelLoading();
if (error.response) {
if (error.response.status === 401) {
jumpLogin();
}
}
Message.error(error?.response?.data?.message || "服务端异常");
return Promise.reject(error);
}
);
return instance;
};


src/utils/index.ts

import { Message } from "element-ui";
import { AxiosResponse } from "axios";
import vm from "@/main";

/**
* 跳转登录
*/
export const jumpLogin = () => {
vm.$Cookies.remove("vue_admin_token");
vm.$router.push(`/login?redirect=${encodeURIComponent(vm.$route.fullPath)}`);
};

/**
* 下载文件
* @param response
* @returns
*/
export const downloadFile = (response: AxiosResponse) => {
console.log("response.data.type:", response.data.type);
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = function () {
try {
console.log("result:", this.result);
const jsonData = JSON.parse((this as any).result); // 成功 说明是普通对象数据
if (jsonData?.code !== 200) {
Message.error(jsonData?.message ?? "请求失败");
reject(jsonData);
}
} catch (err) {
// 解析成对象失败,说明是正常的文件流
const blob = new Blob([response.data]);
// 本地保存文件
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
const filename = response?.headers?.["content-disposition"]
?.split("filename*=")?.[1]
?.substr(7);
link.setAttribute("download", decodeURI(filename));
document.body.appendChild(link);
link.click();
resolve(response.data);
}
};
fileReader.readAsText(response.data);
});
};

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createAxiosByinterceptors } from "@/api/request";

const request = createAxiosByinterceptors({
baseURL: localhost:7007,
});

//lodaing配置
export const appList = (params: any): Promise<any> => request.get("/app", { params, loading: true });
// 不需要默认的全局loading效果可配置loading为false关闭 loading默认为true

// 文件下载
export const exportGoods = (data: any) =>request.post("/export", data, {
responseType: "blob",
});