javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JavaScript解决重复请求

前端JavaScript彻底解决重复请求问题的五种方案

作者:南城FE

在前端开发中,重复请求是一个常见且棘手的问题,比如用户快速点击保存按钮导致生成多条重复单据,或者列表页频繁刷新造成服务器压力飙升,本文将系统梳理重复请求的解决方案,从基础到进阶进行对比分析,并结合实际代码案例解决这一痛点,需要的朋友可以参考下

引言

在前端开发中,重复请求是一个常见且棘手的问题。比如用户快速点击"保存"按钮导致生成多条重复单据,或者列表页频繁刷新造成服务器压力飙升,这些场景不仅影响用户体验,还可能引发数据一致性问题。本文将系统梳理重复请求的解决方案,从基础到进阶进行对比分析,并结合实际代码案例解决这一痛点。

一、重复请求不止是"多花钱"

在讨论解决方案前,我们先明确重复请求的具体影响,避免因"觉得问题不大"而忽视它:

了解危害后,我们来看当前主流的解决方案,及其适用场景和优缺点。

二、5种重复请求解决方案对比

方案1:UI层面控制(最简单但不彻底)

这是最基础的解决方案,通过控制UI交互阻止重复触发请求,核心思路是"让用户无法重复点击"。

实现方式

代码示例(React)

const SaveButton = () => {  
 const [loading, setLoading] = useState(false);  
  
 const handleSave = async () => {  
 if (loading) return; // 防止重复触发  
 setLoading(true);  
 try {  
 await api.submitForm(data);  
 message.success("保存成功");  
 } catch (error) {  
 message.error("保存失败");  
 } finally {  
 setLoading(false); // 请求完成后恢复按钮状态  
 }  
 };  
  
 return <Button loading={loading} onClick={handleSave}>保存</Button>;  
};  

优缺点分析

实现简单,无额外依赖
对现有代码侵入性低
即时反馈,提升用户体验

无法覆盖所有场景(如代码层面直接调用接口)
多个组件调用同一接口时,无法共享状态
无法处理网络延迟导致的"隐性重复请求"

适用场景

方案2:请求拦截器+缓存(适合读操作)

对于查询类接口(如列表查询、详情获取),可通过"请求拦截器+缓存"实现重复请求拦截,核心思路是"相同请求只发一次,结果缓存复用"。

实现原理

代码示例(Axios拦截器)

import axios from 'axios';  
import { sha256 } from 'js-sha256';  
  
// 缓存容器:key=请求唯一标识,value=请求Promise  
const requestCache = new Map();  
  
// 创建Axios实例  
const service = axios.create({  
 baseURL: import.meta.env.VITE_API_BASE_URL,  
 timeout: 5000  
});  
  
// 请求拦截器  
service.interceptors.request.use(  
 (config) => {  
 // 1. 生成请求唯一标识(URL+方法+参数)  
 const requestKey = generateRequestKey(config);  
   
 // 2. 检查缓存:若存在未完成的请求,直接返回缓存的Promise  
 if (requestCache.has(requestKey)) {  
 return requestCache.get(requestKey);  
 }  
   
 // 3. 若不存在缓存,发送请求并缓存Promise  
 const requestPromise = Promise.resolve(config);  
 requestCache.set(requestKey, requestPromise);  
 return requestPromise;  
 },  
 (error) => Promise.reject(error)  
);  
  
// 响应拦截器  
service.interceptors.response.use(  
 (response) => {  
 // 请求完成,清除缓存  
 const requestKey = generateRequestKey(response.config);  
 requestCache.delete(requestKey);  
 return response.data;  
 },  
 (error) => {  
 // 请求失败,同样清除缓存(避免缓存失败状态)  
 if (error.config) {  
 const requestKey = generateRequestKey(error.config);  
 requestCache.delete(requestKey);  
 }  
 return Promise.reject(error);  
 }  
);  
  
// 生成请求唯一标识:基于URL、方法、params、data的哈希值  
function generateRequestKey(config) {  
 const { url, method, params, data } = config;  
 const requestStr = JSON.stringify({ url, method, params, data });  
 // 使用sha256生成哈希值,确保唯一性  
 return sha256(requestStr);  
}  
  
export default service;  

优缺点分析

优点:
对业务代码无侵入,全局生效
减少重复请求,减轻服务器压力
支持多组件共享请求结果

缺点:
不适合写操作(如新增/修改/删除),可能导致数据更新不及时
缓存有效期难控制,需手动处理过期逻辑
无法处理请求取消场景

适用场景

方案3:请求取消+状态管理(适合写操作)

对于写操作接口(如新增、修改、删除),不能使用缓存(需确保每次请求都能触达服务器),此时需通过"请求取消+状态管理"实现重复拦截,核心思路是"相同写请求同时只能存在一个,重复请求直接取消"。

实现原理

代码示例(结合AbortController)

import axios from 'axios';  
import { sha256 } from 'js-sha256';  
  
// 管理未完成的写请求:key=请求唯一标识,value=AbortController  
const pendingWriteRequests = new Map();  
  
