#Axios 通用请求封装
#特性
- 统一
baseURL、超时、Token 注入 - 统一业务错误/HTTP 错误处理
- 支持自动重试(网络错误/超时)
- 支持取消请求(
AbortController) - 支持文件上传/下载
- 返回值类型清晰,不再出现“类型和运行时不一致”
#代码
src/utils/request.ts
import axios, {
AxiosError,
AxiosInstance,
AxiosRequestConfig,
InternalAxiosRequestConfig,
} from 'axios'
/** 后端统一响应结构(可按你的项目调整) */
export interface ApiResponse<T = unknown> {
code: number
message: string
data: T
success?: boolean
}
/** 扩展请求配置 */
export interface RequestConfig extends AxiosRequestConfig {
/** 是否跳过 Token 注入 */
ignoreToken?: boolean
/** 是否打印错误日志 */
showError?: boolean
/** 重试次数(仅网络错误/超时) */
retry?: number
/** 重试间隔(毫秒) */
retryDelay?: number
}
/** createRequest 的可配置项 */
export interface RequestOptions {
baseURL?: string
timeout?: number
tokenKey?: string
tokenPrefix?: string
successCodes?: number[]
}
type RetryConfig = RequestConfig & { _retryCount?: number }
const DEFAULT_OPTIONS: Required<RequestOptions> = {
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 15000,
tokenKey: 'token',
tokenPrefix: 'Bearer',
successCodes: [0, 200],
}
const HTTP_ERROR_MAP: Record<number, string> = {
400: '请求参数错误',
401: '登录状态已失效',
403: '无权限访问',
404: '请求资源不存在',
405: '请求方法不允许',
408: '请求超时',
409: '请求冲突',
422: '参数校验失败',
429: '请求过于频繁',
500: '服务器内部错误',
502: '网关错误',
503: '服务不可用',
504: '网关超时',
}
const isDev = import.meta.env.DEV
const sleep = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms))
const isApiResponse = (data: unknown): data is ApiResponse =>
!!data &&
typeof data === 'object' &&
'code' in data &&
'message' in data
const isTimeoutError = (error: AxiosError): boolean =>
error.code === 'ECONNABORTED' || /timeout/i.test(error.message || '')
const isNetworkError = (error: AxiosError): boolean => !error.response
const canRetryMethod = (method?: string): boolean => {
const m = (method || 'get').toLowerCase()
return m === 'get' || m === 'head' || m === 'options'
}
export const getToken = (tokenKey = DEFAULT_OPTIONS.tokenKey): string | null =>
localStorage.getItem(tokenKey) || sessionStorage.getItem(tokenKey)
export const setToken = (
token: string,
remember = false,
tokenKey = DEFAULT_OPTIONS.tokenKey,
): void => {
if (remember) {
localStorage.setItem(tokenKey, token)
} else {
sessionStorage.setItem(tokenKey, token)
}
}
export const removeToken = (tokenKey = DEFAULT_OPTIONS.tokenKey): void => {
localStorage.removeItem(tokenKey)
sessionStorage.removeItem(tokenKey)
}
/**
* 创建 axios 实例
*/
export const createRequest = (customOptions?: RequestOptions): AxiosInstance => {
const options = {
...DEFAULT_OPTIONS,
...customOptions,
}
const successCodeSet = new Set(options.successCodes)
const instance = axios.create({
baseURL: options.baseURL,
timeout: options.timeout,
withCredentials: false,
})
// 请求拦截
instance.interceptors.request.use(
(config: InternalAxiosRequestConfig & RequestConfig) => {
if (!config.ignoreToken) {
const token = getToken(options.tokenKey)
if (token) {
config.headers.Authorization = `${options.tokenPrefix} ${token}`
}
}
if (isDev) {
console.log('[request]', config.method?.toUpperCase(), config.url, {
params: config.params,
data: config.data,
})
}
return config
},
(error: AxiosError) => Promise.reject(error),
)
// 响应拦截
instance.interceptors.response.use(
(response) => {
// 下载场景:直接返回 Blob / ArrayBuffer
const responseType = response.config.responseType
if (responseType === 'blob' || responseType === 'arraybuffer') {
return response.data
}
const data = response.data
const config = response.config as RequestConfig
// 非统一业务结构,直接透传
if (!isApiResponse(data)) {
return data
}
const isSuccess = successCodeSet.has(data.code) || data.success === true
if (isSuccess) {
return data
}
// 业务失败
if (data.code === 401) {
removeToken(options.tokenKey)
console.warn('[request] 登录状态失效,请重新登录')
}
if (config.showError !== false) {
console.error('[request] 业务错误:', data.message || '请求失败', data)
}
const bizError = new Error(data.message || '请求失败') as Error & {
code?: number
response?: ApiResponse
}
bizError.code = data.code
bizError.response = data
return Promise.reject(bizError)
},
async (error: AxiosError) => {
const config = error.config as RetryConfig | undefined
// 重试:仅网络错误/超时 + 幂等请求
if (
config &&
(isNetworkError(error) || isTimeoutError(error)) &&
canRetryMethod(config.method)
) {
const maxRetries = config.retry ?? 0
const currentRetry = config._retryCount ?? 0
if (currentRetry < maxRetries) {
config._retryCount = currentRetry + 1
const retryDelay = config.retryDelay ?? 1000
if (isDev) {
console.warn(
`[request] 第 ${config._retryCount} 次重试,${retryDelay}ms 后继续`,
)
}
await sleep(retryDelay)
return instance(config)
}
}
const status = error.response?.status
const message = status
? HTTP_ERROR_MAP[status] || `请求失败(${status})`
: isTimeoutError(error)
? '请求超时,请检查网络'
: '网络异常,请稍后重试'
const shouldLogError = (config as RequestConfig | undefined)?.showError !== false
if (shouldLogError) {
console.error('[request] HTTP错误:', message, error)
}
return Promise.reject(error)
},
)
return instance
}
/** 单例实例 */
export const request = createRequest()
/** 通用请求 */
export const http = <T = unknown>(config: RequestConfig): Promise<ApiResponse<T>> =>
request.request<any, ApiResponse<T>>(config)
/** GET */
export const get = <T = unknown>(
url: string,
params?: Record<string, unknown>,
config?: RequestConfig,
): Promise<ApiResponse<T>> =>
request.get<any, ApiResponse<T>>(url, { ...config, params })
/** POST */
export const post = <T = unknown>(
url: string,
data?: unknown,
config?: RequestConfig,
): Promise<ApiResponse<T>> =>
request.post<any, ApiResponse<T>>(url, data, config)
/** PUT */
export const put = <T = unknown>(
url: string,
data?: unknown,
config?: RequestConfig,
): Promise<ApiResponse<T>> =>
request.put<any, ApiResponse<T>>(url, data, config)
/** DELETE */
export const del = <T = unknown>(
url: string,
params?: Record<string, unknown>,
config?: RequestConfig,
): Promise<ApiResponse<T>> =>
request.delete<any, ApiResponse<T>>(url, { ...config, params })
/** PATCH */
export const patch = <T = unknown>(
url: string,
data?: unknown,
config?: RequestConfig,
): Promise<ApiResponse<T>> =>
request.patch<any, ApiResponse<T>>(url, data, config)
/** 上传文件 */
export const upload = <T = unknown>(
url: string,
formData: FormData,
config?: RequestConfig,
): Promise<ApiResponse<T>> => {
// 不手动写 multipart/form-data,浏览器会自动带 boundary
return request.post<any, ApiResponse<T>>(url, formData, {
...config,
headers: {
...config?.headers,
},
})
}
/** 下载文件 */
export const download = (
url: string,
params?: Record<string, unknown>,
config?: RequestConfig,
): Promise<Blob> =>
request.get<any, Blob>(url, {
...config,
params,
responseType: 'blob',
})
/** 创建可取消请求 */
export const createCancellableRequest = <T = unknown>(config: RequestConfig) => {
const controller = new AbortController()
const promise = http<T>({
...config,
signal: controller.signal,
})
return {
request: promise,
cancel: () => controller.abort(),
}
}
export default request#使用示例
src/api/user.ts
import { get, post, download } from '@/utils/request'
export const fetchUserInfo = async () => {
const res = await get<{ id: string; name: string }>('/user/info')
return res.data
}
export const login = async (username: string, password: string) => {
const res = await post<{ token: string }>('/auth/login', { username, password })
return res.data
}
export const exportFile = async () => {
const blob = await download('/report/export', { month: '2026-03' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'report.xlsx'
a.click()
URL.revokeObjectURL(url)
}#请求重试示例
说明:
- 仅网络错误/超时会触发重试
- 默认只对幂等请求重试(
GET/HEAD/OPTIONS)
src/api/retry-demo.ts
import { get } from '@/utils/request'
export const fetchOrderList = async () => {
const res = await get<{ list: Array<{ id: string; amount: number }> }>(
'/order/list',
{ page: 1, pageSize: 20 },
{
retry: 2, // 失败后最多再重试 2 次
retryDelay: 800, // 每次重试间隔 800ms
},
)
return res.data
}#请求取消示例
src/api/cancel-demo.ts
import { createCancellableRequest } from '@/utils/request'
export const fetchSlowData = () => {
const { request, cancel } = createCancellableRequest<{ value: string }>({
url: '/slow-api/data',
method: 'get',
params: { q: 'keyword' },
})
return { request, cancel }
}在vue组件里使用
import { onBeforeUnmount } from 'vue'
import { fetchSlowData } from '@/api/cancel-demo'
const { request, cancel } = fetchSlowData()
request
.then((res) => {
console.log('请求成功:', res.data)
})
.catch((err) => {
console.log('请求失败或被取消:', err?.message || err)
})
// 组件销毁时取消请求,避免无效回调
onBeforeUnmount(() => {
cancel()
})#依赖
npm i axios
