262 lines
7.9 KiB
TypeScript
262 lines
7.9 KiB
TypeScript
import Axios, {
|
||
type AxiosInstance,
|
||
type AxiosRequestConfig,
|
||
type CustomParamsSerializer
|
||
} from "axios";
|
||
import type {
|
||
PureHttpError,
|
||
RequestMethods,
|
||
PureHttpResponse,
|
||
PureHttpRequestConfig
|
||
} from "./types.d";
|
||
import { stringify } from "qs";
|
||
import NProgress from "../progress";
|
||
import { getToken, formatToken } from "@/utils/auth";
|
||
import { useUserStoreHook } from "@/store/modules/user";
|
||
import { string } from "vue-types";
|
||
import router from "@/router";
|
||
|
||
/**请求后端的地址 未配置则访问BaseURL */
|
||
const apiServiceConfig = {
|
||
userCenter: import.meta.env.VITE_API_USERCENTER_URL,
|
||
usercenter: import.meta.env.VITE_API_USERCENTER_URL
|
||
};
|
||
|
||
function setAPIUrl(c: PureHttpRequestConfig): void {
|
||
let url = c.url;
|
||
let token = url.startsWith("/") ? url.split("/")[1] : url.split("/")[0];
|
||
if (apiServiceConfig[token] != null) {
|
||
c.url = url.replaceAll(token, "");
|
||
c.baseURL = apiServiceConfig[token];
|
||
} else c.baseURL = import.meta.env.VITE_API_BASEURL;
|
||
}
|
||
|
||
const snakeToCamel = (str: string): string => {
|
||
// 处理蛇形命名(user_id → userId)
|
||
let result = str.replace(/_([a-zA-Z0-9])/g, (_, letter) =>
|
||
letter.toUpperCase()
|
||
);
|
||
|
||
// 处理大驼峰命名(UserName → userName)
|
||
if (result.length > 0) {
|
||
result = result.charAt(0).toLowerCase() + result.slice(1);
|
||
}
|
||
|
||
// 特殊场景:处理连续下划线(__type → Type)
|
||
result = result.replace(/^_+/, "");
|
||
|
||
return result;
|
||
};
|
||
|
||
/**
|
||
* 递归转换对象/数组的键名为小驼峰格式
|
||
*/
|
||
const convertKeysToCamelCase = <T>(data: any): T => {
|
||
if (Array.isArray(data)) {
|
||
return data.map(item => convertKeysToCamelCase(item)) as unknown as T;
|
||
}
|
||
|
||
if (data !== null && typeof data === "object") {
|
||
return Object.keys(data).reduce((result, key) => {
|
||
const camelKey = snakeToCamel(key);
|
||
const value = data[key];
|
||
return {
|
||
...result,
|
||
[camelKey]: convertKeysToCamelCase(value)
|
||
};
|
||
}, {}) as T;
|
||
}
|
||
|
||
return data as T;
|
||
};
|
||
|
||
// 相关配置请参考:www.axios-js.com/zh-cn/docs/#axios-request-config-1
|
||
const defaultConfig: AxiosRequestConfig = {
|
||
baseURL: import.meta.env.VITE_API_BASEURL,
|
||
// 请求超时时间
|
||
timeout: 10000,
|
||
headers: {
|
||
Accept: "application/json, text/plain, */*",
|
||
"Content-Type": "application/json",
|
||
"X-Requested-With": "XMLHttpRequest"
|
||
},
|
||
// 数组格式参数序列化(https://github.com/axios/axios/issues/5142)
|
||
paramsSerializer: {
|
||
serialize: stringify as unknown as CustomParamsSerializer
|
||
}
|
||
};
|
||
|
||
class PureHttp {
|
||
constructor() {
|
||
this.httpInterceptorsRequest();
|
||
this.httpInterceptorsResponse();
|
||
}
|
||
|
||
/** `token`过期后,暂存待执行的请求 */
|
||
private static requests = [];
|
||
|
||
/** 防止重复刷新`token` */
|
||
private static isRefreshing = false;
|
||
|
||
/** 初始化配置对象 */
|
||
private static initConfig: PureHttpRequestConfig = {};
|
||
|
||
/** 保存当前`Axios`实例对象 */
|
||
private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);
|
||
|
||
/** 重连原始请求 */
|
||
private static retryOriginalRequest(config: PureHttpRequestConfig) {
|
||
return new Promise(resolve => {
|
||
PureHttp.requests.push((token: string) => {
|
||
config.headers["Authorization"] = formatToken(token);
|
||
resolve(config);
|
||
});
|
||
});
|
||
}
|
||
|
||
/** 请求拦截 */
|
||
private httpInterceptorsRequest(): void {
|
||
PureHttp.axiosInstance.interceptors.request.use(
|
||
async (config: PureHttpRequestConfig): Promise<any> => {
|
||
// 开启进度条动画
|
||
NProgress.start();
|
||
if (config.url.indexOf("http") === -1) {
|
||
setAPIUrl(config);
|
||
}
|
||
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
|
||
if (typeof config.beforeRequestCallback === "function") {
|
||
config.beforeRequestCallback(config);
|
||
return config;
|
||
}
|
||
if (PureHttp.initConfig.beforeRequestCallback) {
|
||
PureHttp.initConfig.beforeRequestCallback(config);
|
||
return config;
|
||
}
|
||
/** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */
|
||
const whiteList = ["/refresh-token", "/login"];
|
||
return whiteList.some(url => config.url.endsWith(url))
|
||
? config
|
||
: new Promise(resolve => {
|
||
const data = getToken();
|
||
if (data) {
|
||
const now = new Date().getTime();
|
||
const expired = parseInt(data.expires) - now <= 0;
|
||
if (expired) {
|
||
if (!PureHttp.isRefreshing) {
|
||
PureHttp.isRefreshing = true;
|
||
// token过期刷新
|
||
useUserStoreHook()
|
||
.handRefreshToken({ refreshToken: data.refreshToken })
|
||
.then(res => {
|
||
const token = res.data.accessToken;
|
||
config.headers["Authorization"] = formatToken(token);
|
||
PureHttp.requests.forEach(cb => cb(token));
|
||
PureHttp.requests = [];
|
||
})
|
||
.finally(() => {
|
||
PureHttp.isRefreshing = false;
|
||
});
|
||
}
|
||
resolve(PureHttp.retryOriginalRequest(config));
|
||
} else {
|
||
config.headers["Authorization"] = formatToken(
|
||
data.accessToken
|
||
);
|
||
resolve(config);
|
||
}
|
||
} else {
|
||
resolve(config);
|
||
}
|
||
});
|
||
},
|
||
error => {
|
||
return Promise.reject(error);
|
||
}
|
||
);
|
||
}
|
||
|
||
/** 响应拦截 */
|
||
private httpInterceptorsResponse(): void {
|
||
const instance = PureHttp.axiosInstance;
|
||
instance.interceptors.response.use(
|
||
(response: PureHttpResponse) => {
|
||
const $config = response.config;
|
||
// 关闭进度条动画
|
||
NProgress.done();
|
||
response.data = convertKeysToCamelCase(response.data);
|
||
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
|
||
if (typeof $config.beforeResponseCallback === "function") {
|
||
$config.beforeResponseCallback(response);
|
||
return response.data;
|
||
}
|
||
if (PureHttp.initConfig.beforeResponseCallback) {
|
||
PureHttp.initConfig.beforeResponseCallback(response);
|
||
return response.data;
|
||
}
|
||
return response.data;
|
||
},
|
||
(error: PureHttpError) => {
|
||
const $error = error;
|
||
$error.isCancelRequest = Axios.isCancel($error);
|
||
// 关闭进度条动画
|
||
NProgress.done();
|
||
if (error.response?.status === 403) {
|
||
// 跳转到403页面
|
||
router.push({
|
||
path: "/error/403"
|
||
});
|
||
}
|
||
// 所有的响应异常 区分来源为取消请求/非取消请求
|
||
return Promise.reject($error);
|
||
}
|
||
);
|
||
}
|
||
|
||
/** 通用请求工具函数 */
|
||
public request<T>(
|
||
method: RequestMethods,
|
||
url: string,
|
||
param?: AxiosRequestConfig,
|
||
axiosConfig?: PureHttpRequestConfig
|
||
): Promise<T> {
|
||
const config = {
|
||
method,
|
||
url,
|
||
...param,
|
||
...axiosConfig
|
||
} as PureHttpRequestConfig;
|
||
|
||
// 单独处理自定义请求/响应回调
|
||
return new Promise((resolve, reject) => {
|
||
PureHttp.axiosInstance
|
||
.request(config)
|
||
.then((response: undefined) => {
|
||
resolve(response);
|
||
})
|
||
.catch(error => {
|
||
reject(error);
|
||
});
|
||
});
|
||
}
|
||
|
||
/** 单独抽离的`post`工具函数 */
|
||
public post<T, P>(
|
||
url: string,
|
||
params?: AxiosRequestConfig<P>,
|
||
config?: PureHttpRequestConfig
|
||
): Promise<T> {
|
||
return this.request<T>("post", url, params, config);
|
||
}
|
||
|
||
/** 单独抽离的`get`工具函数 */
|
||
public get<T, P>(
|
||
url: string,
|
||
params?: AxiosRequestConfig<P>,
|
||
config?: PureHttpRequestConfig
|
||
): Promise<T> {
|
||
return this.request<T>("get", url, params, config);
|
||
}
|
||
}
|
||
|
||
export const http = new PureHttp();
|