一直在想如何封装axios才是最合适
- 保留原有库(axios)设计
- 可拔插,没有也不影响
- 简化配置,可扩展
- 功能可自定制原则
- 断网提示
- 接口异常自动重试
- 利用TS特性,IDE可提示传入参和响应参
举个栗子,这里以 vue3-vite2-ts 作为示范。
封装思路
利用 vue-request 状态管理、请求的节流防抖等天然特性加之到Axios身上
将 Axios 和 vue-request 进行整合出新的语言糖
const { data, run, error, loading } = service.use(axiosConfig).run(Options)
参数 axiosConfig 为原 axios 的 请求配置
参数 Options 为原 vue-request 的 Options 参数
解构出来的变量为响应式数据。详细见 公共 API | VueRequest
部分代码
核心类:
/**
* axios封装类,通常这个类不需要作必要的修改配置
* 支持防抖/节流/锁重/断网重连
* 可拦截重复请求(拦截后面的),并将请求结果共享给所有请求源
* 支持 get/post 等多种请求方式,支持自定义headers
*/
import axios, { type AxiosInstance, type AxiosError, type AxiosRequestConfig } from 'axios';
import type { AxiosInterceptors, RequestConfig, useRequestOptions } from './type';
import { useRequest, type Service } from 'vue-request';
class Request {
instance: AxiosInstance;
interceptors?: AxiosInterceptors;
constructor(config: RequestConfig) {
// 请求状态码
const showStatus = (status: number, msg: string) => {
let message = '';
switch (status) {
case 400:
message = '请求错误(400)';
break;
case 401:
message = '未授权,请重新登录(401)';
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 = msg ?? `连接出错(${status})`;
}
return `${message},请稍后重试!`;
};
// 初始化axios实例
this.instance = axios.create(config);
// 实例拦截器
this.interceptors = config.interceptors;
// 注册实例的拦截器
this.instance.interceptors.request.use(this.interceptors?.requestFulfilled, this.interceptors?.requestRejected);
this.instance.interceptors.response.use(this.interceptors?.responseFulfilled, this.interceptors?.responseRejected);
// 注册全局请求拦截器
this.instance.interceptors.request.use(
(config: AxiosRequestConfig<any>) => {
return config;
},
(error: AxiosError) => {
return Promise.reject(error);
}
);
// 注册全局响应拦截器
this.instance.interceptors.response.use(
(res: any) => {
//发生错误返回错误信息
if (axios.isAxiosError(res)) {
return Promise.reject(res);
}
if ('status' in res && res.status !== 200) {
const status = res.status || 0;
res.message = showStatus(status, '');
return Promise.reject(res);
}
//返回成功的响应数据
return res.data;
},
(error: AxiosError) => {
//取消请求,不报错并返回空值
if (axios.isCancel(error)) {
return;
}
//处理http错误,抛到业务代码
if (axios.isAxiosError(error)) {
const status = error.request.status || 0;
if (status == 0 || status == 500) {
return new Promise((resolve, reject) => {
const img = new Image();
//临时判断网络是否通畅
img.src = 'https://www.baidu.com/favicon.ico?_t=' + Date.now();
img.onload = function () {
error.message = showStatus(status, error.message);
reject(error);
};
img.onerror = function () {
error.message = '断网了,请注意您的网络连接';
reject(error);
};
});
}
error.message = showStatus(status, error.message);
return Promise.reject(error);
}
return Promise.reject(error);
}
);
}
request<T>(config: RequestConfig<T>): Promise<T> {
return new Promise((resolve, reject) => {
if (config.interceptors?.requestFulfilled) {
config = config.interceptors.requestFulfilled(config);
}
this.instance
.request<any, T>(config)
.then(res => {
if (config.interceptors?.responseFulfilled) {
res = config.interceptors.responseFulfilled(res);
}
resolve(res);
})
.catch(err => {
if (config.interceptors?.responseRejected) {
err = config.interceptors.responseRejected(err);
}
reject(err);
});
});
}
use<R, P extends unknown[] = any>(config: RequestConfig<R>) {
const req = (options?: useRequestOptions<R, P>) => {
const _Service: Service<R, P> = (...args: P) => {
config.data = args.length > 0 ? args[0] : undefined;
config.params = args.length > 1 ? args[1] : undefined;
return this.request<R>({ ...config });
};
return useRequest<R, P>(_Service, options);
};
return {
run: (options?: useRequestOptions<R, P>) => {
return req(options);
}
};
}
get<T>(url: string, params?: object, config?: RequestConfig<T>): Promise<T> {
config = Object.assign(config || {}, { params });
return this.instance.get(url, config);
}
post<T>(url: string, data?: object, config?: AxiosRequestConfig): Promise<T> {
return this.instance.post(url, data, config);
}
}
export default Request;
实例化:
// service 实例化统一出口
import axios, { type AxiosRequestHeaders } from 'axios';
import { BASE_URL, TIME_OUT } from '@/config';
import AxiosRequest from './request';
// 用于存储请求的标识,便于路由切换时取消请求(仅取消请求不返回响应,并不会截断响应,即前端取消了请求, 实质后端还是会响应的)
const pendingMap = new Map();
/**
* 生成唯一的每个请求的唯一key
* @param config
* @return string
*/
function getPendingKey(config: any) {
const { url, method } = config;
return [url, method].join('&');
}
/**
* 储存每个请求的唯一cancel回调, 以此为标识
* @param config
*/
function addPending(config: any) {
const pendingKey = getPendingKey(config);
config.cancelToken = new axios.CancelToken(cancel => {
if (!pendingMap.has(pendingKey)) {
pendingMap.set(pendingKey, cancel);
}
});
}
/**
* 删除重复的请求
* @param config
*/
function removePending(config: any) {
const pendingKey = getPendingKey(config);
if (pendingMap.has(pendingKey)) {
const cancelToken = pendingMap.get(pendingKey);
cancelToken(pendingKey);
pendingMap.delete(pendingKey);
}
}
const axiosRequest = new AxiosRequest({
baseURL: BASE_URL || '',
timeout: TIME_OUT || 0, // 超时时间,单位毫秒
timeoutErrorMessage: '请求超时',
withCredentials: true, //跨域携带cookie
// 配置实例拦截器
interceptors: {
requestFulfilled: config => {
//清除上次请求,防止重复请求
removePending(config);
addPending(config);
// 携带token的拦截
if (localStorage.getItem('token')) {
(config.headers as AxiosRequestHeaders).Authorization = ('Bearer ' + localStorage.getItem('token')) as string;
}
// 防止GET请求缓存而追加时间戳
if (config.method?.toUpperCase() === 'GET') {
config.params = { ...config.params, _t: new Date().getTime() };
}
// 防止后端无法获取传统表单POST参数
if (typeof config.data == 'object' && Object.values(config.headers as AxiosRequestHeaders).includes('application/x-www-form-urlencoded')) {
//亦可用 import qs from 'qs' 依赖进行处理
//config.data = qs.stringify(config.data)
//直接对象格式化处理
config.data = Object.keys(config.data)
.map(key => encodeURIComponent(key) + '=' + encodeURIComponent(config.data[key]))
.join('&');
}
return config;
},
requestRejected: err => {
return Promise.reject(err);
},
responseFulfilled: res => {
removePending(res.config);
return res;
},
responseRejected: err => {
err.config && removePending(err.config);
return Promise.reject(err);
}
}
});
/**
* 清空所有请求(通常在路由跳转时调用)
*/
export const clearAllPending = () => {
pendingMap.forEach((cancelToken, pendingKey) => {
pendingKey && cancelToken(pendingKey);
pendingMap.delete(pendingKey);
});
};
export const METHOD = {
PUT: 'put',
DELETE: 'delete',
GET: 'get',
POST: 'post'
};
/** 接口响应通用格式 */
export interface IDataType<T = any> {
code: number;
message?: string;
data: T;
}
export default axiosRequest;
使用示例:
import service, { METHOD, type IDataType } from '@/service';
const { data, run, error, loading } = service.use({
url: '/test',
method: METHOD.POST
}).run({manual: false});
run({username:'admin'});
按照TypeScript的风格封装了axios,需要的朋友可以前往vue3-vite2-ts直接拿来使用,对自己来说也是一次学习的收获。封装axios并不难,重点是请求拦截器和响应拦截器,只是要注意ts的类型约束。