Learn.Archives.Web/src/utils/http/index.ts

262 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();