#Axios 请求封装
#封装 axios 实例
utils/request.ts
/**
* axios 请求封装模块
* 提供统一的请求/响应拦截、错误处理、请求重试等功能
*/
import axios, {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
AxiosError,
InternalAxiosRequestConfig
} from 'axios'
import {
ElMessage,
ElMessageBox
} from 'element-plus'
// ==================== 类型定义 ====================
/** 请求配置扩展 */
export interface RequestConfig extends AxiosRequestConfig {
/** 是否显示加载状态 */
loading?: boolean
/** 是否显示错误提示 */
showError?: boolean
/** 请求重试次数 */
retry?: number
/** 请求重试延迟(毫秒) */
retryDelay?: number
/** 是否忽略 Token */
ignoreToken?: boolean
/** 自定义错误处理 */
errorHandler?: (error: AxiosError) => void
}
/** 响应数据结构 */
export interface ResponseData<T = any> {
code: number
data: T
message: string
success: boolean
}
/** 错误响应数据结构 */
export interface ErrorResponse {
code: number
message: string
data?: any
}
/** 请求方法类型 */
export type RequestMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options'
// ==================== 常量配置 ====================
/** 默认配置 */
const DEFAULT_CONFIG = {
/** 基础 URL */
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
/** 请求超时时间 */
timeout: 30000,
/** 默认错误提示 */
errorMessage: '网络请求失败,请稍后重试',
/** 请求重试次数 */
retry: 3,
/** 请求重试延迟 */
retryDelay: 1000,
/** Content-Type */
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
}
// ==================== 工具函数 ====================
/**
* 判断是否为开发环境
*/
const isDev = import.meta.env.DEV
/**
* 获取 Token
*/
const getToken = (): string | null => {
return localStorage.getItem('token') || sessionStorage.getItem('token')
}
/**
* 设置 Token
*/
const setToken = (token: string, remember?: boolean): void => {
if (remember) {
localStorage.setItem('token', token)
} else {
sessionStorage.setItem('token', token)
}
}
/**
* 移除 Token
*/
const removeToken = (): void => {
localStorage.removeItem('token')
sessionStorage.removeItem('token')
}
/**
* 检查是否是取消请求错误
*/
const isCancelRequest = (error: any): boolean => {
return axios.isCancel(error)
}
/**
* 检查是否是超时错误
*/
const isTimeoutError = (error: AxiosError): boolean => {
return error.code === 'ECONNABORTED' || error.message.includes('timeout')
}
/**
* 检查是否是网络错误
*/
const isNetworkError = (error: AxiosError): boolean => {
return !error.response && !error.request
}
/**
* 延迟函数
*/
const delay = (ms: number): Promise<void> => {
return new Promise(resolve => setTimeout(resolve, ms))
}
// ==================== Axios 实例 ====================
/** 创建 axios 实例 */
const createAxiosInstance = (): AxiosInstance => {
const instance = axios.create({
baseURL: DEFAULT_CONFIG.baseURL,
timeout: DEFAULT_CONFIG.timeout,
headers: DEFAULT_CONFIG.headers,
// 允许跨域携带凭证
withCredentials: false,
})
// 配置请求拦截器
instance.interceptors.request.use(
(config: InternalAxiosRequestConfig & RequestConfig) => {
// 获取请求配置
const requestConfig = config as RequestConfig
// 如果不是忽略 Token 的请求,添加 Token
if (!requestConfig.ignoreToken) {
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
}
// 设置 Loading(如果需要)
if (requestConfig.loading) {
// 可以在这里触发全局 Loading
// 例如:store.dispatch('setLoading', true)
}
// 处理请求参数
if (config.method === 'get' && config.params) {
// GET 请求参数序列化优化
config.paramsSerializer = {
serialize: (params: any) => {
return Object.keys(params)
.map(key => {
const value = params[key]
if (Array.isArray(value)) {
return value.map((v: any) => `${key}=${encodeURIComponent(v)}`).join('&')
}
return `${key}=${encodeURIComponent(value)}`
})
.join('&')
},
}
}
// 开发环境打印请求信息
if (isDev) {
console.group(`🚀 ${config.method?.toUpperCase()} ${config.url}`)
console.log('请求参数:', config.params || config.data)
console.log('请求头:', config.headers)
console.groupEnd()
}
return config
},
(error: AxiosError) => {
console.error('请求配置错误:', error)
return Promise.reject(error)
}
)
// 配置响应拦截器
instance.interceptors.response.use(
async (response: AxiosResponse) => {
const config = response.config as RequestConfig
// 关闭 Loading
if (config.loading) {
// store.dispatch('setLoading', false)
}
const { data, status } = response
// 处理业务逻辑错误(根据后端返回的 code 判断)
if (data && typeof data.code === 'number') {
// 请求成功
if (data.code === 200 || data.code === 0 || data.success) {
return response
}
// Token 过期或无效
if (data.code === 401 || data.message?.includes('登录')) {
// 清除 Token
removeToken()
// 提示用户登录过期
await ElMessageBox.alert('登录已过期,请重新登录', '提示', {
confirmButtonText: '去登录',
type: 'warning',
}).catch(() => {})
// 跳转到登录页
// router.push('/login')
return Promise.reject(new Error(data.message || '登录已过期'))
}
// 其他业务错误
if (config.showError !== false) {
ElMessage.error(data.message || DEFAULT_CONFIG.errorMessage)
}
return Promise.reject(new Error(data.message || '请求失败'))
}
// HTTP 状态码处理
if (status >= 200 && status < 300) {
return response
}
// HTTP 错误处理
const errorMsg = getHttpErrorMessage(status)
if (config.showError !== false) {
ElMessage.error(errorMsg)
}
return Promise.reject(new Error(errorMsg))
},
async (error: AxiosError) => {
const config = error.config as RequestConfig & { _retryCount?: number }
// 关闭 Loading
if (config?.loading) {
// store.dispatch('setLoading', false)
}
// 如果是取消请求,不处理
if (isCancelRequest(error)) {
return Promise.reject(error)
}
// 请求重试逻辑
if (config && isNetworkError(error) || isTimeoutError(error)) {
const retryCount = config._retryCount || 0
const maxRetries = config.retry ?? DEFAULT_CONFIG.retry
if (retryCount < maxRetries) {
config._retryCount = retryCount + 1
const retryDelay = config.retryDelay ?? DEFAULT_CONFIG.retryDelay
if (isDev) {
console.warn(`请求失败,${retryDelay}ms 后进行第 ${retryCount + 1} 次重试...`)
}
await delay(retryDelay)
return instance(config)
}
}
// 处理 HTTP 错误
const errorMessage = handleError(error, config)
// 自定义错误处理
if (config?.errorHandler) {
config.errorHandler(error)
} else if (config?.showError !== false) {
ElMessage.error(errorMessage)
}
return Promise.reject(error)
}
)
return instance
}
/**
* 获取 HTTP 错误消息
*/
const getHttpErrorMessage = (status: number): string => {
const errorMap: Record<number, string> = {
400: '请求参数错误',
401: '登录已过期,请重新登录',
403: '没有权限访问该资源',
404: '请求的资源不存在',
405: '请求方法不被允许',
408: '请求超时',
409: '请求冲突',
422: '请求参数验证失败',
429: '请求过于频繁,请稍后再试',
500: '服务器内部错误',
501: '服务未实现',
502: '网关错误',
503: '服务不可用',
504: '网关超时',
}
return errorMap[status] || DEFAULT_CONFIG.errorMessage
}
/**
* 处理错误
*/
const handleError = (error: AxiosError, config?: RequestConfig): string => {
// 有响应结果
if (error.response) {
return getHttpErrorMessage(error.response.status)
}
// 无响应结果(网络错误)
if (error.request) {
if (isTimeoutError(error)) {
return '请求超时,请检查网络连接'
}
return '网络连接失败,请检查网络设置'
}
// 其他错误
return error.message || DEFAULT_CONFIG.errorMessage
}
// ==================== 请求封装 ====================
/** 创建 axios 实例 */
const request = createAxiosInstance()
/**
* 封装 GET 请求
*/
export const get = <T = any>(
url: string,
params?: any,
config?: RequestConfig
): Promise<ResponseData<T>> => {
return request.get(url, { ...config, params })
}
/**
* 封装 POST 请求
*/
export const post = <T = any>(
url: string,
data?: any,
config?: RequestConfig
): Promise<ResponseData<T>> => {
return request.post(url, data, config)
}
/**
* 封装 PUT 请求
*/
export const put = <T = any>(
url: string,
data?: any,
config?: RequestConfig
): Promise<ResponseData<T>> => {
return request.put(url, data, config)
}
/**
* 封装 DELETE 请求
*/
export const del = <T = any>(
url: string,
params?: any,
config?: RequestConfig
): Promise<ResponseData<T>> => {
return request.delete(url, { ...config, params })
}
/**
* 封装 PATCH 请求
*/
export const patch = <T = any>(
url: string,
data?: any,
config?: RequestConfig
): Promise<ResponseData<T>> => {
return request.patch(url, data, config)
}
/**
* 封装上传文件请求
*/
export const upload = <T = any>(
url: string,
formData: FormData,
config?: RequestConfig
): Promise<ResponseData<T>> => {
return request.post(url, formData, {
...config,
headers: {
...config?.headers,
'Content-Type': 'multipart/form-data',
},
})
}
/**
* 封装下载文件请求
*/
export const download = (
url: string,
params?: any,
config?: RequestConfig
): Promise<Blob> => {
return request.get(url, {
...config,
params,
responseType: 'blob',
}) as Promise<any>
}
// ==================== 请求管理 ====================
/** 存储所有正在进行的请求 */
const pendingRequests = new Map<string, AbortController>()
/**
* 生成请求唯一标识
*/
const generateRequestKey = (config: RequestConfig): string => {
const { url, method, params, data } = config
return `${method}-${url}-${JSON.stringify(params)}-${JSON.stringify(data)}`
}
/**
* 添加请求到 pending
*/
export const addPendingRequest = (config: RequestConfig): void => {
const requestKey = generateRequestKey(config)
// 取消之前的同名请求
if (pendingRequests.has(requestKey)) {
const controller = pendingRequests.get(requestKey)
controller?.abort()
pendingRequests.delete(requestKey)
}
// 创建新的 AbortController
const controller = new AbortController()
pendingRequests.set(requestKey, controller)
// 将 signal 绑定到 config
config.signal = controller.signal
}
/**
* 移除请求从 pending
*/
export const removePendingRequest = (config: RequestConfig): void => {
const requestKey = generateRequestKey(config)
pendingRequests.delete(requestKey)
}
/**
* 取消所有 pending 请求
*/
export const cancelAllRequests = (): void => {
pendingRequests.forEach(controller => {
controller.abort()
})
pendingRequests.clear()
}
/**
* 创建一个可以取消的请求
*/
export const createCancellableRequest = <T = any>(
url: string,
config?: RequestConfig
): { request: Promise<ResponseData<T>>; cancel: () => void } => {
const controller = new AbortController()
const cancel = () => controller.abort()
const requestConfig: RequestConfig = {
...config,
signal: controller.signal,
}
const method = (config?.method as RequestMethod) || 'get'
let requestPromise: Promise<ResponseData<T>>
switch (method) {
case 'post':
requestPromise = post(url, config?.data, requestConfig)
break
case 'put':
requestPromise = put(url, config?.data, requestConfig)
break
case 'delete':
requestPromise = del(url, config?.params, requestConfig)
break
case 'patch':
requestPromise = patch(url, config?.data, requestConfig)
break
default:
requestPromise = get(url, config?.params, requestConfig)
}
return { request: requestPromise, cancel }
}
// ==================== 导出 ====================
/** axios 实例(可直接使用) */
export { request as axiosInstance }
/** axios 原生方法 */
export { axios }
export default request#使用示例
api/index.ts
// 基础 GET 请求
import { get, post } from '@/utils/request'
// 简单请求
const fetchUserInfo = async () => {
const res = await get('/user/info')
return res.data
}
// 带参数请求
const searchUsers = async (keyword: string) => {
const res = await get('/user/list', { keyword, page: 1, size: 10 })
return res.data
}
// POST 请求
const login = async (username: string, password: string) => {
const res = await post('/auth/login', { username, password })
return res.data
}
// 显示 Loading 的请求
const fetchDataWithLoading = async () => {
const res = await get('/data', null, { loading: true })
return res.data
}
// 不显示错误提示
const fetchDataNoError = async () => {
const res = await get('/data', null, { showError: false })
return res.data
}
// 自定义错误处理
const fetchDataCustomError = async () => {
const res = await get('/data', null, {
errorHandler: (error) => {
console.error('自定义错误处理:', error)
}
})
return res.data
}
// 文件上传
import { upload } from '@/utils/request'
const uploadFile = async (file: File) => {
const formData = new FormData()
formData.append('file', file)
const res = await upload('/upload', formData)
return res.data
}
// 文件下载
import { download } from '@/utils/request'
const downloadFile = async (fileId: string) => {
const res = await download('/file/download', { fileId })
// 处理下载
const blob = new Blob([res])
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'file.pdf'
link.click()
window.URL.revokeObjectURL(url)
}
// 取消请求
import { createCancellableRequest } from '@/utils/request'
// 在组件中
const { request, cancel } = createCancellableRequest('/api/data')
request.then(res => console.log(res))
// 在需要取消时调用
cancel()
