Files
kaidi/frontend/src/utils/request/Axios.ts
T
2026-08-18 22:51:35 +08:00

366 lines
12 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 type {
AxiosError,
AxiosInstance,
AxiosRequestConfig,
AxiosRequestHeaders,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios';
import axios from 'axios';
import cloneDeep from 'lodash/cloneDeep';
import debounce from 'lodash/debounce';
import isFunction from 'lodash/isFunction';
import throttle from 'lodash/throttle';
import { stringify } from 'qs';
import { ContentTypeEnum } from '@/constants';
import type { AxiosRequestConfigRetry, RequestOptions, Result } from '@/types/axios';
import { AxiosCanceler } from './AxiosCancel';
import type { CreateAxiosOptions } from './AxiosTransform';
function localizeApiError(code: string | undefined, detail: string | undefined, status?: number): string {
const messages: Record<string, string> = {
DATABASE_CONNECTION_FAILED: '数据库连接失败,请检查地址、端口、库名和账号权限',
DATABASE_INITIALIZATION_FAILED: '数据库初始化失败,请确认使用专用空库并检查迁移权限',
DATABASE_ENGINE_NOT_SUPPORTED: '当前版本仅支持 MySQL 8.4',
MYSQL_VERSION_UNSUPPORTED: 'MySQL 版本不受支持,需要 MySQL 8.4.x',
DATABASE_COLLATION_PREPARATION_FAILED: '数据库字符集或排序规则准备失败',
DATABASE_MIGRATION_STATE_INVALID: '数据库迁移历史无效,请使用空库或先修复迁移状态',
DATABASE_SCHEMA_INSPECTION_FAILED: '数据库结构检查失败',
SETUP_CODE_INVALID: '安装码不正确',
SETUP_LOCKED: '安装向导已锁定',
SETUP_STATE_WRITE_FAILED: '安装状态文件写入失败,请检查应用目录权限',
ADMIN_PASSWORD_MISMATCH: '管理员密码确认不一致',
};
if (code && messages[code]) return messages[code];
if (detail && /[\u4E00-\u9FFF]/.test(detail)) return detail;
if (status === 401) return '登录会话已失效';
if (status === 403) return '当前账号没有执行此操作的权限';
if (status === 404) return '请求的资源不存在';
if (status === 409) return '数据已发生变化,请刷新后重试';
if (status === 422) return '提交的数据未通过校验';
if (status && status >= 500) return '服务器处理失败,请查看服务日志';
return '网络请求失败,请检查网络连接后重试';
}
/**
* Axios 模块
*/
export class VAxios {
/**
* Axios实例句柄
* @private
*/
private instance: AxiosInstance;
/**
* Axios配置
* @private
*/
private readonly options: CreateAxiosOptions;
/**
* 节流/防抖包装后的请求函数
* @private
*/
private throttledRequest: ((config: AxiosRequestConfigRetry, options?: RequestOptions) => void) | null = null;
/**
* 待resolve的请求回调队列
* @private
*/
private pendingResolvers: {
resolve: (value: any) => void;
}[] = [];
constructor(options: CreateAxiosOptions) {
this.options = options;
this.instance = axios.create(options);
this.setupInterceptors();
this.setupThrottleDebounce();
}
/**
* 创建Axios实例
* @param config
* @private
*/
private createAxios(config: CreateAxiosOptions): void {
this.instance = axios.create(config);
}
/**
* 获取数据处理类
* @private
*/
private getTransform() {
const { transform } = this.options;
return transform;
}
/**
* 获取Axios实例
*/
getAxios(): AxiosInstance {
return this.instance;
}
/**
* 配置Axios
* @param config
*/
configAxios(config: CreateAxiosOptions) {
if (!this.instance) return;
this.createAxios(config);
}
/**
* 设置公共头部信息
* @param headers
*/
setHeader(headers: Record<string, string>): void {
if (!this.instance) return;
Object.assign(this.instance.defaults.headers, headers);
}
/**
* 设置拦截器
* @private
*/
private setupInterceptors() {
const transform = this.getTransform();
if (!transform) return;
const { requestInterceptors, requestInterceptorsCatch, responseInterceptors, responseInterceptorsCatch } =
transform;
const axiosCanceler = new AxiosCanceler();
// 请求拦截器
this.instance.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
// 如果忽略取消令牌,则不会取消重复的请求
// @ts-expect-error 请求参数通过嵌套赋值
const { ignoreCancelToken } = config.requestOptions;
const ignoreCancel = ignoreCancelToken ?? this.options.requestOptions?.ignoreCancelToken;
if (!ignoreCancel) axiosCanceler.addPending(config);
if (requestInterceptors && isFunction(requestInterceptors)) {
config = (await requestInterceptors(config, this.options)) as InternalAxiosRequestConfig;
}
return config;
}, undefined);
// 请求错误处理
if (requestInterceptorsCatch && isFunction(requestInterceptorsCatch)) {
this.instance.interceptors.request.use(undefined, requestInterceptorsCatch);
}
// 响应结果处理
this.instance.interceptors.response.use((res: AxiosResponse) => {
if (res) axiosCanceler.removePending(res.config);
if (responseInterceptors && isFunction(responseInterceptors)) {
res = responseInterceptors(res);
}
return res;
}, undefined);
// 响应错误处理
if (responseInterceptorsCatch && isFunction(responseInterceptorsCatch)) {
this.instance.interceptors.response.use(undefined, (error) => responseInterceptorsCatch(error, this.instance));
}
}
/**
* 支持 FormData 请求格式
* @param config
*/
supportFormData(config: AxiosRequestConfig) {
const headers = config.headers || (this.options.headers as AxiosRequestHeaders);
const contentType = headers?.['Content-Type'] || headers?.['content-type'];
if (
contentType !== ContentTypeEnum.FormURLEncoded ||
!Reflect.has(config, 'data') ||
config.method?.toUpperCase() === 'GET'
) {
return config;
}
return {
...config,
data: stringify(config.data, { arrayFormat: 'brackets' }),
};
}
/**
* 支持 params 序列化
* @param config
*/
supportParamsStringify(config: AxiosRequestConfig) {
const headers = config.headers || this.options.headers;
const contentType = headers?.['Content-Type'] || headers?.['content-type'];
if (contentType === ContentTypeEnum.FormURLEncoded || !Reflect.has(config, 'params')) {
return config;
}
return {
...config,
paramsSerializer: (params: any) => stringify(params, { arrayFormat: 'brackets' }),
};
}
get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'GET' }, options);
}
post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'POST' }, options);
}
put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'PUT' }, options);
}
delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'DELETE' }, options);
}
patch<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'PATCH' }, options);
}
/**
* 上传文件封装
* @param key 文件所属的key
* @param file 文件
* @param config 请求配置
* @param options
*/
upload<T = any>(key: string, file: File, config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
const params: FormData = config.params ?? new FormData();
params.append(key, file);
return this.request(
{
...config,
method: 'POST',
headers: {
'Content-Type': ContentTypeEnum.FormData,
},
params,
},
options,
);
}
/**
* 初始化节流/防抖
* @private
*/
private setupThrottleDebounce() {
const { requestOptions } = this.options;
if (requestOptions?.throttle?.delay) {
this.throttledRequest = throttle((config: AxiosRequestConfigRetry, options?: RequestOptions) => {
const resolver = this.pendingResolvers.shift();
resolver?.resolve(this.synthesisRequest(config, options));
}, requestOptions.throttle.delay);
} else if (requestOptions?.debounce?.delay) {
this.throttledRequest = debounce((config: AxiosRequestConfigRetry, options?: RequestOptions) => {
const resolver = this.pendingResolvers.shift();
resolver?.resolve(this.synthesisRequest(config, options));
}, requestOptions.debounce.delay);
}
}
/**
* 请求封装
* @param config
* @param options
*/
request<T = any>(config: AxiosRequestConfigRetry, options?: RequestOptions): Promise<T> {
const { requestOptions } = this.options;
if (requestOptions?.throttle !== undefined && requestOptions?.debounce !== undefined) {
throw new Error('throttle and debounce cannot be set at the same time');
}
if (this.throttledRequest) {
return new Promise<T>((resolve) => {
this.pendingResolvers.push({ resolve });
this.throttledRequest!(config, options);
});
}
return this.synthesisRequest(config, options);
}
/**
* 请求方法
* @private
*/
private async synthesisRequest<T = any>(config: AxiosRequestConfigRetry, options?: RequestOptions): Promise<T> {
let conf: CreateAxiosOptions = cloneDeep(config);
const transform = this.getTransform();
const { requestOptions } = this.options;
const opt: RequestOptions = { ...requestOptions, ...options };
const { beforeRequestHook, requestCatchHook, transformRequestHook } = transform || {};
if (beforeRequestHook && isFunction(beforeRequestHook)) {
conf = beforeRequestHook(conf, opt);
}
conf.requestOptions = opt;
conf = this.supportFormData(conf);
// 支持params数组参数格式化,因axios默认的toFormData即为brackets方式,无需配置paramsSerializer为qs,有需要可解除注释,参数参考qs文档
// conf = this.supportParamsStringify(conf);
return new Promise((resolve, reject) => {
this.instance
.request<any, AxiosResponse<Result>>(!config.retryCount ? conf : config)
.then((res: AxiosResponse<Result>) => {
if (transformRequestHook && isFunction(transformRequestHook)) {
try {
const ret = transformRequestHook(res, opt);
resolve(ret);
} catch (err) {
reject(err || new Error('请求错误!'));
}
return;
}
resolve(res as unknown as Promise<T>);
})
.catch((e: Error | AxiosError) => {
if (requestCatchHook && isFunction(requestCatchHook)) {
reject(requestCatchHook(e, opt));
return;
}
if (axios.isAxiosError(e)) {
const problem = e.response?.data as
{ code?: string; detail?: string; requestId?: string; fieldErrors?: Record<string, string> } | undefined;
const requestId = problem?.requestId || e.response?.headers?.['x-request-id'];
const message = localizeApiError(problem?.code, problem?.detail, e.response?.status);
const error = new Error(requestId ? `${message}(请求编号:${requestId})` : message);
Object.assign(error, {
code: problem?.code,
status: e.response?.status,
requestId,
fieldErrors: problem?.fieldErrors,
transportFailure: !e.response,
});
if (e.response?.status === 401 && window.location.pathname !== '/login') {
window.dispatchEvent(new CustomEvent('kaidi:session-expired'));
}
reject(error);
return;
}
reject(e);
});
});
}
}