前言
相信大家平时都在项目中遇到过axios的封装,尤其是有ts的时候,有关axios类型定义及其封装的,这一次我给大家讲解一下,在ts环境中,如果封装axios,本次封装将比较具体,请慢慢食用。
安装axios和typescript依赖
1 | npm i typescript axios -D |
根目录下新建tsconfig.json文件
1 | {} |
编译选项
你可以通过 compilerOptions 来定制你的编译选项:
1 | { |
指定文件
你也可以显式指定需要编译的文件:
1 | { |
你还可以使用 include 和 exclude 选项来指定需要包含的文件和排除的文件:
1 | { |
简单配置vite.config.ts
安装依赖
1 | npm i --save-dev @types/node |
tscofig.json
启用
1 | "moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6) |
vite.config.ts
1 | import { ConfigEnv, UserConfig } from 'vite'; |
测试ts是否正常编译
我适当修改了helloworld的代码
项目编译正常,就说明我们的ts已经成功导入了
配置axios
申明请求配置
- src/types/axios.d.ts
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
28import { AxiosRequestConfig } from 'axios';
// 请求参数
export interface RequestOptions {
apiUrl?: string;
isJoinPrefix?: boolean;
urlPrefix?: string;
joinParamsToUrl?: boolean;
formatDate?: boolean;
isTransformResponse?: boolean;
isReturnNativeResponse?: boolean;
ignoreRepeatRequest?: boolean;
joinTime?: boolean;
withToken?: boolean;
retry?: {
count: number;
delay: number;
};
}
// 返回结果
export interface Result<T = any> {
code: number;
data: T;
message?: string | null;
}
// 重复请求
export interface AxiosRequestConfigRetry extends AxiosRequestConfig {
retryCount?: number;
}
数据转化
- src/utils/request/AxiosTransform.ts
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
35import type { AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { AxiosError } from 'axios';
import type { RequestOptions, Result } from '@/types/axios';
// 创建Axios选项
export interface CreateAxiosOptions extends AxiosRequestConfig {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
authenticationScheme?: string;
// 数据处理
transform?: AxiosTransform;
// 请求选项
requestOptions?: RequestOptions;
}
// Axios 数据处理
export abstract class AxiosTransform {
// 请求前Hook
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
// 转换前Hook
transformRequestHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
// 请求失败处理
requestCatchHook?: (e: Error | AxiosError, options: RequestOptions) => Promise<any>;
// 请求前的拦截器
requestInterceptors?: (config: AxiosRequestConfig, options: CreateAxiosOptions) => InternalAxiosRequestConfig;
// 请求后的拦截器
responseInterceptors?: (res: AxiosResponse) => AxiosResponse;
// 请求前的拦截器错误处理
requestInterceptorsCatch?: (error: AxiosError) => void;
// 请求后的拦截器错误处理
responseInterceptorsCatch?: (error: AxiosError) => void;
}
请求取消
- src/utils/request/AxiosCancel.ts
安装依赖
安装loadash
1 | npm i lodash |
安装loadash的type
1 | npm i @types/lodash --save |
请求取消代码
1 | import type { AxiosRequestConfig, Canceler } from 'axios'; |
请求配置
全局申明
- src/types/global.d.ts
1
2
3
4
5
6
7
8// 通用声明
// Vue
declare module '*.vue' {
import { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
declare type Recordable<T = any> = Record<string, T>;
请求配置
- src/utils/request/utils.ts
比如我们请求需要追加一个时间戳等操作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
55import isString from 'lodash/isString';
import isObject from 'lodash/isObject';
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
export function joinTimestamp<T extends boolean>(join: boolean, restful: T): T extends true ? string : object;
export function joinTimestamp(join: boolean, restful = false): string | object {
if (!join) {
return restful ? '' : {};
}
const now = new Date().getTime();
if (restful) {
return `?_t=${now}`;
}
return { _t: now };
}
// 格式化提交参数时间
export function formatRequestDate(params: Recordable) {
if (Object.prototype.toString.call(params) !== '[object Object]') {
return;
}
for (const key in params) {
// eslint-disable-next-line no-underscore-dangle
if (params[key] && params[key]._isAMomentObject) {
params[key] = params[key].format(DATE_TIME_FORMAT);
}
if (isString(key)) {
const value = params[key];
if (value) {
try {
params[key] = isString(value) ? value.trim() : value;
} catch (error: any) {
throw new Error(error);
}
}
}
if (isObject(params[key])) {
formatRequestDate(params[key]);
}
}
}
// 将对象转为Url参数
export function setObjToUrlParams(baseUrl: string, obj: object): string {
let parameters = '';
for (const key in obj) {
parameters += `${key}=${encodeURIComponent(obj[key])}&`;
}
parameters = parameters.replace(/&$/, '');
return /\?$/.test(baseUrl) ? baseUrl + parameters : baseUrl.replace(/\/?$/, '?') + parameters;
}
处理请求内部详情
安装依赖
1 | npm i qs |
定义请求头
- src/utils/constants/index.ts
1
2
3
4
5
6// 通用请求头
export enum ContentTypeEnum {
Json = 'application/json;charset=UTF-8',
FormURLEncoded = 'application/x-www-form-urlencoded;charset=UTF-8',
FormData = 'multipart/form-data;charset=UTF-8',
}
处理请求详情
- src/utils/request/Axios.ts
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184import axios, { AxiosRequestConfig, InternalAxiosRequestConfig, AxiosInstance, AxiosResponse, AxiosError } from 'axios';
import { stringify } from 'qs';
import isFunction from 'lodash/isFunction';
import cloneDeep from 'lodash/cloneDeep';
import { CreateAxiosOptions } from './AxiosTransform';
import { AxiosCanceler } from './AxiosCancel';
import { AxiosRequestConfigRetry, RequestOptions, Result } from '@/types/axios';
import { ContentTypeEnum } from '@/constants';
// Axios模块
export class VAxios {
// axios句柄
private instance: AxiosInstance;
// axios选项
private readonly options: CreateAxiosOptions;
constructor(options: CreateAxiosOptions) {
this.options = options;
this.instance = axios.create(options);
this.setupInterceptors();
}
// 创建axios句柄
private createAxios(config: CreateAxiosOptions): void {
this.instance = axios.create(config);
}
// 获取数据处理
private getTransform() {
const { transform } = this.options;
return transform;
}
// 获取句柄
getAxios(): AxiosInstance {
return this.instance;
}
// 配置 axios
configAxios(config: CreateAxiosOptions) {
if (!this.instance) {
return;
}
this.createAxios(config);
}
// 设置通用头信息
setHeader(headers: Record<string, string>): void {
if (!this.instance) {
return;
}
Object.assign(this.instance.defaults.headers, headers);
}
// 设置拦截器
private setupInterceptors() {
const transform = this.getTransform();
if (!transform) {
return;
}
const { requestInterceptors, requestInterceptorsCatch, responseInterceptors, responseInterceptorsCatch } =
transform;
const axiosCanceler = new AxiosCanceler();
// 请求配置处理
this.instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
// @ts-ignore
const { ignoreRepeatRequest } = config.requestOptions;
const ignoreRepeat = ignoreRepeatRequest ?? this.options.requestOptions?.ignoreRepeatRequest;
if (!ignoreRepeat) axiosCanceler.addPending(config);
if (requestInterceptors && isFunction(requestInterceptors)) {
config = requestInterceptors(config, this.options);
}
return config;
}, undefined);
// 请求错误处理
if (requestInterceptorsCatch && isFunction(requestInterceptorsCatch)) {
this.instance.interceptors.request.use(undefined, requestInterceptorsCatch);
}
// 响应结果处理
this.instance.interceptors.response.use((res: AxiosResponse) => {
if (res) axiosCanceler.removePending(res.config);
if (responseInterceptors && isFunction(responseInterceptors)) {
res = responseInterceptors(res);
}
return res;
}, undefined);
// 响应错误处理
if (responseInterceptorsCatch && isFunction(responseInterceptorsCatch)) {
this.instance.interceptors.response.use(undefined, responseInterceptorsCatch);
}
}
// 支持Form Data
supportFormData(config: AxiosRequestConfig) {
const headers = config.headers || this.options.headers;
const contentType = headers?.['Content-Type'] || headers?.['content-type'];
if (
contentType !== ContentTypeEnum.FormURLEncoded ||
!Reflect.has(config, 'data') ||
config.method?.toUpperCase() === 'GET'
) {
return config;
}
return {
...config,
data: stringify(config.data, { arrayFormat: 'brackets' }),
};
}
get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'GET' }, options);
}
post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'POST' }, options);
}
put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'PUT' }, options);
}
delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'DELETE' }, options);
}
patch<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'PATCH' }, options);
}
// 请求
async request<T = any>(config: AxiosRequestConfigRetry, options?: RequestOptions): Promise<T> {
let conf: CreateAxiosOptions = cloneDeep(config);
const transform = this.getTransform();
const { requestOptions } = this.options;
const opt: RequestOptions = { ...requestOptions, ...options };
const { beforeRequestHook, requestCatchHook, transformRequestHook } = transform || {};
if (beforeRequestHook && isFunction(beforeRequestHook)) {
conf = beforeRequestHook(conf, opt);
}
conf.requestOptions = opt;
conf = this.supportFormData(conf);
return new Promise((resolve, reject) => {
this.instance
.request<any, AxiosResponse<Result>>(!config.retryCount ? conf : config)
.then((res: AxiosResponse<Result>) => {
if (transformRequestHook && isFunction(transformRequestHook)) {
try {
const ret = transformRequestHook(res, opt);
resolve(ret);
} catch (err) {
reject(err || new Error('请求错误!'));
}
return;
}
resolve(res as unknown as Promise<T>);
})
.catch((e: Error | AxiosError) => {
if (requestCatchHook && isFunction(requestCatchHook)) {
reject(requestCatchHook(e, opt));
return;
}
if (axios.isAxiosError(e)) {
// 在这里重写Axios的错误信息
}
reject(e);
});
});
}
}
在以上的基础之上,便可以将我们的请求需要的内容组装起来即可
请求整合
前缀,token名配置
- src/config/global.ts
1
2export const prefix = 'codesigner';
export const TOKEN_NAME = 'codesigner';
地址配置
-src/config/proxy.ts
1 | export default { |
整合代码
- src/utils/request/index.ts
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201// axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
import isString from 'lodash/isString';
import merge from 'lodash/merge';
import type { InternalAxiosRequestConfig } from 'axios';
import type { AxiosTransform, CreateAxiosOptions } from './AxiosTransform';
import { VAxios } from './Axios';
import proxy from '@/config/proxy';
import { joinTimestamp, formatRequestDate, setObjToUrlParams } from './utils';
import { TOKEN_NAME } from '@/config/global';
import { ContentTypeEnum } from '@/constants';
const env = import.meta.env.MODE || 'development';
// 如果是mock模式 或 没启用直连代理 就不配置host 会走本地Mock拦截 或 Vite 代理
const host = env === 'mock' || !proxy.isRequestProxy ? '' : proxy[env].host;
// 数据处理,方便区分多种处理方式
const transform: AxiosTransform = {
// 处理请求数据。如果数据不是预期格式,可直接抛出错误
transformRequestHook: (res, options) => {
const { isTransformResponse, isReturnNativeResponse } = options;
// 如果204无内容直接返回
const method = res.config.method?.toLowerCase();
if (res.status === 204 || method === 'put' || method === 'patch') {
return res;
}
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
if (isReturnNativeResponse) {
return res;
}
// 不进行任何处理,直接返回
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
if (!isTransformResponse) {
return res.data;
}
// 错误的时候返回
const { data } = res;
if (!data) {
throw new Error('请求接口错误');
}
// 这里 code为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
const { code, message } = data;
// 这里逻辑可以根据项目进行修改
const hasSuccess = data && code === 200;
if (hasSuccess) {
return data.data;
}
throw new Error(`请求接口错误, 错误码: ${code},错误提示:${message}`);
},
// 请求前处理配置
beforeRequestHook: (config, options) => {
const { apiUrl, isJoinPrefix, urlPrefix, joinParamsToUrl, formatDate, joinTime = true } = options;
// 添加接口前缀
if (isJoinPrefix && urlPrefix && isString(urlPrefix)) {
config.url = `${urlPrefix}${config.url}`;
}
// 将baseUrl拼接
if (apiUrl && isString(apiUrl)) {
config.url = `${apiUrl}${config.url}`;
}
const params = config.params || {};
const data = config.data || false;
if (formatDate && data && !isString(data)) {
formatRequestDate(data);
}
if (config.method?.toUpperCase() === 'GET') {
if (!isString(params)) {
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
} else {
// 兼容restful风格
config.url = `${config.url + params}${joinTimestamp(joinTime, true)}`;
config.params = undefined;
}
} else if (!isString(params)) {
if (formatDate) {
formatRequestDate(params);
}
if (
Reflect.has(config, 'data') &&
config.data &&
(Object.keys(config.data).length > 0 || data instanceof FormData)
) {
config.data = data;
config.params = params;
} else {
// 非GET请求如果没有提供data,则将params视为data
config.data = params;
config.params = undefined;
}
if (joinParamsToUrl) {
config.url = setObjToUrlParams(config.url as string, { ...config.params, ...config.data });
}
} else {
// 兼容restful风格
config.url += params;
config.params = undefined;
}
return config;
},
// 请求拦截器处理
requestInterceptors: (config, options) => {
// 请求之前处理config
const token = localStorage.getItem(TOKEN_NAME);
if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
// jwt token
(config as Recordable).headers.Authorization = options.authenticationScheme
? `${options.authenticationScheme} ${token}`
: token;
}
return config as InternalAxiosRequestConfig;
},
// 响应拦截器处理
responseInterceptors: (res) => {
return res;
},
// 响应错误处理
responseInterceptorsCatch: (error: any) => {
const { config } = error;
if (!config || !config.requestOptions.retry) return Promise.reject(error);
config.retryCount = config.retryCount || 0;
if (config.retryCount >= config.requestOptions.retry.count) return Promise.reject(error);
config.retryCount += 1;
const backoff = new Promise((resolve) => {
setTimeout(() => {
resolve(config);
}, config.requestOptions.retry.delay || 1);
});
config.headers = { ...config.headers, 'Content-Type': ContentTypeEnum.Json };
return backoff.then((config) => request.request(config));
},
};
function createAxios(opt?: Partial<CreateAxiosOptions>) {
return new VAxios(
merge(
<CreateAxiosOptions>{
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
// 例如: authenticationScheme: 'Bearer'
authenticationScheme: '',
// 超时
timeout: 10 * 1000,
// 携带Cookie
// withCredentials: true,
// 头信息
headers: { 'Content-Type': ContentTypeEnum.Json },
// 数据处理方式
transform,
// 配置项,下面的选项都可以在独立的接口请求中覆盖
requestOptions: {
// 接口地址
apiUrl: host,
// 是否自动添加接口前缀
// isJoinPrefix: true,
// 接口前缀
// 例如: https://www.baidu.com/api
// urlPrefix: '/api'
// urlPrefix: '/api',
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
isReturnNativeResponse: false,
// 需要对返回数据进行处理
isTransformResponse: true,
// post请求的时候添加参数到url
joinParamsToUrl: false,
// 格式化提交参数时间
formatDate: false,
// 是否加入时间戳
joinTime: false,
// 忽略重复请求
ignoreRepeatRequest: true,
// 是否携带token
withToken: true,
// 重试
retry: {
count: 3,
delay: 1000,
},
},
},
opt || {},
),
);
}
export const request = createAxios();
测试请求
这里以cnode提供的api接口进行测试
cnode的api地址
写请求方式
请求方法
- src/api/test.ts
1
2
3
4
5
6
7
8
9
10
11
12import {request} from '@/utils/request';
import type {QueryTopicParam} from "./model/testModel"
const Api={
list:'/topics', // 分页查询列表
}
export function queryTopicsByPage(param:QueryTopicParam) {
return request.get<unknown>({
url: Api.list,
params: param,
});
}
请求类型定义
- src/api/model/testModel.ts
1
2
3
4
5
6
7
8
9
10// page Number 页数
// tab String 主题分类。目前有 ask share job good
// limit Number 每一页的主题数量
// mdrender String 当为 false 时,不渲染。默认为 true,渲染出现的所有 markdown 格式文本。
export interface QueryTopicParam {
page: number;
tab?: 'ask' | 'share' | 'job' | 'good';
limit: number;
mdrender?: 'true' | 'false';
}
请求调用
- src/components/HelloWorld.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<script setup lang="ts">
import { ref,onMounted } from 'vue'
import {queryTopicsByPage} from "@/api/test";
interface IMsg {
msg:string
}
defineProps<IMsg>()
onMounted(() => {
queryTopicsByPage({page:1,limit:10}).then(res=>{
console.log(res)
})
})
const count = ref(0)
</script>
请求结果
处理报错
我们可以看到,我们的接口是请求通过了,但是并没有处理成功。这是因为我们定义的返回字段与这个不符合,需要去手动修改下
处理完之后,我们就可以打印出我们想要的值了
定义返回字段
1 | export interface QueryTopicResult { |
1 | import {request} from '@/utils/request'; |
此时,我们在vue文件中就可以看到ts的提示了
好了,本文就分享到这里了,封装代码我放在了我的git仓库下期再见!