基于Axios封装请求---防止接口重复请求解决方案

 一、引言

前端接口防止重复请求的实现方案主要基于以下几个原因:

  1. 用户体验:重复发送请求可能导致页面长时间无响应或加载缓慢,从而影响用户的体验。特别是在网络不稳定或请求处理时间较长的情况下,这个问题尤为突出。

  2. 服务器压力:如果前端不限制重复请求,服务器可能会接收到大量的重复请求,这不仅增加了服务器的处理负担,还可能导致资源浪费。

  3. 数据一致性:对于某些操作,如表单提交,重复请求可能导致数据重复插入或更新,从而破坏数据的一致性。

为了实现前端接口防止重复请求,可以采取以下方案:

  1. 设置请求标志:在发送请求时,为请求设置一个唯一的标识符(如请求ID)。在请求处理过程中,可以通过检查该标识符来判断是否已存在相同的请求。如果存在,则取消或忽略重复请求。

  2. 使用防抖(debounce)和节流(throttle)技术:这两种技术都可以用来限制函数的执行频率。防抖是在一定时间间隔内只执行一次函数,而节流是在一定时间间隔内最多执行一次函数。这两种技术可以有效防止用户频繁触发事件导致的重复请求。

  3. 取消未完成的请求:在发送新的请求之前,可以检查是否存在未完成的请求。如果存在,则取消这些请求,以避免重复发送。这通常可以通过使用Promise、AbortController等技术实现。

  4. 前端状态管理:使用状态管理工具(如Redux、Vuex等)来管理请求状态。在发送请求前,检查状态以确定是否已存在相同的请求。这种方案可以更加灵活地控制请求的行为。

  5. 后端接口设计:虽然前端可以采取措施防止重复请求,但后端接口的设计也非常重要。例如,可以为接口设置幂等性,确保即使多次调用接口也不会产生副作用。此外,还可以使用令牌(token)等机制来限制请求的重复发送。

综合使用这些方案,可以有效地防止前端接口的重复请求,提高用户体验和系统的稳定性。

 二、取消未完成的请求

  1、Axios内置的 axios.CancelToken

import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import axios from 'axios'

const CancelToken = axios.CancelToken
const queue: any = [] // 请求队列

const service = axios.create({
  baseURL: '/api',
  timeout: 10 * 60 * 1000,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8',
  },
})

// 取消重复请求
const removeRepeatRequest = (config: AxiosRequestConfig) => {
  for (const key in queue) {
    const index = +key
    const item = queue[key]

    if (
      item.url === config.url &&
      item.method === config.method &&
      JSON.stringify(item.params) === JSON.stringify(config.params) &&
      JSON.stringify(item.data) === JSON.stringify(config.data)
    ) {
      // 执行取消操作
      item.cancel('操作太频繁,请稍后再试')
      queue.splice(index, 1)
    }
  }
}

// 请求拦截器
service.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    removeRepeatRequest(config)

    config.cancelToken = new CancelToken(c => {
      queue.push({
        url: config.url,
        method: config.method,
        params: config.params,
        data: config.data,
        cancel: c,
      })
    })
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse): any => {
    removeRepeatRequest(response.config)

    return Promise.resolve(response)
  },
  error => {
    return Promise.reject(error)
  }
)

export default service

 2、发布订阅方式

💡灵感来源: 前端接口防止重复请求实现方案

/*
 * @Author: LYM
 * @Date: 2024-03-28 14:12:54
 * @LastEditors: LYM
 * @LastEditTime: 2024-03-28 14:56:44
 * @Description: 封装axios
 */
import { gMessageError, gMessageWarning, gMessageSuccess } from '@/plugins/naiveMessage'
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
import axios from 'axios'
import { ContentTypeEnum } from './httpEnum'
import { checkResponseHttpStatus, loginStatusExpiresHandler } from './statusHandler'
import type { IRequestOptions, IResult } from './types'

const baseURL = import.meta.env.VITE_USER_BASE_URL

let isRefreshing: boolean = false
let retryRequests: any[] = []

// 发布订阅
class EventEmitter {
  [x: string]: {}
  constructor() {
    this.event = {}
  }
  on(type: string | number, cbres: any, cbrej: any) {
    if (!this.event[type]) {
      this.event[type] = [[cbres, cbrej]]
    } else {
      this.event[type].push([cbres, cbrej])
    }
  }

