在 Vue3 项目开发中,Axios 是最常用的 HTTP 请求库,但直接在组件中裸写 Axios 会导致代码冗余、难以维护------比如每个请求都要写重复的 baseURL、请求头、错误处理,接口变更时要改遍所有组件。
而合理封装 Axios 能解决这些问题:统一管理请求配置、全局处理拦截器、标准化错误提示、支持取消重复请求......既能提升开发效率,又能让代码更健壮。
今天这篇文章,就带你从零实现 Vue3 + Vite 项目中 Axios 的生产级封装,从基础结构到进阶优化,每一步都有完整代码示例,直接复制就能用!适配 Vue3 组合式 API(
一、前置准备:安装 Axios
首先确保你的 Vue3 项目已搭建完成(推荐用 Vite 搭建),然后安装 Axios,TS 项目需额外安装类型声明:
js
# 安装核心 Axios 库
npm install axios
# 可选:TS 项目必装(提供类型提示,避免报错)
npm install @types/axios --save-dev
二、基础版封装:核心结构(新手友好)
基础版封装聚焦「统一配置 + 简化调用」,适合小型项目或新手入门,核心实现 3 个功能:统一 baseURL、全局请求/响应拦截、简化请求调用。
封装步骤:在 src 目录下新建 utils/request.js(JS 项目)或 utils/request.ts(TS 项目),作为 Axios 封装的核心文件。
2.1 JS 版本(基础版)
js
// src/utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus' // 可选:结合UI库做错误提示(推荐)
// 1. 创建 Axios 实例,配置基础参数
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量(推荐,区分开发/生产)
timeout: 5000, // 超时时间(单位:ms),超过则中断请求
headers: {
'Content-Type': 'application/json;charset=utf-8' // 默认请求头
}
})
// 2. 请求拦截器(请求发送前执行)
// 作用:添加token、统一修改请求参数格式等
service.interceptors.request.use(
(config) => {
// 示例:添加token(登录后存储在localStorage,根据实际项目调整)
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}` // 拼接token格式(后端约定)
}
return config // 必须返回config,否则请求会中断
},
(error) => {
// 请求发送失败(如网络中断、参数错误)
ElMessage.error('请求发送失败,请检查网络或参数')
return Promise.reject(error) // 抛出错误,供组件捕获处理
}
)
// 3. 响应拦截器(请求返回后执行,先于组件接收)
// 作用:统一处理响应数据、拦截错误(如token过期、接口报错)
service.interceptors.response.use(
(response) => {
// 只返回响应体中的data(多数后端接口会包裹一层code/message/data)
const res = response.data
// 示例:根据后端约定的code判断请求是否成功(常见约定:200=成功)
if (res.code !== 200) {
// 非200状态码,视为业务错误(如参数错误、权限不足)
ElMessage.error(res.message || '接口请求失败,请重试')
return Promise.reject(new Error(res.message || '请求失败'))
}
return res.data // 返回真正的业务数据,组件可直接使用
},
(error) => {
// 响应失败(如超时、后端报错、404/500状态码)
let errorMsg = '请求异常,请联系管理员'
// 区分不同错误类型,给出更精准提示
if (error.response) {
// 有响应,但状态码非2xx(如401token过期、404接口不存在、500后端报错)
switch (error.response.status) {
case 401:
errorMsg = '登录已过期,请重新登录'
// 额外操作:清除过期token,跳转到登录页(结合Vue Router)
localStorage.removeItem('token')
window.location.href = '/login'
break
case 404:
errorMsg = '请求的接口不存在'
break
case 500:
errorMsg = '后端服务异常,请稍后重试'
break
default:
errorMsg = error.response.data?.message || errorMsg
}
} else if (error.request) {
// 无响应(如网络中断、超时)
errorMsg = '网络异常或请求超时,请检查网络'
}
ElMessage.error(errorMsg)
return Promise.reject(error)
}
)
// 4. 封装常用请求方法(get/post/put/delete),简化组件调用
// get请求:params传参(拼接在URL后)
export const get = (url, params = {}) => {
return service({
url,
method: 'get',
params
})
}
// post请求:data传参(请求体中)
export const post = (url, data = {}) => {
return service({
url,
method: 'post',
data
})
}
// put请求(修改数据)
export const put = (url, data = {}) => {
return service({
url,
method: 'put',
data
})
}
// delete请求(删除数据)
export const del = (url, params = {}) => {
return service({
url,
method: 'delete',
params
})
}
// 导出Axios实例(特殊场景可直接使用,如取消请求)
export default service
2.2 TS 版本(基础版,补充类型提示)
TS 项目需添加类型声明,避免类型报错,提升开发体验,核心修改的是「请求/响应类型」和「参数类型」:
js
// src/utils/request.ts
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import { ElMessage } from 'element-plus'
// 定义后端响应的统一格式(根据你的后端接口调整)
interface ResponseData<T = any> {
code: number
message: string
data: T
}
// 1. 创建Axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 5000,
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
})
// 2. 请求拦截器
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token = localStorage.getItem('token')
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error: AxiosError) => {
ElMessage.error('请求发送失败,请检查网络或参数')
return Promise.reject(error)
}
)
// 3. 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse<ResponseData>) => {
const res = response.data
if (res.code !== 200) {
ElMessage.error(res.message || '接口请求失败,请重试')
return Promise.reject(new Error(res.message || '请求失败'))
}
return res.data // 返回业务数据,自动推导类型
},
(error: AxiosError<ResponseData>) => {
let errorMsg = '请求异常,请联系管理员'
if (error.response) {
const status = error.response.status
switch (status) {
case 401:
errorMsg = '登录已过期,请重新登录'
localStorage.removeItem('token')
window.location.href = '/login'
break
case 404:
errorMsg = '请求的接口不存在'
break
case 500:
errorMsg = '后端服务异常,请稍后重试'
break
default:
errorMsg = error.response.data?.message || errorMsg
}
} else if (error.request) {
errorMsg = '网络异常或请求超时,请检查网络'
}
ElMessage.error(errorMsg)
return Promise.reject(error)
}
)
// 4. 封装请求方法,添加类型声明
// get请求
export const get = <T = any>(url: string, params?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> => {
return service({
url,
method: 'get',
params,
...config
})
}
// post请求
export const post = <T = any>(url: string, data?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> => {
return service({
url,
method: 'post',
data,
...config
})
}
// put请求
export const put = <T = any>(url: string, data?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> => {
return service({
url,
method: 'put',
data,
...config
})
}
// delete请求
export const del = <T = any>(url: string, params?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> => {
return service({
url,
method: 'delete',
params,
...config
})
}
export default service
2.3 环境变量配置(关键步骤)
上面封装中用到的import.meta.env.VITE_API_BASE_URL,是 Vite 的环境变量,用于区分「开发环境」和「生产环境」的接口地址,避免手动修改。
在项目根目录新建 2 个文件:.env.development(开发环境)和 .env.production(生产环境):
js
# .env.development(开发环境,npm run dev 时生效)
VITE_API_BASE_URL = 'http://localhost:3000/api' # 本地后端接口地址
# .env.production(生产环境,npm run build 时生效)
VITE_API_BASE_URL = 'https://api.yourdomain.com' # 线上后端接口地址
注意:Vite 环境变量必须以VITE_ 开头,否则无法读取。
2.4 组件中如何使用(简化调用)
封装完成后,在 Vue3 组件(支持
js
<script setup>
// 导入封装好的请求方法
import { get, post } from '@/utils/request'
import { ref, onMounted } from 'vue'
const userList = ref([])
// 1. get请求(获取用户列表,params传参)
const getUserList = async () => {
try {
// 直接调用,无需写baseURL、请求头
const res = await get('/user/list', { page: 1, size: 10 })
userList.value = res // 直接使用响应数据(已过滤外层code/message)
} catch (error) {
// 可选:组件内单独处理错误(全局已处理过,这里可省略)
console.log('获取用户列表失败:', error)
}
}
// 2. post请求(提交表单,data传参)
const submitForm = async (formData) => {
try {
const res = await post('/user/add', formData)
ElMessage.success('提交成功')
} catch (error) {
// 无需额外提示,全局响应拦截器已做错误提示
}
}
// 页面挂载时调用get请求
onMounted(() => {
getUserList()
})
</script>
对比裸写 Axios,封装后的调用更简洁,且所有请求的配置、错误处理都统一管理,后续修改接口地址、token 格式,只需改 request.js/ts 一个文件。
三、进阶版封装:生产级优化(必看)
基础版封装能满足小型项目,但在中大型项目中,还需要补充「取消重复请求、请求loading、接口加密、异常重试」等功能,让封装更健壮、更贴合生产需求。
3.1 优化1:取消重复请求(避免接口冗余)
场景:用户快速点击两次按钮,会发起两次相同的请求(如提交表单),导致后端重复处理。解决方案:用 Axios 的 CancelToken(Axios 0.x)或 AbortController(Axios 1.x+)取消重复请求。
以下是 Axios 1.x+ 版本(当前最新版)的实现方式(AbortController 更规范):
js
// src/utils/request.js(仅修改新增部分,其余代码不变)
import axios from 'axios'
import { ElMessage } from 'element-plus'
// 存储正在请求的接口(key:请求标识,value:AbortController实例)
const pendingRequests = new Map()
// 生成请求标识(url + method + 参数,确保唯一)
const generateRequestKey = (config) => {
const { url, method, params, data } = config
// 序列化参数,避免相同请求因参数顺序不同被误判为不同请求
const paramsStr = JSON.stringify(params || {})
const dataStr = JSON.stringify(data || {})
return `${url}-${method}-${paramsStr}-${dataStr}`
}
// 取消重复请求
const cancelPendingRequest = (config) => {
const requestKey = generateRequestKey(config)
// 如果有重复请求,取消之前的
if (pendingRequests.has(requestKey)) {
const controller = pendingRequests.get(requestKey)
controller.abort() // 取消请求
pendingRequests.delete(requestKey) // 移除取消的请求
}
}
// 1. 创建Axios实例(新增signal配置)
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 5000,
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
})
// 2. 请求拦截器(修改:添加取消重复请求逻辑)
service.interceptors.request.use(
(config) => {
// 取消重复请求(发起当前请求前,取消之前相同的请求)
cancelPendingRequest(config)
// 创建AbortController实例,用于取消请求
const controller = new AbortController()
config.signal = controller.signal
// 存储当前请求
const requestKey = generateRequestKey(config)
pendingRequests.set(requestKey, controller)
// 添加token(原有逻辑不变)
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
ElMessage.error('请求发送失败,请检查网络或参数')
return Promise.reject(error)
}
)
// 3. 响应拦截器(修改:移除已完成的请求)
service.interceptors.response.use(
(response) => {
const config = response.config
const requestKey = generateRequestKey(config)
pendingRequests.delete(requestKey) // 请求完成,移除存储
const res = response.data
if (res.code !== 200) {
ElMessage.error(res.message || '接口请求失败,请重试')
return Promise.reject(new Error(res.message || '请求失败'))
}
return res.data
},
(error) => {
// 处理取消请求的错误(单独捕获,不提示用户)
if (axios.isCancel(error)) {
console.log('请求已取消:', error.message)
return Promise.reject(new Error('请求已取消'))
}
// 移除失败的请求
if (error.config) {
const requestKey = generateRequestKey(error.config)
pendingRequests.delete(requestKey)
}
// 原有错误处理逻辑不变
let errorMsg = '请求异常,请联系管理员'
if (error.response) {
// ... 原有状态码判断逻辑
} else if (error.request) {
errorMsg = '网络异常或请求超时,请检查网络'
}
ElMessage.error(errorMsg)
return Promise.reject(error)
}
)
// 4. 封装请求方法(不变)
export const get = (url, params = {}) => { /* ... */ }
export const post = (url, data = {}) => { /* ... */ }
// ... 其余方法
3.2 优化2:全局请求 Loading(提升交互体验)
场景:请求耗时较长时,用户不知道是否在加载,容易重复点击。解决方案:添加全局 Loading,所有请求发起时显示 Loading,全部请求完成后隐藏。
结合 Element Plus 的 ElLoading 实现(需安装 Element Plus):
js
// src/utils/request.js(新增Loading相关逻辑)
import axios from 'axios'
import { ElMessage, ElLoading } from 'element-plus'
// 新增:Loading实例和请求计数
let loadingInstance = null // Loading实例
let requestCount = 0 // 请求计数器(避免多个请求重复显示/隐藏Loading)
// 显示Loading
const showLoading = () => {
if (requestCount === 0) {
// 只有当没有请求时,才显示Loading
loadingInstance = ElLoading.service({
lock: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.5)'
})
}
requestCount++
}
// 隐藏Loading
const hideLoading = () => {
requestCount--
if (requestCount === 0) {
// 所有请求完成后,才隐藏Loading
loadingInstance?.close()
}
}
// 1. 创建Axios实例(不变)
const service = axios.create({ /* ... */ })
// 2. 请求拦截器(新增:显示Loading)
service.interceptors.request.use(
(config) => {
showLoading() // 发起请求时显示Loading
// ... 原有取消重复请求、添加token逻辑
return config
},
(error) => {
hideLoading() // 请求失败,隐藏Loading
ElMessage.error('请求发送失败,请检查网络或参数')
return Promise.reject(error)
}
)
// 3. 响应拦截器(新增:隐藏Loading)
service.interceptors.response.use(
(response) => {
hideLoading() // 请求成功,隐藏Loading
// ... 原有移除重复请求、处理响应逻辑
return res.data
},
(error) => {
hideLoading() // 响应失败,隐藏Loading
// ... 原有错误处理逻辑
return Promise.reject(error)
}
)
注意:requestCount 计数器是关键,避免多个请求同时发起时,单个请求完成就隐藏 Loading。
3.3 优化3:接口模块化管理(中大型项目必做)
场景:项目接口较多时,所有请求都写在组件中,会导致代码混乱,后续维护困难。解决方案:将接口按模块拆分,统一管理在 api 文件夹中。
步骤:在 src 目录下新建api 文件夹,按业务模块拆分文件(如 api/user.js、api/goods.js):
js
// src/api/user.js(用户模块接口)
import { get, post, put, del } from '@/utils/request'
// 接口模块化封装,每个接口对应一个函数
export const userApi = {
// 获取用户列表
getUserList: (params) => get('/user/list', params),
// 添加用户
addUser: (data) => post('/user/add', data),
// 修改用户信息
editUser: (id, data) => put(`/user/${id}`, data),
// 删除用户
deleteUser: (id) => del('/user/delete', { id }),
// 用户登录
login: (data) => post('/user/login', data)
}
// src/api/goods.js(商品模块接口)
import { get, post } from '@/utils/request'
export const goodsApi = {
// 获取商品详情
getGoodsDetail: (id) => get(`/goods/${id}`),
// 搜索商品
searchGoods: (params) => get('/goods/search', params)
}
组件中使用时,直接导入对应模块的接口,代码更清晰、更易维护:
js
<script setup>
// 导入用户模块接口
import { userApi } from '@/api/user'
import { ref, onMounted } from 'vue'
const userList = ref([])
const getUserList = async () => {
try {
// 直接调用接口函数,参数清晰
const res = await userApi.getUserList({ page: 1, size: 10 })
userList.value = res
} catch (error) {
console.log(error)
}
}
onMounted(() => {
getUserList()
})
</script>
3.4 其他生产级优化(可选,按需添加)
- 请求重试:针对网络波动导致的请求失败,自动重试 1-2 次(避免用户手动重试),用 axios-retry 插件实现。
- 请求加密:敏感接口(如登录、支付)的参数加密(如 AES 加密),在请求拦截器中处理参数加密。
- 接口日志:开发环境打印请求/响应日志(便于调试),生产环境关闭日志(避免泄露敏感信息)。
- 自定义请求头:支持部分接口单独设置请求头(如文件上传接口设置 Content-Type: multipart/form-data)。
四、避坑指南(新手必看)
- 环境变量读取失败 :Vite 环境变量必须以
VITE_开头,且只能在客户端代码中读取,不能在服务端代码中使用。 - token 失效未跳转 :确保响应拦截器中 401 状态码的判断逻辑正确,且
window.location.href = '/login'没有被注释,同时检查 token 是否正确存储/清除。 - 重复请求取消无效:请求标识(requestKey)必须唯一,确保 params 和 data 被正确序列化(避免因参数顺序不同导致标识不同)。
- Loading 闪烁:请求耗时过短(如 100ms 内完成),会导致 Loading 一闪而过,可添加 Loading 延迟显示(如 300ms 后显示,避免闪烁)。
- TS 类型报错 :确保后端响应格式和定义的
ResponseData接口一致,否则会出现类型不匹配报错。 - 文件上传接口失败 :文件上传接口需单独设置请求头
'Content-Type': 'multipart/form-data',且传参用 FormData 格式。
五、总结
Vue3 封装 Axios 的核心是「统一管理 + 简化调用 + 异常处理」,从基础版的拦截器封装,到进阶版的重复请求取消、Loading 优化、接口模块化,一步步提升封装的健壮性和实用性。
总结几个关键要点:
- 用
axios.create()创建实例,统一配置 baseURL、超时时间等。 - 请求拦截器:添加 token、取消重复请求、显示 Loading。
- 响应拦截器:统一处理响应数据、拦截错误(token 过期、404/500)、隐藏 Loading。
- 中大型项目:接口按模块拆分,提升代码可维护性。
- 生产环境:补充取消重复请求、请求加密等优化,让封装更健壮。
封装完成后,后续开发只需专注于业务逻辑,无需关注请求的底层配置,极大提升开发效率。本文的封装方案适配绝大多数 Vue3 项目,大家可根据自己的后端接口规范和业务需求,灵活调整拦截器逻辑和接口格式。