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

前言

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

原因很简单------有些用户点点点,疯狂点击按钮,就能薅到优惠券或者触发一些逻辑 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
相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax