axios取消重复请求(保留第一次,取消之后的重复请求)

博客讨论了如何在使用axios时处理重复请求的问题,尤其是对于登录等操作,避免因用户频繁点击导致的请求冲突。作者修改了现有的解决方案,以确保相同的请求仅保留第一次,后续的重复请求会被取消。在实施过程中,作者发现一个bug,即响应后的请求无法正确从map中删除,原因是生成requestKey的方法在不同阶段产生了不一致的结果。通过创建两个不同的requestKey生成函数,分别用于添加请求和响应时使用,问题得到了解决。

我在网上找了一个axios解决重复请求的代码

import axios from 'axios'
axios.defaults.baseURL = 'http://124.93.196.45:10001/'
const pendingRequest = new Map()

//`generateReqKey`:用于根据当前请求的信息,生成请求 Key;
function generateReqKey(config: any) {
  const { method, url, params, data } = config
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}

//`addPendingRequest:用于把当前请求信息添加到pendingRequest对象中;
function addPendingRequest(config: any) {
  const requestKey = generateReqKey(config)
  config.cancelToken =
    config.cancelToken ||
    new axios.CancelToken((cancel: any) => {
      if (!pendingRequest.has(requestKey)) {
        pendingRequest.set(requestKey, cancel)
      }
    })
}

//`removePendingRequest`:检查是否存在重复请求,若存在则取消已发的请求。
function removePendingRequest(config: any) {
  const requestKey = generateReqKey(config)
  if (pendingRequest.has(requestKey)) {
    const cancelToken = pendingRequest.get(requestKey)
    cancelToken(requestKey)
    pendingRequest.delete(requestKey)
  }
}
function clearPending() {
  for (const [requestKey, cancelToken] of pendingRequest) {
    cancelToken(requestKey)
  }
  pendingRequest.clear()
}
axios.interceptors.request.use(
  function (config: any) {
    removePendingRequest(config) // 检查是否存在重复请求,若存在则取消已发的请求
    addPendingRequest(config) // 把当前请求信息添加到pendingRequest对象中

    return config
  },
  (error) => {
    // 这里出现错误可能是网络波动造成的,清空 pendingRequests 对象
    pendingRequest.clear()
    return Promise.reject(error)
  }
)
axios.interceptors.response.use(
  (response) => {
    removePendingRequest(response.config) // 从pendingRequest对象中移除请求
    return response
  },
  (error) => {
    removePendingRequest(error.config || {}) // 从pendingRequest对象中移除请求
    if (axios.isCancel(error)) {
      console.warn(error)
      return Promise.reject(error)
    } else {
      // 添加其它异常处理
    }
    return Promise.reject(error)
  }
)
//通过请求拦截器实现如果有token自动添加token,如果没有token就不携带token
export default axios
export {clearPending}
//添加全局路由守卫,在跳转时自动清空PendingRequest
router.beforeEach((to, from, next) => {
  clearPending();
}) 

但是这种方式他是取消之前相同的请求保留最后一次请求,于是会出现不好的体验:例如我疯狂点击登录按钮,他会一直登录不进去(因为上次一请求都未完成就被取消了开启新的请求,如此反复一直在请求没有响应)

于是我准备修改一下,根据他的代码改成如果是相同的请求(请求参数相同),我就保留第一次的请求,取消当前发送的请求

1.引入取消请求的语句
+  const CancelToken = axios.CancelToken;
+  const source = CancelToken.source(); 
2.我修改了removePendingRequest()函数
function removePendingRequest(config: any) {
  const requestKey = generateReqKey(config)
  if (pendingRequest.has(requestKey)) {
  //删除原来用于取消之前重复请求的语句
-    const cancelToken = pendingRequest.get(requestKey)
-    cancelToken(requestKey)
-    pendingRequest.delete(requestKey)
//添加自己的取消请求语句:取消当前请求
+    config.cancelToken = source.token;
+     source.cancel()   
  }
} 
3.修改响应拦截器移除请求的语句
axios.interceptors.response.use(
  (response) => {
     //原本用于移除的函数被我们修改了所以这里移除请求不起作用,必须自己重新编写移除的语句
-    removePendingRequest(response.config)
     //重新获取响应的config自己手动在map里移除一次该请求
+    const requestKey = GenerateReqKey(response.config)
+    pendingRequest.delete(requestKey) // 从pendingRequest对象中移除请求
    return response
  },
  (error) => {
    removePendingRequest(error.config || {}) // 从pendingRequest对象中移除请求
    if (axios.isCancel(error)) {
      console.warn(error)
      return Promise.reject(error)
    } else {
      // 添加其它异常处理
    }
    return Promise.reject(error)
  }
) 

