Appearance
axios封装
功能列表
- 1、全局loading
- 2、自动防止重复提交
- 3、页面跳转时,自动取消还未完成的请求
- 4、登录信息过期,用户无感知刷新登录状态
- 5、全局拦截器,错误提示等等
- 6、快捷调用方式
全局loading
- 1、全局定义一个
loadingCount
用来记录请求的次数,默认是0 - 2、当有用户调用接口时,如果
loadingCount
时0,开启loading动画,并且loadingCount累加 - 3、当接口请求成功、或者接口异常时,loadingCount递减,当loadingCount等于0时,说明所以接口已经请求完毕,需要关闭loading动画
自动防止重复提交
- 1、全局定义一个
Map
,用来记录接口生成的标记 - 2、当有用户调用接口时,根据接口的地址、请求的方式、参数,生成一个字符串
- 3、在接口发送之前在
request.use
进行拦截,如果Map对象中已经存在此标记,说明上一次请求还未结束,抛出一自定义异常来终止这次的请求 - 4、当接口请求成功、或者接口异常时需要从Map对象中删除接口标记
页面跳转时,自动取消还未完成的请求
- 1、当有用户调用接口时在
request.use
进行拦截,创建一个axios.CancelToken
,把需要取消的请求,存储vuex在队列中 - 2、Vue当页面发生跳转时,首先会进入
beforeEach
钩子函数,从vuex取出CancelToken,取消未完成的请求
登录信息过期,用户无感知刷新登录状态
- 1、首先我们是采用的JWT的方式,用来记录用户的状态,在登录的时候会
accessToken
过期时间是几个小时,而另外一个refreshToken
用来刷新accessToken
- 2、预先和服务端定义好,当用户状态过期时,请求接口的时候返回一个
0200002
的code码 - 3、当前端在response.use中得知状态码是
0200002
,首先用户通过refreshToken
刷新 - 4、刷新成功后,直接在继续通过上下文对象请求完成接口调用
解决并发登录信息过期问题
- 1、全局定义一个
waitToken
标志位默认为false
- 2、当拦截到接口登录信息过期时,如果
waitToken
标志位false
时,将标志位改为true
并且去换取新的accessToken
- 3、否则创建一个
Promise
对象,通过subscribe订阅器
将resolve(this.request(res.config))
收集起来 - 4、当accessToken换取之后,通过
dispatch
出发执行订阅的函数执行resolve
,并且将收集器清空
ts
import axios from 'axios';
import type {
AxiosInstance,
AxiosRequestConfig,
Canceler,
AxiosResponse,
} from 'axios';
import { showLoadingToast, closeToast, showFailToast } from 'vant';
import { refreshToken } from './../apis';
import { store } from './../store';
import { I18nMessages, Typelanguage } from '../locales';
import pubSub from '.';
interface Params {
[key: string]: unknown;
}
interface IAxiosHelper {
isLoading: boolean;
isToast: boolean;
isCancelReq: boolean;
isDeduplication: boolean;
reqUniqueKey: string;
}
const { subscribe, dispatch } = pubSub();
axios.defaults.headers.common['Content-Type'] =
'application/json;charset=UTF-8';
class Httprequest {
private timeout: number;
private queue: Params;
private loadingCount: number;
private waitToken: boolean; // 是否正在换取新的Token
constructor() {
this.timeout = 50000; // 请求的超时时间50秒
this.queue = {}; // 请求队列,防止重复点击
this.loadingCount = 0;
this.waitToken = false;
}
_closeLoading(isLoading: boolean) {
if (isLoading) {
this.loadingCount--;
if (this.loadingCount <= 0) {
closeToast();
this.loadingCount = 0;
}
}
}
setinterceptors(
instance: AxiosInstance,
{
isLoading,
isToast,
isCancelReq,
isDeduplication,
reqUniqueKey,
}: IAxiosHelper,
) {
const { processing, serverException } =
I18nMessages[store.state.language.type as Typelanguage].http;
instance.interceptors.request.use((config) => {
const token: string = store.state.user.token;
if (config.headers) {
if (token) {
config.headers['X-Token'] = token;
}
}
if (isDeduplication) {
if (this.queue[reqUniqueKey]) {
throw 'EXCEPTION:REPEATCLICK';
} else {
this.queue[reqUniqueKey] = reqUniqueKey;
}
}
if (isLoading) {
this.loadingCount++;
showLoadingToast({
duration: 0,
forbidClick: true,
message: 'Loading...',
});
}
if (isCancelReq) {
config.cancelToken = new axios.CancelToken((cancel: Canceler) => {
store.dispatch(`cancelReq/enqueue`, cancel);
});
}
return config;
});
instance.interceptors.response.use(
(res: AxiosResponse) => {
delete this.queue[reqUniqueKey];
this._closeLoading(isLoading);
if (Number(res.data.code) === 0) {
return Promise.resolve(res.data.data);
} else if (res.data.code === '0200002') {
console.log('登录过期');
// 登录过期
const refreshTokenApi = new Promise<void>((resolve, reject) => {
if (!this.waitToken) {
this.waitToken = true;
// 换取刷新Token
store.dispatch('user/updateUserInfo', {
token: store.state.user.refreshToken,
});
refreshToken()
.then((tokenRes) => {
const token = tokenRes.token;
if (token) {
// 更新用户token&refreshToken
store.dispatch('user/updateUserInfo', {
token: token,
refreshToken: tokenRes.refreshToken,
});
resolve();
this.waitToken = false;
dispatch();
} else {
console.log('重新登录');
reject();
this.waitToken = false;
dispatch();
}
})
.catch(() => {
reject();
this.waitToken = false;
dispatch();
});
} else {
console.log('正在换取Token,请等待', this.waitToken);
return new Promise((resolve) => {
subscribe(() => {
setTimeout(() => resolve(this.request(res.config)));
});
});
}
});
return refreshTokenApi.then(() => this.request(res.config));
}
if (isToast) {
showFailToast(res.data.message);
}
return Promise.reject(res.data);
},
(error) => {
this._closeLoading(isLoading);
if (error === 'EXCEPTION:REPEATCLICK') {
showFailToast(processing);
} else if (error.__proto__.constructor.name === 'Cancel') {
delete this.queue[reqUniqueKey];
console.log('请求取消');
} else {
delete this.queue[reqUniqueKey];
// 请求取消不处理
if (error.message !== 'canceled') {
showFailToast(serverException);
}
}
return Promise.reject(error.message || error);
},
);
}
/**
*
* @param {*} axiosConfig axios 参数
* @param {*} isLoading 开启loading
* @param {*} isToast 开启接口异常提示
* @param {*} isCancelReq 取消上一个页面未完成的请求
* @param {*} isDeduplication 开启接口防重
* @returns
*/
request<T>(
axiosConfig: AxiosRequestConfig,
{
isLoading = true,
isToast = true,
isCancelReq = true,
isDeduplication = true,
}: Partial<IAxiosHelper> = {},
): Promise<T> {
const instance: AxiosInstance = axios.create();
axiosConfig.method || (axiosConfig.method = 'post');
const config = {
baseURL: import.meta.env.VITE_API_HOST,
timeout: this.timeout,
...axiosConfig,
};
let reqUniqueKey = '';
if (isDeduplication) {
let paramsStr = '';
if (axiosConfig.method.toLocaleUpperCase() === 'GET') {
paramsStr = JSON.stringify(config.params || {});
} else {
paramsStr = JSON.stringify(config.data || {});
}
// 确定唯一性(路由+接口地址+请求方式)
reqUniqueKey =
`${location.href}${config.method}${config.url}${paramsStr}`.toLocaleUpperCase();
}
this.setinterceptors(instance, {
isLoading,
isToast,
isCancelReq,
isDeduplication,
reqUniqueKey,
});
return instance(config);
}
}
const http = new Httprequest();
export const httpGet = <T>(
url: string,
params?: Params,
axiosHelper?: Partial<IAxiosHelper>,
): Promise<T> => {
return http.request<T>(
{
url,
params,
method: 'GET',
},
axiosHelper,
);
};
export const httpPost = <T>(
url: string,
data?: Params,
axiosHelper?: Partial<IAxiosHelper>,
): Promise<T> => {
return http.request<T>(
{
url,
data,
},
axiosHelper,
);
};
export default http.request.bind(http);
ts
import { UserInfo } from '../store/user';
import { httpGet, httpPost } from '../utils/Httprequest';
export const loginApi = (data: {
phone: string;
code: string;
}): Promise<UserInfo> => {
return httpPost('/user/login', data);
};
// 刷新Token
export const refreshToken = (): Promise<{
token: string;
refreshToken: string;
}> => {
return httpGet(
'/user/refresh',
{},
{
isLoading: false,
isToast: false,
isCancelReq: false,
isDeduplication: false,
},
);
};