Axios取消重复请求,但能让最新请求作为最终返回,且能共享状态 ,不知小伙您有没有尝到真香~

起因,在项目中发现多个地方同时请求同一个接口且参数相同时,只有最后一个接口能正常返回,并且只有一个地方会展示查询结果...这个问题让我想起了在封装Axios时,参考网上的例子直接抄来用。

错误方式

以上这种情况发出了8个相同的请求,前7个被取消了,只有最后一个生效,这种会导致前面的请求都报错,得不到想要的结果,先来分析一下以上的实现原理,下面贴出关键代码。

js 复制代码
// fetch.js
import axios from 'axios';

const service = axios.create({
  baseURL: '/api',
  // validateStatus: (status) => status <= 500, // 拦截状态码大于或等于500
  headers: {
    common: { Accept: 'application/json; charset=UTF-8' },
    patch: { 'Content-Type': 'application/json; charset=UTF-8' },
    post: { 'Content-Type': 'application/json; charset=UTF-8' },
    put: { 'Content-Type': 'application/json; charset=UTF-8' }
  },
  transformRequest: (data) => JSON.stringify(data),
  timeout: 30000, // 请求超时时间
  isRepeatRequest: true, // 是否开启重复请求拦截
})
const pending = {}
const CancelToken = axios.CancelToken

const paramsList = ['get', 'delete', 'patch']
const dataList = ['post', 'put']
const isTypeList = (method) => {
  if (paramsList.includes(method)) {
    return 'params'
  } else if (dataList.includes(method)) {
    return 'data'
  }
}

/**
 * 获取请求的key
 * @param {object} config
 * @param {boolean} isSplice - 是否拼接请求头,请求前需要拼接,请求后不需要拼接
 * @returns
 */
const getRequestIdentify = (config, isSplice = false) => {
  let url = config.url
  if (isSplice) {
    url = config.baseURL + config.url
  }
  const params = { ...(config[isTypeList(config.method)] || {}) }
  // t 是随机数,不参与计算唯一值
  delete params.t
  return encodeURIComponent(url + JSON.stringify(params))
}

/**
 * 取消重复
 * @param {string} key - 请求唯一url
 * @param {boolean} isRequest - 是否执行取消请求
 */
const removePending = (key, isRequest = false) => {
  if (pending[key] && isRequest) {
    pending[key].cancel('取消重复请求')
  }
  delete pending[key]
}


service.interceptors.request.use((config) => {
  const requestId = getRequestIdentify(config, true)
  config.requestId = requestId

  // 根据配置是否移除重复请求
  config.isRepeatRequest && removePending(requestId, true)

  if (!config.cancelToken) {
    const source = CancelToken.source()
    source.token.cancel = source.cancel
    config.cancelToken = source.token
  }
  // 缓存该请求的取消重复的方法
  pending[requestId] = config.cancelToken



  return config
})

service.interceptors.response.use((response) => {
  // 请求完成,移除缓存
  response.config.isRepeatRequest && removePending(response.config.requestId, false)

  return response.data
}, (error) => {
  if (axios.isCancel(error)) return Promise.reject(error)

  // 请求完成,移除缓存
  error.config?.isRepeatRequest && removePending(response.config.requestId, false)

  return Promise.reject(error)
})

/**
 * get请求方法
 * @export axios
 * @param {string} url - 请求地址
 * @param {object} params - 请求参数
 * @param {object|undefined|Null} 其他参数
 * @returns
 */
export const GET = (url, params, other) => {
  params = params || {}
  params.t = Date.now()
  return service({
    url: url,
    method: 'GET',
    params,
    ...(other || {})
  })
}

通过pending定义空对象(第16行 ),每次请求前判断拦截是否有重复(第63行 ),有得直接拦截,并返回错误,随后在请求完成时移除请求(第80、88行)。

最终方式

以下将分享一种能够取消重复请求,但能让最新请求作为最终返回,且能多个请求共享状态的方法,关机思路通过对axios.create实例化的方法多包裹一层Promise,并在每次请求前记录一些关键参数。

js 复制代码
// fetchSuper.js
const isTypeList = (method) => {
  method = (method || '').toLowerCase()
  if (paramsList.includes(method)) {
    return 'params'
  } else if (dataList.includes(method)) {
    return 'data'
  }
}
// 用来判断取消请求的标识
const REPEAT_REQUEST_TEXT = 'repeatRequest'

/**
 * 包装实际请求动作
 * @param {Axios.config} config - 最终合并后的参数
 * @param {string} requestId - 请求的唯一值
 * @param {Promise.resolve} resolve
 * @param {Promise.reject} reject 
 */
const defaultAdapterRequest = (config, requestId, resolve, reject) => {
  service(config)
    .then((response) => {
      // 请求成功时,删除缓存,并返回到最上层
      delete pending[requestId]
      resolve && resolve(response)
    })
    .catch((error) => {
      if (!(axios.isCancel(error) && error.message === REPEAT_REQUEST_TEXT)) {
        delete pending[requestId]
        reject && reject(error)
      }
    })
}

/**
 * 包装实际请求动作
 * @param {Axios.config} config - 最终合并后的参数
 * @param {string} requestId - 请求的唯一值
 * @param {Promise.resolve} resolve
 * @param {Promise.reject} reject 
 */