// 写请求专用Axios实例  
const writeService = axios.create({  
 baseURL: import.meta.env.VITE_API_BASE_URL,  
 timeout: 5000  
});  
  
// 发起写请求(如POST/PUT/DELETE)  
export function sendWriteRequest(config) {  
 // 1. 生成请求唯一标识  
 const requestKey = generateRequestKey(config);  
   
 // 2. 检查是否存在未完成的相同请求:若有,取消新请求  
 if (pendingWriteRequests.has(requestKey)) {  
 const newController = new AbortController();  
 // 取消新请求  
 newController.abort('重复请求已取消');  
 return Promise.reject(new Error('重复请求已取消'));  
 }  
   
 // 3. 创建AbortController,用于取消请求  
 const controller = new AbortController();  
 const newConfig = {  
 ...config,  
 signal: controller.signal // 绑定取消信号  
 };  
   
 // 4. 将请求标识和取消控制器存入容器  
 pendingWriteRequests.set(requestKey, controller);  
   
 // 5. 发送请求,完成后清除容器  
 return writeService(newConfig)  
 .then((response) => {  
 pendingWriteRequests.delete(requestKey);  
 return response.data;  
 })  
 .catch((error) => {  
 pendingWriteRequests.delete(requestKey);  
 // 过滤"主动取消"的错误,避免业务层处理  
 if (error.name === 'AbortError') {  
 console.log('请求已取消:', requestKey);  
 return Promise.reject(new Error('请求已取消'));  
 }  
 return Promise.reject(error);  
 });  
}  
  
// 生成请求唯一标识(同方案2)  
function generateRequestKey(config) {  
 const { url, method, params, data } = config;  
 const requestStr = JSON.stringify({ url, method, params, data });  
 return sha256(requestStr);  
}  
  
// 手动取消指定请求(如页面卸载时)  
export function cancelWriteRequest(config) {  
 const requestKey = generateRequestKey(config);  
 if (pendingWriteRequests.has(requestKey)) {  
 const controller = pendingWriteRequests.get(requestKey);  
 controller.abort('手动取消请求');  
 pendingWriteRequests.delete(requestKey);  
 }  
}  
  
export default writeService;  

优缺点分析

优点:
适合写操作,确保数据一致性
支持手动取消(如页面卸载)
避免重复写请求导致的数据问题

缺点:
实现较复杂,需手动管理取消逻辑
对业务代码有一定侵入性(需使用专用请求函数)
无法复用请求结果,每次请求都需触达服务器

适用场景

方案4:订阅-发布模式(多订阅者共享请求结果)

当多个组件同时调用同一接口时,可通过"订阅-发布模式"实现"一次请求,多端复用",核心思路是"相同请求只发送一次,结果分发给所有订阅者",这也是参考范文中采用的核心方案。

实现原理

代码示例(基于参考范文封装)

import axios from 'axios';  
import { sha256 } from 'js-sha256';  
  
class RequestSubscriber {  
 // 容器:key=请求唯一标识,value={ promise: 请求Promise, subscribers: 订阅者列表 }  
 constructor() {  
 this.requestStore = new Map();  
 this.instance = axios.create({  
 baseURL: import.meta.env.VITE_API_BASE_URL,  
 timeout: 5000  
 });  
 }  
  
 // 发起请求(订阅)  
 request(config) {  
 const requestKey = this.generateRequestKey(config);  
 const storeItem = this.requestStore.get(requestKey);  
  
 // 1. 若请求已存在,添加订阅者  
 if (storeItem) {  
 return new Promise((resolve, reject) => {  
 storeItem.subscribers.push({ resolve, reject });  
 });  
 }  
  
 // 2. 若请求不存在,创建请求并订阅  
 const subscribers = [];  
 const controller = new AbortController();  
 const newConfig = { ...config, signal: controller.signal };  
  
 // 创建请求Promise  
 const requestPromise = this.instance(newConfig)  
 .then((response) => {  
 // 请求成功,通知所有订阅者  
 this.notifySubscribers(requestKey, 'resolve', response.data);  
 return response.data;  
 })  
 .catch((error) => {  
 // 请求失败,通知所有订阅者  
 this.notifySubscribers(requestKey, 'reject', error);  
 return Promise.reject(error);  
 })  
 .finally(() => {  
 // 请求完成,清除容器  
 this.requestStore.delete(requestKey);  
 });  
  
 // 存入容器  
 this.requestStore.set(requestKey, {  
 promise: requestPromise,  
 subscribers,  
 controller  
 });  
  
 // 返回当前订阅的Promise  
 return new Promise((resolve, reject) => {  
 subscribers.push({ resolve, reject });  
 });  
 }  
  
 // 通知所有订阅者  
 notifySubscribers(requestKey, type, data) {  
 const storeItem = this.requestStore.get(requestKey);  
 if (!storeItem) return;  
  
 storeItem.subscribers.forEach((subscriber) => {  
 subscriber[type](data);  
 });  
 }  
  
