在前端项目中,Axios 几乎是 HTTP 请求的标配。但随着项目规模扩大,简单的 axios.get 已经无法满足需求。本文将从零开始,手把手带你封装一套企业级 Axios 配置,涵盖:实例化、请求/响应拦截、自动重试、取消重复请求、全局 Loading、错误处理细化、开发环境日志等,最后提供可直接使用的完整代码。
1. 创建 Axios 实例与环境变量
企业项目通常有多个环境(开发、测试、生产),使用环境变量管理接口基址是最佳实践。
typescript
// request.ts
import axios from 'axios'
const instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量读取
timeout: 10000, // 请求超时时间
})
建议将
VITE_API_BASE_URL配置在.env文件中,方便不同环境切换。
2. 请求拦截器:认证、Loading、取消重复请求
请求拦截器在请求发出前执行,我们在这里完成三件事:
- 携带 Token:从 Store 中取出用户令牌,添加到请求头。
- 全局 Loading:通过计数器控制全屏 Loading 的显示/隐藏。
- 取消重复请求:如果短时间内发起相同请求(参数一致),取消之前的请求。
2.1 取消重复请求的实现
利用 axios.CancelToken 和 Map 存储每个请求的取消函数。请求的"唯一标识"由 method、url、params、data 共同生成。
typescript
const pendingMap = new Map<string, AbortController>()
function getRequestKey(config: any): string {
const { method, url, params, data } = config
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}
// 请求拦截器
instance.interceptors.request.use((config) => {
// 1. 显示 Loading
showLoading()
// 2. 生成请求键,取消相同请求
const key = getRequestKey(config)
if (pendingMap.has(key)) {
const controller = pendingMap.get(key)
controller?.abort() // 取消之前的请求
pendingMap.delete(key)
}
// 3. 使用 AbortController 代替 CancelToken(现代浏览器)
const controller = new AbortController()
config.signal = controller.signal
pendingMap.set(key, controller)
// 4. 添加 Token
const token = useUserStore().token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
}, (error) => {
return Promise.reject(error)
})
说明 :现代浏览器推荐使用
AbortController替代已废弃的CancelToken。这里为了兼容性,也可以继续使用CancelToken,但AbortController是未来趋势。
2.2 全局 Loading 管理
Loading 需要支持多个并发请求,只有所有请求都完成时才关闭。我们使用计数器实现。
typescript
let loadingInstance: any
let requestCount = 0
function showLoading() {
if (requestCount === 0) {
loadingInstance = ElLoading.service({
fullscreen: true,
text: '加载中...',
background: 'rgba(0,0,0,0.7)',
})
}
requestCount++
}
function hideLoading() {
requestCount--
if (requestCount === 0 && loadingInstance) {
loadingInstance.close()
loadingInstance = null
}
}
3. 响应拦截器:业务状态码、HTTP 错误、401 跳转
响应拦截器负责:
- 关闭 Loading
- 从 pendingMap 中移除已完成请求
- 处理业务状态码(如
code !== 0视为业务失败) - 处理 HTTP 状态码(4xx、5xx)
- 401 未授权时跳转登录页
3.1 成功响应处理
typescript
instance.interceptors.response.use(
(response) => {
hideLoading()
const key = getRequestKey(response.config)
pendingMap.delete(key)
// 假设后端统一返回格式:{ code: number, message: string, data: any }
const { code, message, data } = response.data
if (code === 0) {
// 业务成功,直接返回 data(调用方无需再取 .data.data)
return data
}
// 业务失败,统一提示
ElMessage.error(message || '操作失败')
return Promise.reject(new Error(message || '业务错误'))
},
(error) => {
hideLoading()
if (error.config) {
const key = getRequestKey(error.config)
pendingMap.delete(key)
}
// 处理取消请求(不弹错误)
if (axios.isCancel(error)) {
console.log('请求取消:', error.message)
return Promise.reject(error)
}
// HTTP 错误细化处理
if (error.response) {
const status = error.response.status
const msg = error.response.data?.message || error.message
switch (status) {
case 401:
// 未授权,跳转登录
router.push('/login')
ElMessage.error('登录已过期,请重新登录')
break
case 403:
ElMessage.error('没有权限访问')
break
case 404:
ElMessage.error('请求资源不存在')
break
case 500:
case 502:
case 503:
case 504:
ElMessage.error('服务器异常,请稍后重试')
break
default:
ElMessage.error(msg || '请求失败')
}
} else if (error.code === 'ECONNABORTED') {
ElMessage.error('请求超时,请检查网络')
} else if (error.message.includes('Network Error')) {
ElMessage.error('网络异常,请检查网络连接')
} else {
ElMessage.error(error.message || '未知错误')
}
return Promise.reject(error)
}
)
4. 自动重试机制
网络抖动或临时故障时,自动重试能极大提升用户体验。使用 axios-retry 插件。
bash
npm install axios-retry
配置示例:
typescript
import axiosRetry from 'axios-retry'
axiosRetry(instance, {
retries: 3, // 最大重试次数
retryDelay: (retryCount) => {
console.log(`第 ${retryCount} 次重试...`)
if (retryCount === 1) {
ElMessage.warning('网络不稳定,正在尝试重连')
}
return retryCount * 1000 // 递增延迟
},
retryCondition: (error) => {
// 只有网络错误或 5xx 错误才重试
return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
(error.response?.status ?? 0) >= 500
}
})
5. 开发环境日志
在开发环境下打印请求和响应信息,便于调试。
typescript
if (import.meta.env.DEV) {
instance.interceptors.request.use((config) => {
console.log(`[Request] ${config.method?.toUpperCase()} ${config.url}`, config)
return config
})
instance.interceptors.response.use(
(response) => {
console.log(`[Response] ${response.config.method?.toUpperCase()} ${response.config.url}`, response)
return response
},
(error) => {
console.error('[Response Error]', error)
return Promise.reject(error)
}
)
}
6. 文件上传进度支持
对于大文件上传,通常需要显示上传进度。Axios 原生支持 onUploadProgress 回调。
typescript
function uploadFile(file: File, onProgress?: (percent: number) => void) {
const formData = new FormData()
formData.append('file', file)
return instance.post('/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total)
onProgress?.(percent)
}
},
// 如果需要取消上传,可以传入 signal 或 cancelToken
})
}
7. 完整配置示例
下面是一个集成了上述所有功能的完整 request.ts 文件,可以直接复制到项目中使用(记得安装依赖:axios、axios-retry、element-plus 并正确导入路由和 store)。
typescript
// request.ts
import axios from 'axios'
import axiosRetry from 'axios-retry'
import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import router from '@/router'
import { useUserStore } from '@/stores/user'
import { ElMessage, ElLoading } from 'element-plus'
// ---------- 类型扩展 ----------
declare module 'axios' {
export interface InternalAxiosRequestConfig {
_retry?: boolean // 用于重试标记
}
}
// ---------- Loading 管理器 ----------
let loadingInstance: ReturnType<typeof ElLoading.service> | null = null
let requestCount = 0
function showLoading() {
if (requestCount === 0) {
loadingInstance = ElLoading.service({
fullscreen: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.7)'
})
}
requestCount++
}
function hideLoading() {
requestCount--
if (requestCount === 0 && loadingInstance) {
loadingInstance.close()
loadingInstance = null
}
}
// ---------- 取消重复请求 ----------
const pendingMap = new Map<string, AbortController>()
function getRequestKey(config: InternalAxiosRequestConfig): string {
const { method, url, params, data } = config
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}
// ---------- 创建实例 ----------
const instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
// ---------- 自动重试 ----------
axiosRetry(instance, {
retries: 3,
retryDelay: (retryCount) => {
console.log(`网络错误,正在重试... 第${retryCount}次`)
if (retryCount === 1) {
ElMessage.warning('网络不稳定,正在尝试重连')
}
return retryCount * 1000
},
retryCondition: (error) => {
return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
(error.response?.status ?? 0) >= 500
}
})
// ---------- 请求拦截器 ----------
instance.interceptors.request.use(
(config) => {
// 1. 显示 Loading
showLoading()
// 2. 取消重复请求
const key = getRequestKey(config)
if (pendingMap.has(key)) {
const controller = pendingMap.get(key)
controller?.abort()
pendingMap.delete(key)
}
const controller = new AbortController()
config.signal = controller.signal
pendingMap.set(key, controller)
// 3. 添加 Token
const token = useUserStore().token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// ---------- 响应拦截器 ----------
instance.interceptors.response.use(
(response: AxiosResponse) => {
hideLoading()
const key = getRequestKey(response.config)
pendingMap.delete(key)
// 假设后端返回格式:{ code: number, message: string, data: any }
const { code, message, data } = response.data
if (code === 0) {
// 业务成功,直接返回 data
return data
}
// 业务失败
ElMessage.error(message || '操作失败')
return Promise.reject(new Error(message || '业务错误'))
},
(error) => {
hideLoading()
if (error.config) {
const key = getRequestKey(error.config)
pendingMap.delete(key)
}
// 取消请求不报错
if (axios.isCancel(error)) {
console.log('请求取消:', error.message)
return Promise.reject(error)
}
// HTTP 错误细化
if (error.response) {
const status = error.response.status
const msg = error.response.data?.message || error.message
switch (status) {
case 401:
router.push('/login')
ElMessage.error('登录已过期,请重新登录')
break
case 403:
ElMessage.error('没有权限访问')
break
case 404:
ElMessage.error('请求资源不存在')
break
case 500:
case 502:
case 503:
case 504:
ElMessage.error('服务器异常,请稍后重试')
break
default:
ElMessage.error(msg || '请求失败')
}
} else if (error.code === 'ECONNABORTED') {
ElMessage.error('请求超时,请检查网络')
} else if (error.message.includes('Network Error')) {
ElMessage.error('网络异常,请检查网络连接')
} else {
ElMessage.error(error.message || '未知错误')
}
return Promise.reject(error)
}
)
// ---------- 开发环境日志 ----------
if (import.meta.env.DEV) {
instance.interceptors.request.use((config) => {
console.log(`[Request] ${config.method?.toUpperCase()} ${config.url}`, config)
return config
})
instance.interceptors.response.use(
(response) => {
console.log(`[Response] ${response.config.method?.toUpperCase()} ${response.config.url}`, response)
return response
},
(error) => {
console.error('[Response Error]', error)
return Promise.reject(error)
}
)
}
export default instance
8. 使用示例
typescript
import request from './request'
// GET 请求
const getUserInfo = (id: number) => {
return request.get('/user/info', { params: { id } })
}
// POST 请求
const login = (data: { username: string; password: string }) => {
return request.post('/auth/login', data)
}
// 文件上传(带进度)
const uploadAvatar = (file: File, onProgress: (p: number) => void) => {
const formData = new FormData()
formData.append('file', file)
return request.post('/upload/avatar', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (e) => {
if (e.total) {
const percent = Math.round((e.loaded * 100) / e.total)
onProgress(percent)
}
}
})
}
总结
本文从零开始构建了一套企业级 Axios 配置,涵盖了:
- 环境变量与实例化
- 请求拦截器(Token、Loading、取消重复请求)
- 响应拦截器(业务码处理、HTTP 错误、401 跳转)
- 自动重试机制
- 开发环境日志
- 文件上传进度支持
这套配置能够满足绝大多数中大型项目的需求,具有良好的健壮性和可维护性。你可以根据项目具体情况调整细节,如业务状态码字段、Loading 样式等。希望本文能帮助你写出更专业的 Axios 封装!