人人都会AI编程

请求取消、重复请求拦截、失败自动重试

更新时间:2026-07-09

这三项能力是 Axios 封装中非常实用的“稳定性增强层”,它们共同解决了网络请求中三个典型的痛点:组件销毁后残留的请求导致状态更新错误、用户快速点击导致的重复提交、以及网络波动引起的临时失败。以下逐一介绍实现思路与可直接使用的代码模式。


1. 请求取消:告别组件卸载后的“幽灵更新”

当组件已经销毁(比如用户快速切换路由),但之前发起的异步请求仍在路上,请求完成后会尝试更新一个已经不存在的组件状态,Vue 会在控制台抛出类似“Cannot read property of null”的警告,严重时还会引起内存泄漏。解决方法是在组件卸载时主动取消所有未完成的请求。

实现方式:利用 Axios 的 CancelToken 或者原生的 AbortController(推荐,Vue 3 + Axios 0.22+ 均支持)。

// 在组件内管理请求取消
import { ref, onBeforeUnmount } from 'vue'
import axios from 'axios'

const data = ref(null)
const controller = ref(null)

const fetchData = () => {
  // 如果上次请求还没结束,先取消它
  controller.value?.abort()
  // 创建新的 AbortController
  controller.value = new AbortController()
  
  axios.get('/api/data', {
    signal: controller.value.signal
  })
    .then(res => data.value = res.data)
    .catch(err => {
      if (err.code !== 'ERR_CANCELED') {
        // 只处理非取消的错误
        console.error(err)
      }
    })
}

// 组件卸载时取消所有未完成的请求
onBeforeUnmount(() => {
  controller.value?.abort()
})

注意点

  • AbortController 可以多次调用 abort(),已经结束的请求不会受影响。
  • 被取消的请求会进入 catch,需要通过 err.code === 'ERR_CANCELED' 或者 axios.isCancel(err) 来区分取消错误和真正的网络错误,避免弹出无意义的错误提示。

2. 重复请求拦截:防止“手抖”造成的表单重复提交

用户快速连续点击“提交”按钮,可能瞬间发出多次相同请求,不仅浪费带宽,还可能导致数据重复写入。我们可以在请求发起前判断是否存在相同的“进行中”请求,如果有就直接取消前一次请求,或者拦截本次请求。

方案一:取消前一个重复请求(适用于查询、实时搜索)

通过维护一个“请求映射表”,将每个请求的唯一标识(URL + 方法 + 排序后的参数)与一个 AbortController 关联。新请求到来时,取消相同标识的旧请求。

// 放置在 Axios 实例的拦截器中
import axios from 'axios'

const pendingMap = new Map()

function generateRequestKey(config) {
  const { method, url, params, data } = config
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}

function addPending(config) {
  const key = generateRequestKey(config)
  if (pendingMap.has(key)) {
    // 存在进行中的重复请求,取消它
    pendingMap.get(key).abort()
    pendingMap.delete(key)
  }
  
  const controller = new AbortController()
  config.signal = controller.signal
  pendingMap.set(key, controller)
}

function removePending(config) {
  const key = generateRequestKey(config)
  if (pendingMap.has(key)) {
    pendingMap.delete(key)
  }
}

const instance = axios.create()

instance.interceptors.request.use(config => {
  addPending(config)
  return config
})

instance.interceptors.response.use(
  response => {
    removePending(response.config)
    return response
  },
  error => {
    if (axios.isCancel(error)) {
      // 被取消的请求不需要额外处理
      console.log('重复请求被取消', error.message)
    } else {
      removePending(error.config || {})
    }
    return Promise.reject(error)
  }
)

方案二:完全阻止重复请求(适用于提交类操作)

如果希望同一时间段内只能存在一个特定请求,直接拒绝后续的请求,直到第一个完成:

const pendingMap = new Map()

