这三项能力是 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 再匆忙补课。