在使用 Vue 3(或 Vue 2)配合 Vite / Webpack 开发时,我们经常会遇到一个"隐性陷阱":开发环境下热更新(HMR, Hot Module Replacement)会导致 Axios 响应拦截器被重复注册。这个问题在控制台中表现为接口响应日志被多次打印、错误提示重复弹出,甚至引发逻辑混乱。
本文将结合实际代码,深入剖析该问题的成因,并介绍一种稳定、可靠的解决方案。
一、问题现象
假设我们在 request.js 中配置了 Axios 的响应拦截器:
// request.js
import axios from 'axios'
axios.interceptors.response.use(
(res) => {
console.log('响应拦截器执行')
// 处理响应逻辑...
return res.data
},
(err) => {
// 错误处理...
return Promise.reject(err)
}
)
当你在开发过程中修改任意文件触发热更新后,再次发起请求,会发现控制台中 "响应拦截器执行" 被打印了 两次、四次甚至更多次!
这说明:每次热更新都会重新执行 request.js 模块,从而重复注册新的拦截器。而旧的拦截器并未被清除,导致多个拦截器实例同时生效。
二、错误尝试:使用局部变量控制
很多开发者第一反应是用一个布尔变量来防止重复注册:
let isResponseRegistered = false
if (!isResponseRegistered) {
axios.interceptors.response.use(...)
isResponseRegistered = true
}
但你会发现,这个方案在热更新下依然失效------控制台还是打印了两次日志。
为什么?
因为 热更新会重新执行整个模块(包括变量声明) 。也就是说,每次 HMR 触发时,isResponseRegistered 都会被重置为 false,于是拦截器又被注册了一次。
💡 关键点:模块级变量在热更新时会被重新初始化,无法跨更新周期保持状态。
三、正确方案:将标志挂载到 Axios 实例上
要解决这个问题,必须使用一个 不会被热更新重置的对象属性 来记录是否已注册拦截器。
Axios 本身是一个对象(函数对象),我们可以直接在其上挂载自定义属性:
// 防止热更新重复注册响应拦截器
if (!axios.__myResponseInterceptor__) {
axios.interceptors.response.use(
(res) => {
console.log('响应拦截器执行')
// ...处理逻辑
return res.data
},
(err) => {
return Promise.reject(err)
}
)
axios.__myResponseInterceptor__ = true // 标记已注册
}
为什么这个方案有效?
axios是从node_modules引入的模块,在 HMR 中 通常不会被重新加载(属于"稳定的依赖")。- 因此,挂载在
axios上的属性__myResponseInterceptor__在热更新期间保持不变。 - 即使
request.js被反复执行,也能准确判断拦截器是否已存在。
✅ 这是一种被社区广泛验证的有效做法,类似方案也用于防止重复注册全局组件、插件等。
四、完整代码示例(来自实际项目)
以下是从你提供的 request.js 中提取的核心逻辑:
// 防止响应拦截器被重复注册
if (!axios.__myResponseInterceptor__) {
axios.interceptors.response.use(
(res) => {
// 二进制数据直接返回
if (res.request?.responseType === 'blob' || ...) {
return res.data
}
if (!res.data) {
return { code: 200, data: null, message: '热更新中' }
}
switch (Number(res.data.code)) {
case 401:
router.push('/login')
break
case 200:
if (res.data.token) {
localStorage.setItem('token', res.data.token)
}
return res.data
default:
return res.data
}
},
(err) => {
return Promise.reject(err)
}
)
axios.__myResponseInterceptor__ = true // 关键:标记已注册
}
该方案成功解决了热更新下的重复注册问题,且不影响生产环境(生产环境无 HMR,只执行一次)。
五、其他可行方案(补充)
-
使用
import.meta.hot?.accept()手动管理副作用 (Vite 特有)可在模块卸载时移除拦截器,但实现复杂,不推荐。
-
将拦截器注册移到
main.js或入口文件减少被 HMR 影响的概率,但若入口文件也被更新,仍可能失效。
-
使用单例模式封装 Axios 实例
例如导出一个
createAxiosInstance()函数,并缓存实例。但需确保调用方不重复创建。
相比之下,挂载标志位到 axios 对象上是最简单、可靠、低侵入性的方案。
六、总结
| 方案 | 是否有效 | 原因 |
|---|---|---|
局部变量 let flag = false |
❌ | 热更新重置变量 |
挂载到 axios 对象上 |
✅ | axios 模块稳定,属性持久 |
| 移到 main.js | ⚠️ | 降低概率,但非根治 |
| 手动 HMR 清理 | ✅ 但复杂 | 需要监听模块更新事件 |
最佳实践 :在开发 Axios 封装库时,务必考虑 HMR 场景,使用
axios.__xxx__标志位防止重复注册拦截器。
希望本文能帮助你彻底理解并解决这一"开发期幽灵 bug"。如果你觉得有用,欢迎点赞、收藏、转发!