 // 取消请求(如组件卸载)  
 cancelRequest(config) {  
 const requestKey = this.generateRequestKey(config);  
 const storeItem = this.requestStore.get(requestKey);  
 if (storeItem) {  
 // 取消请求  
 storeItem.controller.abort('请求已取消');  
 // 清除容器  
 this.requestStore.delete(requestKey);  
 }  
 }  
  
 // 生成请求唯一标识  
 generateRequestKey(config) {  
 const { url, method, params, data } = config;  
 const requestStr = JSON.stringify({ url, method, params, data });  
 return sha256(requestStr).slice(0, 40); // 截取前40位,平衡唯一性和长度  
 }  
}  
  
// 单例模式:确保全局只有一个实例  
export const requestSubscriber = new RequestSubscriber();  

优缺点分析

优点:
多组件共享请求结果,减少请求次数
支持请求取消,避免内存泄漏
兼顾读操作和写操作(写操作可关闭共享)

缺点:
实现复杂,需维护订阅者列表和请求状态
调试难度高,需跟踪订阅者和请求状态
对新手不友好,需理解订阅-发布模式

适用场景

方案5:后端配合拦截(最彻底的方案)

前端方案虽能解决大部分场景,但仍存在"极端情况漏洞"(如网络延迟导致的请求绕过前端拦截),此时需后端配合,从源头拦截重复请求,核心思路是"后端基于唯一标识判断是否为重复请求"。

实现原理

代码示例(前后端配合)

前端部分

import axios from 'axios';  
import { v4 as uuidv4 } from 'uuid';  
  
const service = axios.create({  
 baseURL: import.meta.env.VITE_API_BASE_URL,  
 timeout: 5000  
});  
  
// 请求拦截器:添加唯一请求ID  
service.interceptors.request.use(  
 (config) => {  
 // 生成唯一请求ID(UUID)  
 const requestId = uuidv4();  
 // 存入请求头  
 config.headers['X-Request-ID'] = requestId;  
 // 存入localStorage,用于后续重复请求判断(可选)  
 localStorage.setItem(`request_${requestId}`, 'pending');  
 return config;  
 },  
 (error) => Promise.reject(error)  
);  
  
// 响应拦截器:处理重复请求错误  
service.interceptors.response.use(  
 (response) => {  
 const requestId = response.config.headers['X-Request-ID'];  
 // 请求完成,删除localStorage中的标识  
 localStorage.removeItem(`request_${requestId}`);  
 return response.data;  
 },  
 (error) => {  
 if (error.response?.data?.code === 'DUPLICATE_REQUEST') {  
 // 后端返回重复请求错误,提示用户  
 message.warning('请勿重复操作');  
 const requestId = error.config.headers['X-Request-ID'];  
 localStorage.removeItem(`request_${requestId}`);  
 return Promise.reject(new Error('重复请求已拦截'));  
 }  
 return Promise.reject(error);  
 }  
);  
  
export default service;  
``

**后端部分(Node.js + Redis)**:

``const express = require('express');  
const redis = require('redis');  
const { v4: uuidv4 } = require('uuid');  
  
const app = express();  
const redisClient = redis.createClient({  
 url: process.env.REDIS_URL  
});  
redisClient.connect();  
  
// 重复请求拦截中间件  
app.use(async (req, res, next) => {  
 const requestId = req.headers['x-request-id'];  
 if (!requestId) {  
 return res.status(400).json({ code: 'INVALID_REQUEST', message: '缺少请求ID' });  
 }  
  
 // 检查Redis中是否存在该请求ID  
 const exists = await redisClient.exists(`request:${requestId}`);  
 if (exists) {  
 // 已存在,判定为重复请求  
 return res.status(400).json({ code: 'DUPLICATE_REQUEST', message: '重复请求已拦截' });  
 }  
  
 // 不存在,存入Redis(设置5秒过期,避免内存泄漏)  
 await redisClient.setEx(`request:${requestId}`, 5, 'pending');  
 next();  
});  
  
// 业务接口  
app.post('/api/submit-form', (req, res) => {  
 // 处理表单提交逻辑  
 res.json({ code: 'SUCCESS', message: '提交成功' });  
});  
  
app.listen(3000, () => {  
 console.log('Server running on port 3000');  
});  

优缺点分析

优点:
从源头拦截重复请求,最彻底
不受前端环境影响(如多标签页、多设备)
支持分布式系统,可跨服务判断重复请求

缺点:
需后端配合,增加后端开发成本
依赖Redis等存储服务,增加部署复杂度
需处理请求ID的过期逻辑,避免存储膨胀

适用场景

三、推荐组合方案实现"彻底解决"

单一方案无法覆盖所有场景,实际项目中建议采用"组合方案",兼顾性能、体验和数据一致性:

通过这种"三层防护",可彻底解决前端重复请求问题,同时兼顾开发效率和系统稳定性。

以上就是前端JavaScript彻底解决重复请求问题的五种方案的详细内容,更多关于JavaScript解决重复请求的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文