instance.interceptors.request.use(config => {
  const key = generateRequestKey(config)
  if (pendingMap.has(key)) {
    // 直接返回一个永远 pending 的 Promise,或者抛出错误
    return Promise.reject(new axios.Cancel('重复请求被拦截'))
  }
  
  const controller = new AbortController()
  config.signal = controller.signal
  pendingMap.set(key, controller)
  return config
}, error => Promise.reject(error))

instance.interceptors.response.use(
  response => {
    const key = generateRequestKey(response.config)
    pendingMap.delete(key)
    return response
  },
  error => {
    if (!axios.isCancel(error)) {
      const key = generateRequestKey(error.config || {})
      pendingMap.delete(key)
    }
    return Promise.reject(error)
  }
)

最佳实践

  • 根据业务场景灵活选择:查询类接口通常用“取消前一个”(如搜索联想),提交类接口用“立即阻止”。
  • 配合按钮的 loading 状态从 UI 层面双重防护,体验更好。
  • 如果使用了 Pinia 或者组件状态,可以在请求开始时设置 loading = true,结束后恢复,避免用户重复操作。

3. 失败自动重试:应对网络抖动和临时故障

弱网环境下,一次请求可能因 DNS 解析延迟、TCP 握手失败等临时问题而报错,直接提示用户“网络错误”体验很差。通过失败重试机制,可以给请求“二次机会”。

实现原理:在响应拦截器的错误分支中,判断错误类型是否需要重试(通常只有网络错误或 5xx 服务端错误才重试,4xx 业务错误不重试),并根据配置的次数和延迟重新发送请求。

// 在 Axios 封装的实例中添加重试逻辑
const instance = axios.create({
  retry: 3,               // 最大重试次数
  retryDelay: 1000,       // 重试延迟(毫秒)
  retryCondition: (err) => {
    // 只有网络超时或 5xx 错误才重试
    return axios.isAxiosError(err) && (!err.response || err.response.status >= 500)
  }
})

instance.interceptors.response.use(undefined, async (err) => {
  const config = err.config
  
  // 如果没有配置重试次数,或者已经达到最大重试次数,直接返回错误
  if (!config || !config.retry) return Promise.reject(err)
  
  config.__retryCount = config.__retryCount || 0
  
  if (config.__retryCount >= config.retry) {
    return Promise.reject(err)
  }
  
  // 判断是否符合重试条件
  if (config.retryCondition && !config.retryCondition(err)) {
    return Promise.reject(err)
  }
  
  config.__retryCount += 1
  
  // 创建延迟 Promise
  const backoff = new Promise(resolve => {
    setTimeout(() => {
      resolve()
    }, config.retryDelay || 1000)
  })
  
  console.log(`请求失败,将进行第 ${config.__retryCount} 次重试`)
  
  await backoff
  // 重新发送请求(注意此时 config 中的 signal 可能已失效,需要清除)
  return instance(config)
})

注意点

  • 不要对所有错误重试:4xx 客户端错误(如 401 未授权、404 不存在)重试毫无意义,应直接进入业务错误处理流程。
  • 退避策略:简单的做法是固定延迟,更好的做法是使用指数退避(例如第 1 次重试 1 秒,第 2 次 2 秒,第 3 次 4 秒),避免对服务器造成压力。
  • 重试计数:使用自定义属性 config.__retryCount 存放当前重试次数,避免污染原始配置。
  • 清理重复请求映射:如果项目中同时使用了重复请求拦截,在重试前需要确保该请求已经从 pendingMap 中移除,否则会被自己的拦截器误判为重复。
  • 超时与取消:重试期间如果组件被销毁,应配合请求取消机制中止重试,避免无效等待。

通过这三项能力的组合,一个 Axios 实例就能在绝大多数网络场景下保持健壮:不会因为组件卸载而报错、不会因用户误操作导致重复提交、也不会因网络一闪而直接抛错。这些都是前端工程化中“把事情做稳”的基本功,值得在项目初期就封装好,而不是等到线上出现 bug 再匆忙补课。