  emit(type: string | number, res: any, ansType: string) {
    if (!this.event[type]) return
    else {
      this.event[type].forEach((cbArr: ((arg0: any) => void)[]) => {
        if (ansType === 'resolve') {
          cbArr[0](res)
        } else {
          cbArr[1](res)
        }
      })
    }
  }
}

// 根据请求生成对应的key
const generateReqKey = (
  config: { method: string; url: string; params: string; data: string },
  hash: string
) => {
  const { method, url, params, data } = config
  return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join('&')
}

// 判断是否为上传请求
const isFileUploadApi = (config: { data: any }) => {
  return Object.prototype.toString.call(config.data) === '[object FormData]'
}

// 存储已发送但未响应的请求
const pendingRequest = new Set()
// 发布订阅容器
const ev = new EventEmitter()

const service = axios.create({
  baseURL: import.meta.env.VITE_BASE_URL,
  timeout: 10 * 60 * 1000,
  headers: {
    'Content-Type': ContentTypeEnum.FORM_URLENCODED,
  },
})

// 请求拦截器
service.interceptors.request.use(
  async (config: any) => {
    const hash = location.hash
    // 生成请求Key
    const reqKey = generateReqKey(config, hash)

    if (!isFileUploadApi(config) && pendingRequest.has(reqKey)) {
      // 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
      // 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
      let res = null
      try {
        // 接口成功响应
        res = await new Promise((resolve, reject) => {
          ev.on(reqKey, resolve, reject)
        })
        return Promise.reject({
          type: 'limitResSuccess',
          val: res,
        })
      } catch (limitFunErr) {
        // 接口报错
        return Promise.reject({
          type: 'limitResError',
          val: limitFunErr,
        })
      }
    } else {
      // 将请求的key保存在config
      config.pendKey = reqKey
      pendingRequest.add(reqKey)
    }

    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse): any => {
    const res = response.data || {}

    // 将拿到的结果发布给其他相同的接口
    handleSuccessResponse_limit(response)

    switch (res.code) {
      case 206:
        // 旧密码不正确
        break
      case 401:
        // 业务系统未登录,调用login接口登录
        return loginStatusExpiresHandler(response, request, service)
      case 402:
        // 登录失败
        gMessageWarning({
          content: '登录失败,请联系管理员',
        })
        break
      case 403:
        // 无权限,跳转到无权限页面
        gMessageWarning({
          content: res.msg || '权限不足',
        })
        break
      case 404:
        // 获取csrfToken,重新释放请求
        if (
          res.msg === '丢失服务器端颁发的CSRFTOKEN' ||
          res.msg === '请求中请携带颁发的CSRFTOKEN'
        ) {
          if (!isRefreshing) {
            isRefreshing = true
            // 请求token
            request({ url: '/csrfToken', baseURL }).then((data: any) => {
              if (data.code === 200) {
                // 遍历缓存队列 发起请求 传入最新token
                retryRequests.forEach(cb => cb())
                // 重试完清空这个队列
                retryRequests = []
              }
            })
          }
          return new Promise(resolve => {
            // 将resolve放进队列,用一个函数形式来保存,等token刷新后调用执行
            retryRequests.push(() => {
              resolve(service(response.config))
            })
          })
        }

        break
      case 500:
        // 服务器错误
        gMessageError({
          content: '服务器错误,请联系管理员',
        })
        return
    }

    return Promise.resolve(response)
  },
  error => {
    const { code, message } = error
    if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
      gMessageError({
        content: '接口请求超时,请刷新页面重试!',
      })
      return
    }
    const err = JSON.stringify(error)
    if (err && err.includes('Network Error')) {
      gMessageError({
        content: '网络异常,请检查您的网络连接是否正常!',
      })
      return
    }

    // http 状态码提示信息处理
    const isCancel = axios.isCancel(error)
    if (!isCancel) {
      checkResponseHttpStatus(error.response && error.response.status, message)
    }

    return handleErrorResponse_limit(error)
  }
)

// 接口响应成功
const handleSuccessResponse_limit = (response: any) => {
  const reqKey = response.config.pendKey
  if (pendingRequest.has(reqKey)) {
    let x = null
    try {
      x = JSON.parse(JSON.stringify(response))
    } catch (e) {
      x = response
    }
    pendingRequest.delete(reqKey)
    ev.emit(reqKey, x, 'resolve')
    delete ev.reqKey
  }
}