const defaultAdapterRequest = (config, requestId, resolve, reject) => {
  service(config)
    .then((response) => {
      // 请求成功时,删除缓存,并返回到最上层
      delete pending[requestId]
      resolve && resolve(response)
    })
    .catch((error) => {
      if (!(axios.isCancel(error) && error.message === REPEAT_REQUEST_TEXT)) {
        delete pending[requestId]
        reject && reject(error)
      }
    })
}

/**
 * 包装请求方法
 * @param {Axios.config} config 
 * @returns 
 */
const packService = (config) => {
  // 这里为什么不用 webpack.merge 进行深度合并,是因为太消耗性能且一般用不上,普通合并即可
  const mergeConfig = Object.assign({}, service.defaults, config)
  const requestId = getRequestIdentify(mergeConfig)
  mergeConfig.requestId = requestId
  if (!mergeConfig.cancelToken) {
    const source = CancelToken.source()
    source.token.cancel = source.cancel
    mergeConfig.cancelToken = source.token
  }

  // 上传文件或者主动不要重复,则直接请求
  if (
    !mergeConfig.isRepeatRequest ||
    mergeConfig.headers?.['Content-Type'] === 'multipart/form-data;charset=UTF-8'
  ) {
    return service(mergeConfig)
  }

  // 关键就在这里,如果第一次进来
  if (!pending[requestId]) {
    pending[requestId] = {}
    // 包装多一层Promise,并往缓存存入 cancelToken、resolve、reject、promiseFn
    const promiseFn = new Promise((resolve, reject) => {
      pending[requestId] = {
        cancelToken: mergeConfig.cancelToken,
        resolve,
        reject
      }
      defaultAdapterRequest(mergeConfig, requestId, resolve, reject)
    })
    pending[requestId].promiseFn = promiseFn
    return promiseFn
  }

  // 非第一次进来,则直接取消上一次的请求,并且替换缓存的cancelToken为当前的,否则下一次进来不能正确取消一次的请求
  const { cancelToken, resolve, reject, promiseFn } = pending[requestId]
  cancelToken.cancel(REPEAT_REQUEST_TEXT)
  pending[requestId].cancelToken = mergeConfig.cancelToken
  defaultAdapterRequest(mergeConfig, requestId, resolve, reject)
  return promiseFn
}

// 最后暴露出去的方法,则改为用packService进行包装

/**
 * get请求方法
 * @export axios
 * @param {string} url - 请求地址
 * @param {object} params - 请求参数
 * @param {object|undefined|Null} 其他参数
 * @returns
 */
export const GET = (url, params, other) => {
  params = params || {}
  params.t = Date.now()
  return packService({
    url: url,
    method: 'GET',
    params,
    ...(other || {})
  })
}

/**
 * post请求方法
 * @export axios
 * @param {string} url - 请求地址
 * @param {object} data - 请求参数
 * @param {object|undefined|Null} 其他参数
 * @returns
 */
export const POST = (url, data = {}, other) => {
  return packService({
    url,
    method: 'POST',
    params: { t: Date.now() },
    data,
    ...(other || {})
  })
}

defaultAdapterRequest函数用于包装实际的请求操作。它接收一个配置对象 config、一个请求的唯一标识 requestId 和两个回调函数 resolvereject。当请求成功时,从缓存中删除该请求的信息,并通过 resolve 回调返回响应;如果请求失败(但不是因为重复请求被取消),同样从缓存中删除信息并通过 reject 回调传递错误。

packService是一个关键的函数,用于处理请求前的准备工作。它首先合并了默认配置和传入的配置,然后生成一个请求的唯一标识 requestId。它会检查是否有相同的请求正在处理中。如果有,它会取消之前的请求并重新发起新的请求;如果没有,则直接发起请求并将相关信息存储在缓存中。

总结

以上是个人经验总结,可能适用你,也可能不适用你,选择一款作为自己项目合适搭配即可。另外,本来是想从Axiosadapter下手进行改造,发现最终请求回来并不会走自定义的adapter方法,看了源码才知道,这只是其中一个环,并不会作为最终返回,故而在外层加多一层Promise进行拦截,还有在转换请求唯一值(requestId)可能存在性能问题,如果入参很多,转换效率就会下降,不知道各位道友是否更好的方法。

Demo

相关推荐
kyriewen12 分钟前
豆包和千问同时关了智能体,我用它们搭的 3 个自动化全废了——迁移方案整理
前端·javascript·ai编程
前端一小卒25 分钟前
我用 TypeScript 从零手写了一个 Claude Code,然后发现它的核心只有 30 行
前端·agent
铁皮饭盒44 分钟前
用 Bun.cron 定时 7 月 7 日,为啥? 看图1
javascript
大圣编程2 小时前
Python中continue语句的用法是什么?
开发语言·前端·python
yuhaiqiang2 小时前
随手 vibecoding 的浏览器插件已经 6000 多次下载,聊聊他的产品设计
前端·后端·面试
之歆3 小时前
Vue商品详情与放大镜组件
前端·javascript·vue.js
再吃一根胡萝卜3 小时前
如何把小米 MiMo 接入 CodeBuddy,打造私有 Agent
前端
负责的蛋挞4 小时前
异步HttpModule的实现方式
java·服务器·前端
丹宇码农7 小时前
把 HLS 字幕玩出花:zwPlayer 如何让 M3U8 视频支持全文搜索、翻译与码率自适应
前端·javascript·音视频·hls·视频播放器