如此下来确实可以实现如果是相同的请求(请求参数相同)保留第一次的请求,取消当前发送的请求 但是出现了一个bug:等待第一次请求完成还是发送不了当前相同参数的请求 原因是响应后本来应该在响应完成的时候在map中去除该请求但是却没移除成功了

于是我打印了一下添加至map时requestKey的响应生成的requestKey

1651807207329_@1%J`MPJRRURO(I}4)07UQM.png

很明显他们根本不一样所以pendingRequest.delete(requestKey)的时候map肯定移除不了此请求,而且根据打印的结果很明显是GenerateReqKey()函数在响应生成的requestKey时比添加生成的requestKey多JSON.stringify()了一次

继续分析他们在执行GenerateReqKey()时传入的config是否一致 0bd708eb2dd91daf026c3798b22c1a3ad85814bb04f4e612fc15b6c302b5732aQzpcVXNlcnNcd2luMTBcQXBwRGF0YVxSb2FtaW5nXERpbmdUYWxrXDgzOTU3NDgwOV92MlxJbWFnZUZpbGVzXDE2NTE4MDc1NDc4MjBfQzhgNltIMzZMSVpGe1U0SlRWTkxJXzkucG5n.png

500f0d26027663b279c0657a340bd7af15890089882a18eb26274a275778edabQzpcVXNlcnNcd2luMTBcQXBwRGF0YVxSb2FtaW5nXERpbmdUYWxrXDgzOTU3NDgwOV92MlxJbWFnZUZpbGVzXDE2NTE4MDc3OTI2NDBfVVJMX2E5ODdkMzM5MjVlZjEyZDg4ODE4ZWJjMzdhOTU0NTFh.png

YCX6J@(Q77DBR4B)MIA6Y.png

72a630e0486f79b8a88ec433dacef441c73d30c0ee0239f6326a47e674937f67QzpcVXNlcnNcd2luMTBcQXBwRGF0YVxSb2FtaW5nXERpbmdUYWxrXDgzOTU3NDgwOV92MlxJbWFnZUZpbGVzXDE2NTE4MDc4NTkwODlfUktfKTJaS0R+KVhINTZFQF1QNilZM0cucG5n.png

我设置一个add和res用来放添加时的config和响应时的config,判断他们是否相同,返回了true显示一模一样,一模一样的参数到了gererateReqKey为函数为什么返回却不一样?这个问题我也不是很清楚,但是我已经有了解决方案了

直接设置两个不一样的生成requestKey的函数,判断是添加时要生成还是响应回来时要生成
如果添加就调用添加的函数
如果响应就调用响应的函数

//正常的
function generateReqKey(config: any) {
  const { method, url, params, data } = config
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
} 
//响应的(不再执行JSON.stringify())
function resGenerateReqKey(config: any) {
  const { method, url, params, data } = config
  return [method, url, params, data].join('&')
} 

于是就可以成功地在响应后map里移除请求了

下面是全部的源代码

import axios from 'axios'
axios.defaults.baseURL = 'http://124.93.196.45:10001/'
const pendingRequest = new Map()
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
function generateReqKey(config: any) {
  const { method, url, params, data } = config
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
} function resGenerateReqKey(config: any) {
  const { method, url, params, data } = config
  return [method, url, params, data].join('&')
}

function addPendingRequest(config: any) {
  const requestKey = generateReqKey(config)
  console.log(requestKey, 'add');

  config.cancelToken =
    config.cancelToken ||
    new axios.CancelToken((cancel: any) => {
      if (!pendingRequest.has(requestKey)) {
        pendingRequest.set(requestKey, cancel)
      }
    })
}
function removePendingRequest(config: any) {
  const requestKey = generateReqKey(config)
  if (pendingRequest.has(requestKey)) {

    config.cancelToken = source.token;
    source.cancel()

  }
}
function clearPending() {
  for (const [requestKey, cancelToken] of pendingRequest) {
    cancelToken(requestKey)
  }
  pendingRequest.clear()
}
axios.interceptors.request.use(
  function (config: any) {
    if (localStorage.token) {
    //通过请求拦截器实现如果有token自动添加token,如果没有token就不携带token
      config.headers['Authorization'] = `Bearer ${localStorage.token}`
    }
    removePendingRequest(config) // 检查是否存在重复请求,若存在则取消已发的请求
    addPendingRequest(config) // 把当前请求信息添加到pendingRequest对象中

    return config
  },
  (error) => {
    // 这里出现错误可能是网络波动造成的,清空 pendingRequests 对象
    pendingRequest.clear()
    return Promise.reject(error)
  }
)
axios.interceptors.response.use(
  (response) => {
    const requestKey = resGenerateReqKey(response.config)

    pendingRequest.delete(requestKey)
    // 从pendingRequest对象中移除请求
    if (response.data.code != 200) {
      alert(response.data.msg)
    }
    return response
  },
  (error) => {
    removePendingRequest(error.config || {}) // 从pendingRequest对象中移除请求
    if (axios.isCancel(error)) {
      console.warn(error)
      return Promise.reject(error)
    } else {
      // 添加其它异常处理
    }
    return Promise.reject(error)
  }
)

export default axios
export { clearPending } 
<think>好的,我现在需要处理用户关于“axios处理重复请求”的问题。用户之前已经询问过如何用qs库替换GET参数处理,现在转向另一个axios相关的问题。首先,我需要理解用户的需求是什么。他们可能遇到了重复请求导致的问题,比如表单重复提交、数据重复加载,或者资源浪费。用户可能希望了解如何识别和阻止重复请求,提升应用性能和数据一致性。 接下来,我需要回忆axios的相关知识。axios本身并没有内置的重复请求处理机制,但可以通过拦截器、CancelToken或AbortController来实现。同时,用户可能想知道不同的方法及其优缺点,比如基于URL和参数的哈希,或者使用请求标识符。还需要考虑如何管理取消令牌,避免内存泄漏,或者在Vue、React等框架中的最佳实践。 然后,我需要组织回答的结构。用户之前的回复结构清晰,分步骤、关键点、配置选项等,所以这次也应该保持类似结构。可能需要包括重复请求的场景、解决方案(拦截器、CancelToken、AbortController)、实现步骤、注意事项等。同时,用户可能希望有代码示例,方便直接应用。 还需要注意用户可能的深层需求。例如,他们可能在开发中遇到重复请求的问题,特别是在SPA中,快速点击按钮导致多次提交。或者,在数据获取时,页面切换时未取消请求,导致状态混乱。因此,需要覆盖这些场景,并提供对应的解决方案。 另外,考虑到用户之前使用了qs库替换参数处理,可能他们的项目正在优化axios的配置,所以需要强调如何将重复请求处理整合到现有的axios实例中,保持代码的整洁和可维护性。 最后,要确保回答准确,避免错误。比如,CancelToken已被AbortController逐渐取代,但需要说明兼容性。同时,示例代码要正确,包括如何生成唯一标识,如何存储和取消请求等。可能还需要提到第三方库如axios-extensions,但以原生解决方案为主。 总结步骤: 1. 分析用户需求和可能的问题场景。 2. 确定使用axios拦截器和取消机制作为解决方案。 3. 结构化回答:场景、解决方案、实现步骤、代码示例、注意事项。 4. 强调与现有axios实例的整合,如拦截器的使用。 5. 提供不同方法的比较,如CancelToken vs AbortController。 6. 注意事项包括内存管理、唯一标识生成、错误处理等。 7. 可能的优化点,如防抖、请求缓存。</think>--- ### **Axios 处理重复请求的完整方案** 重复请求通常出现在以下场景中: - 用户快速点击提交按钮 - 网络延迟导致用户重复操作 - 页面切换时未及时取消未完成请求 - 定时器触发的自动刷新未做防重 --- #### **1. 核心解决思路** | **方法** | **实现原理** | **适用场景** | |----------------|---------------------------------------|---------------------------| | **请求标识符** | 为每个请求生成唯一标识,拦截重复标识 | 所有类型的重复请求 | | **自动取消** | 取消前一个相同请求(覆盖式请求) | 搜索建议、分页等连续性操作 | | **防抖控制** | 延迟执行,短时间内只保留最后一次请求 | 按钮点击、输入框搜索 | | **状态标记** | 通过全局状态管理标记正在进行的请求 | 需要UI状态同步的场景 | --- ### **2. 基于拦截器的实现方案(推荐)** #### **(1) 创建请求缓存映射** ```javascript // 存储进行中的请求 const pendingRequests = new Map(); /** * 生成请求唯一标识 * @param {object} config 请求配置 * @returns {string} 唯一标识符 */ function generateReqKey(config) { const { method, url, params, data } = config; return [method, url, qs.stringify(params), qs.stringify(data)].join('&'); } ``` #### **(2) 添加请求拦截器** ```javascript axios.interceptors.request.use(config => { // 生成请求标识 const requestKey = generateReqKey(config); // 检查重复请求 if (pendingRequests.has(requestKey)) { const cancel = pendingRequests.get(requestKey); cancel('重复请求被自动取消'); // 取消前一个请求 pendingRequests.delete(requestKey); // 删除旧记录 } // 创建新取消令牌 config.cancelToken = new axios.CancelToken(cancel => { pendingRequests.set(requestKey, cancel); }); return config; }); ``` #### **(3) 添加响应拦截器** ```javascript axios.interceptors.response.use( response => { // 请求完成后移除记录 const requestKey = generateReqKey(response.config); pendingRequests.delete(requestKey); return response; }, error => { // 如果是主动取消请求,不做错误处理 if (axios.isCancel(error)) { console.warn('已取消请求:', error.message); return Promise.reject({ isCanceled: true }); // 返回特殊标记 } // 其他错误处理 return Promise.reject(error); } ); ``` --- ### **3. 高级优化技巧** #### **(1) 自动取消模式(覆盖式请求)** ```javascript // 在请求拦截器中添加: if (config.enableCancelDuplicated) { const requestKey = generateReqKey(config); if (pendingRequests.has(requestKey)) { pendingRequests.get(requestKey)(); // 立即取消前一个 } } ``` #### **(2) 防抖模式(延迟请求)** ```javascript const debounceMap = new Map(); axios.interceptors.request.use(config => { if (config.debounceTime) { const key = generateReqKey(config); // 清除已有定时器 if (debounceMap.has(key)) { clearTimeout(debounceMap.get(key)); } // 创建延迟Promise return new Promise(resolve => { debounceMap.set(key, setTimeout(() => { debounceMap.delete(key); resolve(config); }, config.debounceTime)); }); } return config; }); ``` #### **(3) 结合Vue/React状态管理** ```javascript // Vue示例:在store中管理请求状态 const store = { state: { ongoingRequests: {} }, mutations: { setRequestPending(state, key) { Vue.set(state.ongoingRequests, key, true); }, setRequestComplete(state, key) { Vue.delete(state.ongoingRequests, key); } } }; // 在拦截器中同步状态 axios.interceptors.request.use(config => { const key = generateReqKey(config); store.commit('setRequestPending', key); }); axios.interceptors.response.use( response => { const key = generateReqKey(response.config); store.commit('setRequestComplete', key); }, error => { if (!axios.isCancel(error)) { const key = generateReqKey(error.config); store.commit('setRequestComplete', key); } } ); ``` --- ### **4. 不同取消方式的对比** | **特性** | **CancelToken** | **AbortController** (Axios >=0.22.0) | |-----------------------|-------------------------------------|---------------------------------------| | 浏览器兼容性 | 所有支持Promise的环境 | 需要现代浏览器(IE不支持) | | 取消方式 | `cancel()` 函数调用 | `abort()` 方法调用 | | 错误类型 | `Cancel` 对象 | `AbortError` DOM异常 | | 多请求控制 | 每个请求独立token | 可共享同一个signal | | 推荐使用场景 | 兼容旧项目 | 新项目/现代浏览器环境 | **AbortController 示例**: ```javascript const controller = new AbortController(); axios.get('/api', { signal: controller.signal }).catch(err => { if (err.name === 'AbortError') { console.log('请求取消'); } }); // 取消请求 controller.abort(); ``` --- ### **5. 注意事项** 1. **敏感参数处理**:生成唯一标识时排除随机参数(如时间戳) ```javascript function generateReqKey(config) { const filteredParams = omit(config.params, ['_t']); // 使用lodash.omit return [config.method, config.url, qs.stringify(filteredParams)].join('&'); } ``` 2. **内存泄漏预防**:设置最大缓存数量 ```javascript const MAX_PENDING = 100; if (pendingRequests.size > MAX_PENDING) { const oldestKey = Array.from(pendingRequests.keys())[0]; pendingRequests.get(oldestKey)('清理过期请求'); pendingRequests.delete(oldestKey); } ``` 3. **异常处理**:确保所有响应路径都清理缓存 ```javascript // 在响应拦截器的error分支中 if (error.config) { const key = generateReqKey(error.config); pendingRequests.delete(key); } ``` 4. **文件上传特殊处理**:对于FormData需要特殊处理 ```javascript function generateReqKey(config) { if (config.data instanceof FormData) { return [config.method, config.url].join('&'); // 不处理FormData内容 } // ...原有逻辑 } ``` --- ### **6. 完整代码整合** ```javascript import axios from 'axios'; import qs from 'qs'; const pendingRequests = new Map(); function generateReqKey(config) { const { method, url } = config; let params = config.params || {}; let data = config.data || {}; // 过滤随机参数 params = Object.fromEntries( Object.entries(params).filter(([key]) => !key.startsWith('_')) ); // 处理FormData if (data instanceof FormData) { return [method, url].join('&'); } return [method, url, qs.stringify(params), qs.stringify(data)].join('&'); } // 请求拦截器 axios.interceptors.request.use(config => { const requestKey = generateReqKey(config); if (pendingRequests.has(requestKey)) { const cancel = pendingRequests.get(requestKey); cancel(`重复请求: ${requestKey}`); pendingRequests.delete(requestKey); } config.cancelToken = new axios.CancelToken(cancel => { pendingRequests.set(requestKey, cancel); }); return config; }); // 响应拦截器 axios.interceptors.response.use( response => { const key = generateReqKey(response.config); pendingRequests.delete(key); return response; }, error => { if (axios.isCancel(error)) { return Promise.reject({ isCanceled: true }); } if (error.config) { const key = generateReqKey(error.config); pendingRequests.delete(key); } return Promise.reject(error); } ); // 使用示例 const fetchData = async () => { try { const response = await axios.get('/api/data', { params: { page: 1 } }); console.log(response.data); } catch (error) { if (!error.isCanceled) { console.error('请求错误:', error); } } }; ``` --- ### **7. 最佳实践建议** 1. **按需启用**:通过配置项控制是否检查重复请求 ```javascript axios.get('/api', { enableDuplicateCheck: true // 自定义配置项 }); ``` 2. **错误监控**:将取消请求记录到监控系统 ```javascript if (axios.isCancel(error)) { sendToMonitoring({ type: 'CANCELED_REQUEST', url: error.config.url }); } ``` 3. **组合策略**:关键操作使用「防抖 + 重复检查」双重保护 ```javascript submitButton.addEventListener('click', () => { axios.post('/submit', data, { debounceTime: 500, enableDuplicateCheck: true }); }); ``` 4. **SSR兼容**:在服务端渲染时禁用该功能 ```javascript if (typeof window !== 'undefined') { // 添加拦截器 } ``` --- ### **总结** 通过拦截器实现的重复请求处理方案可以: - ✅ 自动拦截重复请求 - ✅ 保持代码整洁(集中式管理) - ✅ 支持多种取消策略 - ✅ 兼容现代浏览器和Node.js环境 建议根据具体业务需求选择「完全拦截」或「覆盖式取消」模式,对于关键操作建议结合防抖/节流使用。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值