解决 Vue + Axios 热更新导致响应拦截器重复注册的问题

在使用 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,只执行一次)。


五、其他可行方案(补充)

  1. 使用 import.meta.hot?.accept() 手动管理副作用 (Vite 特有)

    可在模块卸载时移除拦截器,但实现复杂,不推荐。

  2. 将拦截器注册移到 main.js 或入口文件

    减少被 HMR 影响的概率,但若入口文件也被更新,仍可能失效。

  3. 使用单例模式封装 Axios 实例

    例如导出一个 createAxiosInstance() 函数,并缓存实例。但需确保调用方不重复创建。

相比之下,挂载标志位到 axios 对象上是最简单、可靠、低侵入性的方案。


六、总结

方案 是否有效 原因
局部变量 let flag = false 热更新重置变量
挂载到 axios 对象上 axios 模块稳定,属性持久
移到 main.js ⚠️ 降低概率,但非根治
手动 HMR 清理 ✅ 但复杂 需要监听模块更新事件

最佳实践 :在开发 Axios 封装库时,务必考虑 HMR 场景,使用 axios.__xxx__ 标志位防止重复注册拦截器。

希望本文能帮助你彻底理解并解决这一"开发期幽灵 bug"。如果你觉得有用,欢迎点赞、收藏、转发!

相关推荐
Coder_preston2 小时前
JavaScript学习指南
开发语言·javascript·ecmascript
岁岁种桃花儿2 小时前
NodeJs从入门到上天:什么是Node.js
前端·node.js
Jinuss2 小时前
源码分析之React中Scheduler调度器的最小二叉堆
javascript·算法·react.js
a1117762 小时前
电流卡片特效(html网页 开源)
javascript·css·css3
colicode2 小时前
语音报警接口开发参考:紧急情况下快速调用语音API发送安全警报
前端·语音识别
狗都不学爬虫_2 小时前
JS逆向 -最新版 盼之(decode__1174、ssxmod_itna、ssxmod_itna2)纯算
javascript·爬虫·python·网络爬虫·wasm
天天进步20152 小时前
透明的可观测性:剖析 Motia Workbench 与插件系统架构
javascript
夏河始溢2 小时前
一八四、Zustand 状态管理详解、与 Redux、MobX 的对比分析
前端·javascript·react.js·状态管理·zustand
wangmengxxw2 小时前
设计模式 -详解
开发语言·javascript·设计模式