在实际项目中,直接使用 axios.get('/api/users') 虽然能跑通,但很快就会遇到三个问题:每个请求都要带 token,处理失败时要写一堆重复的错误提示,接口基准地址一换就要改几十个文件。Axios 提供了拦截器和默认配置机制,让我们可以在一处集中处理这些通用逻辑,真正做到“一次封装,全局受益”。
创建实例与全局配置
首先,不再直接引入 axios,而是创建一个独立实例,为它设置项目专属的默认配置。
// src/utils/request.js
import axios from 'axios'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量读取,如 '/api'
timeout: 10000, // 超时时间 10s
headers: {
'Content-Type': 'application/json' // 默认请求头
}
})
export default request
这样做的好处很明显:整个项目都通过 request 发送请求,以后改接口地址、调整超时只需要修改一处。环境变量让开发/测试/生产的环境切换零改代码。
请求拦截器:发请求前自动处理
请求拦截器在所有请求发出前被调用,适合处理全局性的前置逻辑,比如自动携带用户 token。
// 注入请求拦截器
request.interceptors.request.use(
(config) => {
// 从状态管理或本地存储中获取 token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
典型应用场景:
- 自动追加 token、租户 ID、语言等统一参数
- 根据请求方法统一处理 Content-Type(如文件上传改为
multipart/form-data) - 发送请求时显示全局 loading 动画
- 打印请求日志(开发调试用)
注意:拦截器中的 config 必须返回,否则请求会被挂起。
响应拦截器:统一处理返回数据与错误
响应拦截器在请求得到响应后、业务代码拿到数据前执行,适合做数据剥离、错误集中处理。
request.interceptors.response.use(
(response) => {
const res = response.data
// 根据后端约定的格式判断业务是否成功
if (res.code !== 200) {
// 业务错误
ElMessage.error(res.message || '请求失败')
return Promise.reject(new Error(res.message || 'Error'))
}
// 剥离出实际数据,业务代码直接拿到 data
return res.data
},
(error) => {
// 处理 HTTP 层面的错误(如 401、404、500)
const message = error.response?.data?.message || error.message || '网络异常'
ElMessage.error(message)
return Promise.reject(error)
}
)
响应拦截器的常见分层处理:
- 剥离 response.data.data
后端通常返回 { code: 200, data: [...], message: 'ok' },在拦截器里直接返回 res.data,业务代码就不用每次都点一层 data。
- 业务错误统一弹窗
当 code 不为成功码时,直接弹出错误消息,业务代码只需要正常处理成功结果即可,不用重复写 if (res.code !== 200)。
- HTTP 状态码处理
401(未登录)可以跳转到登录页并清空本地 token;403(无权限)可以提示“您没有操作权限”;500 类错误提示服务器异常。
- 日志上报
在拦截器中捕获异常并上报到监控平台,便于排查线上问题。
错误统一处理与分离
上面的拦截器虽然处理了错误,但有时业务本身也需要对某些请求单独处理异常(如表单校验失败不需要全局弹窗)。此时可以在具体请求的 catch 中阻止全局错误:
// 某个 API 的调用
import request from '@/utils/request'
export function getUserList(params) {
return request.get('/users', { params }).catch((err) => {
// 如果业务代码已经 catch,则不再向上抛出,避免全局拦截器再次提示
if (err.__handled) return Promise.reject(err)
err.__handled = true
throw err
})
}
更优雅的做法是,在全局拦截器里判断错误类型或用一个标志位,让业务代码能“吞掉”某个错误。
为什么这样封装?
把请求/响应拦截器和全局配置集中在一个文件中,带来的好处是:
- 代码简洁:业务代码消除大量重复的 token 拼接、错误提示、状态判断。
- 行为一致:全站的接口异常处理、loading 状态都按统一逻辑运行,用户体验统一。
- 易于维护:后端改了响应格式?换个拦截器即可,无需改动几十个接口文件。
- 安全性:token、签名等敏感逻辑集中控制,避免因业务代码疏漏导致安全隐患。
一个真实的教训:某项目初期没有统一封装,登录过期后每个页面都需要单独处理 401,结果有的页面直接报错白屏,有的弹了个无法操作的过期提示,用户投诉不断。后来引入上述封装,只改一行拦截器代码,全站问题消失。
这套封装是 Vue 项目中数据请求的基石,后续的“请求取消”、“失败重试”、“接口防抖”等都是在这个基础上进一步叠加的能力。