import Axios, { type AxiosInstance, type AxiosRequestConfig, type CustomParamsSerializer, type AxiosResponse, type InternalAxiosRequestConfig, type AxiosError } from 'axios' import { stringify } from 'qs' // 基础配置 const defaultConfig: AxiosRequestConfig = { timeout: 6000, headers: { 'Content-Type': 'application/json;charset=utf-8' }, paramsSerializer: { serialize: stringify as unknown as CustomParamsSerializer } } // 响应数据基础结构 export interface BaseResponse { code: number error?: string isAuthorized?: boolean isSuccess?: boolean } type OmitBaseResponse = Omit export type ResponseData = BaseResponse & OmitBaseResponse export type ResponseValidator = (data: ResponseData) => boolean // 重试配置 export interface RetryConfig { retries?: number retryDelay?: number retryCondition?: (error: AxiosError) => boolean } // 拦截器配置类型 interface InterceptorsConfig { requestInterceptor?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig requestErrorInterceptor?: (error: AxiosError) => Promise responseInterceptor?: (response: AxiosResponse>) => any responseErrorInterceptor?: (error: AxiosError) => Promise } // 请求唯一键 type RequestKey = string | symbol /** * 获取url参数 * @param url * @returns */ export function getParams(url: string) { const paramsString = url.split('?')[1] const searchParams = new URLSearchParams(paramsString) return Object.fromEntries(searchParams.entries()) } class HttpClient { private instance: AxiosInstance private requestInterceptorId?: number private responseInterceptorId?: number private abortControllers: Map = new Map() constructor(customConfig?: AxiosRequestConfig, interceptors?: InterceptorsConfig) { this.instance = Axios.create({ ...defaultConfig, ...customConfig }) this.initInterceptors(interceptors) } private initInterceptors(interceptors?: InterceptorsConfig): void { this.initRequestInterceptor( interceptors?.requestInterceptor, interceptors?.requestErrorInterceptor ) this.initResponseInterceptor( interceptors?.responseInterceptor, interceptors?.responseErrorInterceptor ) } private initRequestInterceptor( customInterceptor?: InterceptorsConfig['requestInterceptor'], customErrorInterceptor?: InterceptorsConfig['requestErrorInterceptor'] ): void { const defaultInterceptor = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => { const search = getParams(window.location.href) const enterpriseCode = search?.['enterpriseCode'] const token = localStorage.getItem('token_' + enterpriseCode) || document.cookie.match(new RegExp('(^| )' + 'x-sessionId_b' + '=([^;]*)(;|$)'))?.[2] // 添加token if (token) { if (!config.headers) { config.headers = {} as InternalAxiosRequestConfig['headers'] } config.headers.Authorization = token } return config } // 默认请求错误拦截器 const defaultErrorInterceptor = (error: AxiosError): Promise => { // todo 错误处理 return Promise.reject(error) } this.requestInterceptorId = this.instance.interceptors.request.use( customInterceptor || defaultInterceptor, customErrorInterceptor || defaultErrorInterceptor ) } private initResponseInterceptor( customInterceptor?: InterceptorsConfig['responseInterceptor'], customErrorInterceptor?: InterceptorsConfig['responseErrorInterceptor'] ): void { this.responseInterceptorId = this.instance.interceptors.response.use( customInterceptor, customErrorInterceptor ) } private getRequestKey(config: AxiosRequestConfig): RequestKey | undefined { if (!config.url) return undefined return `${config.method?.toUpperCase()}-${config.url}` } private setupCancelController( config: AxiosRequestConfig, requestKey?: RequestKey ): AxiosRequestConfig { const key = requestKey || this.getRequestKey(config) if (!key) return config // 如果已有相同key的请求,先取消它 this.cancelRequest(key) const controller = new AbortController() this.abortControllers.set(key, controller) return { ...config, signal: controller.signal } } public removeRequestInterceptor(): void { if (this.requestInterceptorId !== undefined) { this.instance.interceptors.request.eject(this.requestInterceptorId) this.requestInterceptorId = undefined // 重置ID,避免重复移除 } } public removeResponseInterceptor(): void { if (this.responseInterceptorId !== undefined) { this.instance.interceptors.response.eject(this.responseInterceptorId) this.responseInterceptorId = undefined // 重置ID,避免重复移除 } } public setRequestInterceptor( customInterceptor?: InterceptorsConfig['requestInterceptor'], customErrorInterceptor?: InterceptorsConfig['requestErrorInterceptor'] ): void { this.removeRequestInterceptor() this.initRequestInterceptor(customInterceptor, customErrorInterceptor) } public setResponseInterceptor( customInterceptor?: InterceptorsConfig['responseInterceptor'], customErrorInterceptor?: InterceptorsConfig['responseErrorInterceptor'] ): void { this.removeResponseInterceptor() this.initResponseInterceptor(customInterceptor, customErrorInterceptor) } public getInstance(): AxiosInstance { return this.instance } public cancelRequest(key: RequestKey, message?: string): boolean { const controller = this.abortControllers.get(key) if (controller) { controller.abort(message || `取消请求: ${String(key)}`) this.abortControllers.delete(key) return true } return false } public cancelAllRequests(message?: string): void { this.abortControllers.forEach((controller, key) => { controller.abort(message || `取消所有请求: ${String(key)}`) }) this.abortControllers.clear() } public static isCancel(error: unknown): boolean { return Axios.isCancel(error) } private sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } /** * 通用请求方法 * @param url 请求地址 * @param config 请求配置 * @returns 响应数据 */ public async request( url: string, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig } ): Promise> { const { requestKey, retry, method, ...restConfig } = config || {} const defaultRetryCondition = (error: AxiosError) => { // 默认只重试网络错误或5xx服务器错误 return !error.response || (error.response.status >= 500 && error.response.status < 600) } const retryConfig = { retries: 0, retryDelay: 1000, retryCondition: defaultRetryCondition, ...retry } let lastError: any const key = requestKey || this.getRequestKey({ ...restConfig, method, url }) for (let attempt = 0; attempt <= retryConfig.retries; attempt++) { try { if (attempt > 0 && key) { this.abortControllers.delete(key) } const requestConfig = this.setupCancelController({ ...restConfig, method, url }, requestKey) const response = await this.instance.request>(requestConfig) return response.data } catch (error) { lastError = error if ( attempt === retryConfig.retries || !retryConfig.retryCondition(error as AxiosError) || HttpClient.isCancel(error) ) { break } // 延迟后重试 if (retryConfig.retryDelay > 0) { await this.sleep(retryConfig.retryDelay) } } } return Promise.reject(lastError) } /** * GET 请求 * @param url 请求地址 * @param config 请求配置 * @returns 响应数据 */ public get( url: string, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig } ): Promise> { return this.request(url, { ...config, method: 'GET' }) } /** * POST 请求 * @param url 请求地址 * @param data 请求数据 * @param config 请求配置 * @returns 响应数据 */ public post( url: string, data?: any, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig } ): Promise> { return this.request(url, { ...config, data, method: 'POST' }) } /** * PUT 请求 * @param url 请求地址 * @param data 请求数据 * @param config 请求配置 * @returns 响应数据 */ public put( url: string, data?: any, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig } ): Promise> { return this.request(url, { ...config, data, method: 'PUT' }) } /** * DELETE 请求 * @param url 请求地址 * @param config 请求配置 * @returns 响应数据 */ public delete( url: string, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig } ): Promise> { return this.request(url, { ...config, method: 'DELETE' }) } /** * PATCH 请求 * @param url 请求地址 * @param data 请求数据 * @param config 请求配置 * @returns 响应数据 */ public patch( url: string, data?: any, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig } ): Promise> { return this.request(url, { ...config, data, method: 'PATCH' }) } } const http = new HttpClient() // 导出request供api-service使用 export const request = http.request.bind(http) export default http