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"; import { ElMessage } from "element-plus"; import { message } from "../message"; /**请求后端的地址 未配置则访问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 = (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: 30 * 1000, 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 => { // 开启进度条动画 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(); if (!(response.data instanceof Blob)) 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( method: RequestMethods, url: string, param?: AxiosRequestConfig, axiosConfig?: PureHttpRequestConfig ): Promise { const config = { method, url, ...param, ...axiosConfig } as PureHttpRequestConfig; // 单独处理自定义请求/响应回调 return new Promise((resolve, reject) => { PureHttp.axiosInstance .request(config) .then((response: any) => { if (response.code != null && response.code !== 200) { message(response.message, { type: "error" }); } resolve(response); }) .catch(error => { if (error.status != 200) { ElMessage.warning("请求失败" + error.message); } reject(error); }); }); } /** 单独抽离的`post`工具函数 */ public post( url: string, params?: AxiosRequestConfig

, config?: PureHttpRequestConfig ): Promise { return this.request("post", url, params, config); } /** 单独抽离的`get`工具函数 */ public get( url: string, params?: AxiosRequestConfig

, config?: PureHttpRequestConfig ): Promise { return this.request("get", url, params, config); } } export const http = new PureHttp();