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

前言

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

原因很简单------有些用户点点点,疯狂点击按钮,就能薅到优惠券或者触发一些逻辑 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:

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

原因是:

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

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

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

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


总结

三种方案对比:

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

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

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


后记

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

前端这套更像是:

  • 避免重复请求浪费资源
  • 提升用户体验
  • 减少一些潜在的 bug
相关推荐
Jmyd01233 小时前
Web端3D营销展厅技术实现路径:从素材上云到线索回流的数据链路
前端·3d·性能优化·三维数字化
砚凝霜3 小时前
【软考信息安全】第二十二章 Web服务器安全分析与增强——Apache与IIS
服务器·前端·安全
u0111026755 小时前
JPG、PNG、WebP 怎么选?按透明度、体积和使用场景做判断
前端·javascript·vue.js·图像处理·ai作画
jerrywus6 小时前
让 AI 写完代码后交一份报告:我改造了 answer-me-with-html
前端·claude·cursor
派小心.6 小时前
小程序分享链路中来源参数的传递校验
大数据·前端·数据库·人工智能·小程序·数据分析
hhb_6187 小时前
一键生成数据看板,ECharts实战演示
前端·javascript·echarts
Java后端的Ai之路7 小时前
08_React组件API
前端·react.js·前端框架
小马不起床7 小时前
零基础玩转 FastAPI_09:前端基础工程构建
前端·状态模式·fastapi
风骏时光牛马8 小时前
用户管理模块接口开发
前端
彭亚川Allen8 小时前
从零手搓 RAG 之检索全流程,给大模型接上自己的知识库
前端·后端·ai编程