Appearance
axios 封装
功能列表
- 1、全局 Loading 控制
- 2、自动防止重复提交
- 3、路由跳转自动取消请求
- 4、双 Token,无感知刷新
- 5、全局拦截与错误处理
- 6、快捷调用方式
全局 Loading 控制
全局 Loading 控制机制
计数器初始化
全局维护一个请求计数器loadingCount
,初始值为 0Loading 触发机制
- 当发起新请求时,若计数器为 0 则显示全局 Loading 动画
- 每次请求发起时计数器自增(无论是否显示 Loading)
智能关闭机制
- 请求完成后(无论成功或失败)计数器自减
- 仅当计数器归零时自动关闭 Loading 动画
- 确保所有并发请求完成前保持 Loading 状态
核心优势
精准控制
避免多请求场景下 Loading 频繁闪烁资源优化
仅当首个请求触发显示,后续请求复用状态容错保障
异常场景也能确保计数器递减,防止"卡死"无缝体验
所有并发请求完成后自然关闭,无提前关闭风险
示例场景:当页面同时发起 3 个请求时,Loading 只会在首个请求时显示,并在最后一个请求完成后关闭,即使各请求响应时间不同。
自动防止重复提交
- 全局请求映射表
创建 RequestMap(Map 类型)存储所有进行中的请求标记 - 唯一请求指纹生成
根据请求的方式、参数,生成一个请求指纹 - 请求拦截校验
在请求发送前执行拦截,检查 RequestMap 中是否存在该请求指纹,若存在则直接抛出异常,终止请求 - 自动清理机制 请求成功、失败或被取消时,从 RequestMap 中删除对应的请求指纹
路由跳转自动取消请求
请求拦截阶段,在请求发送前创建取消令牌并存储
路由拦截阶段,在路由跳转前取消未完成请求
双 Token,无感知刷新(解决并发登录)
- Token 检查机制 在请求响应阶段进行拦截,检查当前 Token 是否过期
- Token 过期处理
若 Token 过期,则尝试刷新 Token - Token 刷新机制
在刷新 Token 的过程中,将所有未完成的请求挂起(存储在订阅队列中),等待 Token 刷新成功后再继续发送 - Token 刷新成功
刷新成功后,将所有挂起的请求进行派发
重新发送 - Token 刷新失败
刷新失败后,统一处理错误,如跳转到登录页面
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,
}
);
};