// 接口响应失败
const handleErrorResponse_limit = (error: { type: string; val: any; config: { pendKey: any } }) => {
  if (error.type && error.type === 'limitResSuccess') {
    return Promise.resolve(error.val)
  } else if (error.type && error.type === 'limitResError') {
    return Promise.reject(error.val)
  } else {
    const reqKey = error.config.pendKey
    if (pendingRequest.has(reqKey)) {
      let x = null
      try {
        x = JSON.parse(JSON.stringify(error))
      } catch (e) {
        x = error
      }
      pendingRequest.delete(reqKey)
      ev.emit(reqKey, x, 'reject')
      delete ev.reqKey
    }
  }
  return Promise.reject(error)
}

export default service

export const request = (config: AxiosRequestConfig, options?: IRequestOptions) => {
  return new Promise((resolve, reject) => {
    service(config)
      .then((response: AxiosResponse<IResult>) => {
        // 返回原始数据 包含http信息
        if (options?.isReturnNativeResponse) {
          resolve(response)
        }
        // 返回的接口信息
        const msg = response.data.msg
        // 是否显示成功信息
        if (options?.isShowSuccessMessage) {
          gMessageSuccess({
            content: options.successMessageText ?? msg ?? '操作成功',
          })
        }
        if (options?.isShowErrorMessage) {
          gMessageError({
            content: options.errorMessageText ?? msg ?? '操作失败',
          })
        }
        resolve(response.data)
      })
      .catch(error => {
        reject(error)
      })
  })
}

 httpEnum.ts

/**
 * @description: ContentType类型
 */
export enum ContentTypeEnum {
  // json
  JSON = 'application/json;charset=UTF-8',
  // json
  TEXT = 'text/plain;charset=UTF-8',
  // form-data 一般配合qs
  FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
  // form-data  上传
  FORM_DATA = 'multipart/form-data;charset=UTF-8',
}

/**
 * @description: 请求方法
 */
export enum MethodEnum {
  GET = 'GET',
  POST = 'POST',
  PATCH = 'PATCH',
  PUT = 'PUT',
  DELETE = 'DELETE',
}

 naiveMessage.ts 基于naive-ui分装提示

/*
 * @Author: LYM
 * @Date: 2023-03-28 08:47:39
 * @LastEditors: LYM
 * @LastEditTime: 2023-04-25 08:58:25
 * @Description: naive message提示
 */
import { createDiscreteApi, lightTheme, type ConfigProviderProps } from 'naive-ui'
import { computed } from 'vue'
import { IconWarningFill, IconInfoFill, IconCircleCloseFilled, IconSuccessFill } from '@/icons'

const configProviderPropsRef = computed<ConfigProviderProps>(() => ({
  theme: lightTheme,
}))

const { message } = createDiscreteApi(['message'], {
  configProviderProps: configProviderPropsRef,
})

// 警告
export const gMessageWarning = (params?: any) => {
  const {
    content = '这是一条message warning信息!',
    icon = IconWarningFill,
    duration = 5000,
  } = params || {}
  message.warning(content, {
    icon: () => h(icon, null),
    duration,
  })
}

// 成功
export const gMessageSuccess = (params?: any) => {
  const {
    content = '这是一条message success信息!',
    icon = IconSuccessFill,
    duration = 5000,
  } = params || {}
  message.success(content, {
    icon: () => h(icon, null),
    duration,
  })
}

// 失败
export const gMessageError = (params?: any) => {
  const {
    content = '这是一条message error信息!',
    icon = IconCircleCloseFilled,
    duration = 5000,
  } = params || {}
  message.error(content, {
    icon: () => h(icon, null),
    duration,
  })
}

// 信息
export const gMessageInfo = (params?: any) => {
  const {
    content = '这是一条message info信息!',
    icon = IconInfoFill,
    duration = 5000,
  } = params || {}
  message.info(content, {
    icon: () => h(icon, null),
    duration,
  })
}

// loading
export const gMessageLoading = (params?: any) => {
  const { content = '这是一条message Loading信息!', duration = 5000 } = params || {}
  message.loading(content, {
    duration,
  })
}

const gMessageObj = {
  info: {
    icon: IconInfoFill,
  },
  warning: {
    icon: IconWarningFill,
  },
  success: {
    icon: IconSuccessFill,
  },
  error: {
    icon: IconCircleCloseFilled,
  },
}

//  合并
export const gMessage = (params?: any) => {
  const { content = '这是一条message信息!', duration = 5000, type = 'info' } = params || {}
  message.create(content, {
    duration,
    type,
    icon: () => h(gMessageObj[type], null),
  })
}

checkResponseHttpStatus请求状态码收集处理---自行分装

loginStatusExpiresHandler登录过期或者token失效收集处理---自行分装

注意: 心跳、轮询等请求可以在入参中透传随机key值解决

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/499637.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Intel Arc显卡安装Stable Diffusion

StableDiffusion是一种基于深度学习的文本到图像生成模型&#xff0c;于2022年发布。它主要用于根据文本描述生成详细图像&#xff0c;也可应用于其他任务&#xff0c;如内补绘制、外补绘制和在提示词指导下生成图像翻译。通过给定文本提示词&#xff0c;该模型会输出一张匹配提…

C语言编译与链接

前言 我们想一个问题&#xff0c;我们写的C语言代码都是文本信息&#xff0c;电脑能直接执行c语言代码吗&#xff1f;肯定不能啊&#xff0c;计算机能执行的是二进制指令&#xff0c;所以将C语言转化为二进制指令需要一段过程&#xff0c;这篇博客讲一下编译与链接&#xff0c;…

Go打造REST Server【二】:用路由的三方库来实现

前言 在之前的文章中&#xff0c;我们用Go的标准库来实现了服务器&#xff0c;JSON渲染重构为辅助函数&#xff0c;使特定的路由处理程序相当简洁。 我们剩下的问题是路径路由逻辑&#xff0c;这是所有编写无依赖HTTP服务器的人都会遇到的问题&#xff0c;除非服务器只处理一到…

【计算机网络篇】数据链路层(4.2)可靠传输的实现机制

文章目录 &#x1f354;可靠传输的实现机制⭐停止 - 等待协议&#x1f5d2;️注意 &#x1f50e;停止 - 等待协议的信道利用率&#x1f5c3;️练习题 ⭐回退N帧协议&#x1f388;回退N帧协议的基本工作流程&#x1f50e;无传输差错的情况&#x1f50e;超时重传的情况&#x1f5…

Nomad Web更新没有最快只有更快

大家好&#xff0c;才是真的好。 很长时间没介绍运行在浏览器中的Notes客户端即Nomad Web更新情况。 不用安装&#xff0c;直接使用&#xff0c;还可以完美地兼容适应各种操作系统&#xff0c;Nomad Web一定是Notes/Domino产品现在和将来重点发展的用户访问模式。 不过&…

wsl kali在无缝模式下显示kali桌面的问题

Seamless mode shows the kali desktop 无缝模式下&#xff0c;同时显示kali的Panel和桌面 In Settings -> Session and Startup -> Current Session Change the “Restart Style” for the xfdesktop entry to “Never” Restart Win-KeX 高阶玩法 在Windows Te…

【MATLAB源码-第172期】基于matlab的小波变换能量率BP神经网络的机械轴承故障分析以及识别,附带程序说明。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 在现代工业生产中&#xff0c;轴承是最为常见和关键的机械基础部件之一&#xff0c;其性能状态直接影响着整个机械系统的稳定性和可靠性。由于轴承在运行过程中不断承受高负荷和摩擦&#xff0c;故障发生的概率相对较高。轴承…

ICLR2024:南洋理工发布!改几个参数就为大模型注入后门

随着大语言模型&#xff08;LLMs&#xff09;在处理自然语言处理&#xff08;NLP&#xff09;相关任务中的广泛应用&#xff0c;它们在人们日常生活中的作用日益凸显。例如&#xff0c;ChatGPT等模型已被用于各种文本生成、分类和情感分析任务。然而&#xff0c;这些模型潜在的…

HarmonyOS实战开发-如何实现一个支持加减乘除混合运算的计算器。

介绍 本篇Codelab基于基础组件、容器组件&#xff0c;实现一个支持加减乘除混合运算的计算器。 说明&#xff1a; 由于数字都是双精度浮点数&#xff0c;在计算机中是二进制存储数据的&#xff0c;因此小数和非安全整数&#xff08;超过整数的安全范围[-Math.pow(2, 53)&#…

如何使用Docker搭建WBO在线协作工具并实现无公网IP远程编辑本地白板

