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

前言

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

原因很简单------有些用户点点点,疯狂点击按钮,就能薅到优惠券或者触发一些逻辑 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
相关推荐
子兮曰2 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰2 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万2 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝2 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋2 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁2 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95272 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大2 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师2 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学2 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端