老板要求:前端要在全局层面防止接口被重复请求

前言

有一天老板突然要求:前端要在全局层面防止接口被重复请求

原因很简单------有些用户点点点,疯狂点击按钮,就能薅到优惠券或者触发一些逻辑 bug。

其实最稳妥的办法应该是在 服务端限制,但老板偏偏要前端做。那没办法,咱就来研究下前端怎么搞。

因为老项目接口多如牛毛,一个个排查加 loading 不现实,只能来一套全局拦截方案


方案一:全屏 Loading

最容易想到的方案:

  • 请求拦截器里加一个全屏 Loading(转圈圈)。
  • 响应拦截器里关掉 Loading。

这样,用户点第二次的时候,看到 Loading 就不会继续点了。

缺点:

  • 很粗暴,体验不友好。
  • 有的接口自己本身就有局部 Loading,这么一套就会变成"双转圈",头皮发麻。

总结:能用,但很丑。


方案二:拦截相同请求,不让发出去

换个思路:

如果两个请求完全一模一样,那就拦掉后面的,不让它们发到后端。

关键点: 一个请求可以用这些东西来唯一标识:

  • 请求方法(GET/POST)
  • URL
  • 参数(params / data)
  • 页面地址(hash 或 pathname)

于是我们就写了个函数,把这些信息拼成一个 key:

js 复制代码
function generateReqKey(config, hash) {
  const { method, url, params, data } = config;
  return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}

思路很简单:

  • 请求前,把 key 存到一个集合里。
  • 如果下一个请求的 key 已经存在,说明它是重复的 → 拦掉。
  • 等第一个请求返回了,就把 key 从集合里移除。

问题来了:

  1. 如果多个请求被拦掉,但后续逻辑里有错误处理,就会弹出一堆报错提示(用户体验极差)。
  2. 如果同一个页面的两个组件都要调同一个接口,后发的请求就拿不到数据了 → 页面挂了。

总结:思路没错,但坑很多。


方案三:挂起相同请求,结果共享(最终方案)

那我们能不能这样:

  • 第一个请求照常发出去。
  • 后面相同的请求,不是真的发,而是先挂起
  • 等第一个请求返回结果(成功/失败),再把结果共享给这些挂起的请求。

这样:

  • 不会重复打接口。
  • 后续的请求也能拿到结果。
  • 用户体验正常。
js 复制代码
import axios from "axios";

// 创建 axios 实例
let instance = axios.create({
  baseURL: "/api/" // 这里你可以换成自己的接口前缀
});

/**
 * 发布订阅类(EventEmitter)
 * - 用来存放"挂起的请求"
 * - 第一个请求成功/失败时,把结果广播给挂起的请求
 */
class EventEmitter {
  constructor() {
    this.event = {}; // 用来存放事件和对应的回调函数
  }

  // 订阅(挂起的请求)
  on(type, cbResolve, cbReject) {
    if (!this.event[type]) {
      this.event[type] = [[cbResolve, cbReject]];
    } else {
      this.event[type].push([cbResolve, cbReject]);
    }
  }

  // 发布(当请求返回时,把结果广播给所有订阅者)
  emit(type, res, ansType) {
    if (!this.event[type]) return;
    this.event[type].forEach(cbArr => {
      if (ansType === "resolve") {
        cbArr[0](res); // 通知成功的订阅者
      } else {
        cbArr[1](res); // 通知失败的订阅者
      }
    });
  }
}

实现方式:发布订阅模式

我们写了一个简单的 EventEmitter

  • on → 把"挂起的请求"登记到订阅列表里。
  • emit → 第一个请求返回时,把结果广播给订阅者。

在请求拦截器里:

  • 如果发现请求 key 已经存在,就挂起它,等结果回来。
  • 如果是新请求,就发出去。

在响应拦截器里:

  • 请求完成时,通知所有订阅的挂起请求,返回结果。

👉 完整代码在 Demo 地址


特殊情况:文件上传

写完以为万事大吉,结果发现上传文件的时候有 bug:

  • 我传两个不同文件,但接口只发了一次。

原因是:

  • 请求体里的 FormDataJSON.stringify 转成了 {}
  • 所以不同的文件生成的 key 是一样的,被误判成重复请求。

解决办法: 在生成 key 时,先判断是不是文件上传:

js 复制代码
function isFileUploadApi(config) {
  return Object.prototype.toString.call(config.data) === "[object FormData]";
}

如果是文件上传,就直接放行,不再做重复请求拦截。


总结

三种方案对比:

方案 思路 优点 缺点
方案一 全局 Loading 简单 粗暴、体验差
方案二 相同请求直接拦截 节省接口 容易误伤,逻辑混乱
方案三 相同请求挂起 + 结果共享 最优解,体验好 代码复杂,需要维护

最终采用的是 方案三(挂起相同请求,结果共享),再配合文件上传的特殊处理,基本覆盖了线上场景。

这样一来,不用逐个接口改代码,省心省力,老板也满意。


后记

这个功能看似是"前端优化",但本质上还是 前端兜底,真正要防止薅羊毛,还是得后端加限流和幂等处理。

前端这套更像是:

  • 避免重复请求浪费资源
  • 提升用户体验
  • 减少一些潜在的 bug
相关推荐
yes or ok5 分钟前
前端工程师面试题-vue
前端·javascript·vue.js
我要成为前端高手20 分钟前
给不支持摇树的三方库(phaser) tree-shake?
前端·javascript
Noxi_lumors35 分钟前
VITE BALABALA require balabla not supported
前端·vite
周胜237 分钟前
node-sass
前端
aloha_43 分钟前
Windows 系统中,杀死占用某个端口(如 8080)的进程
前端
牧野星辰44 分钟前
让el-table长个小脑袋,记住我的滚动位置
前端·javascript·element
code_YuJun1 小时前
React 常见问题
前端
_Congratulate1 小时前
vue3高德地图api整合封装(自定义撒点、轨迹等)
前端·javascript·vue.js
用户904706683571 小时前
TL如何进行有效的CR
前端
富婆苗子1 小时前
关于wangeditor的自定义组件和元素
前端·javascript