文章目录 前言1. 部署WBO白板2. 本地访问WBO白板3. Linux 安装cpolar4. 配置WBO公网访问地址5. 公网远程访问WBO白板6. 固定WBO白板公网地址 前言 WBO在线协作白板是一个自由和开源的在线协作白板&#xff0c;允许多个用户同时在一个虚拟的大型白板上画图。该白板对所有线上用…

使用mybatis的@Interceptor实现拦截sql

一 mybatis的拦截器 1.1 拦截器介绍 拦截器是一种基于 AOP&#xff08;面向切面编程&#xff09;的技术&#xff0c;它可以在目标对象的方法执行前后插入自定义的逻辑。 1.2 语法介绍 1.注解Intercepts Intercepts({Signature(type StatementHandler.class, method “…

electron+VUE Browserwindow与webview通信

仅做记录 前言&#xff1a; electronVUEVITE框架&#xff0c;用的是VUE3.0 主进程定义&#xff1a;用于接收webview发送的消息 ipcMain.on(MyWebviewMessage, (event, message) > {logger.info(收到webmsg message)//转发给渲染进程}) porelaod/webPreload.js定义 cons…

C语言结合体和枚举的魅力展现

前言 ✨✨欢迎&#x1f44d;&#x1f44d;点赞☕️☕️收藏✍✍评论 个人主页&#xff1a;秋邱’博客 所属栏目&#xff1a;人工智能 &#xff08;感谢您的光临&#xff0c;您的光临蓬荜生辉&#xff09; 引言: 前面我们已经讲了结构体的声明&#xff0c;自引用&#xff0c;内存…

C++ 前K个高频单词的六种解法

目录 大堆 小堆 vectorsort vectorstable_sort multimap set/multiset 与GPT的对话 1.对于比较类型中 < 运算符重载的理解 2.map有稳定性的说法吗 ​编辑 3.为什么map和set类的仿函数后面要加const来修饰*this 5.关于名词的理解 6.匿名对象对类要求 7.map和set的…

面向对象:继承

文章目录 一、什么叫继承&#xff1f;二、单继承三、多继承3.1多继承的各种情况3.1.1一般情况3.1.1特殊情况&#xff08;菱形继承&#xff09; 四、菱形继承引发的问题4.1 问题1:数据冗余4.2 问题2:二义性&#xff08;无法确定到底是访问哪个&#xff09; 五、虚拟继承解决菱形…

深度剖析鞋服品牌商品数字化管理的重要性

随着信息技术的迅猛发展与市场竞争的加剧&#xff0c;鞋服品牌商品数字化管理的重要性愈发凸显。数字化管理不仅关乎企业运营效率的提升&#xff0c;更是品牌实现差异化竞争、提升顾客体验、构建智慧零售生态的关键所在。对于鞋服品牌企业而言&#xff0c;提升商品数字化管理的…

python中raise_for_status方法的作用

文章目录 说明示例1&#xff1a;基本使用示例2&#xff1a;多种异常 说明 raise_for_status() 方法在 Python 的 requests 库中用于在发送 HTTP 请求后检查响应的状态码。如果响应的状态码表示请求未成功&#xff08;即状态码不是 2xx&#xff09;&#xff0c;则该方法会抛出一…

C/C++中重载函数取地址的方法

目录 1.现象 2.指定参数取函数地址 3.利用Qt的类QOverload 1.现象 函数重载在C/C编码中是非常常见的&#xff0c;但是我们在std::bind或std::function绑定函数地址的时候&#xff0c;直接取地址&#xff0c;程序编译就会报错&#xff0c;示例如下&#xff1a; class CFunc1…

【全套源码教程】基于SpringBoot+MyBatis框架的智慧生活商城系统的设计与实现

目录 前言 需求分析 可行性分析 技术实现 后端框架&#xff1a;Spring Boot 持久层框架&#xff1a;MyBatis 前端框架&#xff1a;Vue.js 数据库&#xff1a;MySQL 功能介绍 前台功能拓展 商品详情单管理 个人中心 秒杀活动 推荐系统 评论与评分系统 后台功能拓…

慢工之旅:婺源的故事

在当今这个快节奏、高竞争的时代&#xff0c;我们常常发现自己处于持续的忙碌和压力之中。然而&#xff0c;在今年春季&#xff0c;我们选择了一条不同的道路——一次团队旅行到江西婺源。这不仅是一场远离日常工作的旅行&#xff0c;而且成为了我们团队对工作、生活及寻求内心…
最新文章