在前端開發中,重複請求是一個常見且棘手的問題。比如用户快速點擊"保存"按鈕導致生成多條重複單據,或者列表頁頻繁刷新造成服務器壓力飆升,這些場景不僅影響用户體驗,還可能引發數據一致性問題。本文將系統梳理重複請求的解決方案,從基礎到進階進行對比分析,並結合實際代碼案例解決這一痛點。
一、重複請求不止是"多花錢"
在討論解決方案前,我們先明確重複請求的具體影響,避免因"覺得問題不大"而忽視它:
- 數據一致性風險:如表單重複提交導致生成多個相同訂單、重複創建用户,後續需要額外成本修復數據
- 服務器資源浪費:相同請求反覆發送,佔用帶寬和服務器算力,極端情況下可能引發服務過載
- 前端體驗降級:重複請求可能導致頁面多次渲染閃爍,或觸發多次錯誤提示
- 網絡資源消耗:尤其在移動端,重複請求會浪費用户流量,增加加載時間
瞭解危害後,我們來看當前主流的解決方案,及其適用場景和優缺點。
二、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:請求攔截器+緩存(適合讀操作)
對於查詢類接口(如列表查詢、詳情獲取),可通過"請求攔截器+緩存"實現重複請求攔截,核心思路是"相同請求只發一次,結果緩存複用"。
實現原理
- 定義緩存容器(如Map),存儲已發送但未完成的請求Promise
- 發起請求前,生成請求唯一標識(如URL+參數+方法的哈希值)
- 若緩存中存在該請求的Promise,直接返回緩存的Promise;若不存在,發送請求並將Promise存入緩存
- 請求完成(成功/失敗)後,清除緩存,確保下次請求可正常發起
代碼示例(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(或CancelToken),將取消函數和請求標識存入容器
- 請求完成(成功/失敗)或取消後,從容器中移除該請求標識
代碼示例(結合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:訂閲-發佈模式(多訂閲者共享請求結果)
當多個組件同時調用同一接口時,可通過"訂閲-發佈模式"實現"一次請求,多端複用",核心思路是"相同請求只發送一次,結果分發給所有訂閲者",這也是參考範文中採用的核心方案。
實現原理
- 維護一個請求狀態容器:key=請求唯一標識,value=訂閲者列表+請求Promise
- 組件發起請求時,生成請求唯一標識,檢查容器:
- 若請求已存在(未完成):將當前組件的回調函數加入訂閲者列表
- 若請求不存在:發送請求,將Promise存入容器,並添加當前組件的訂閲者
- 請求完成後,遍歷訂閲者列表,將結果分發給所有訂閲者
- 訂閲者取消訂閲(如組件卸載)時,從訂閲者列表中移除自身
代碼示例(基於參考範文封裝)
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:後端配合攔截(最徹底的方案)
前端方案雖能解決大部分場景,但仍存在"極端情況漏洞"(如網絡延遲導致的請求繞過前端攔截),此時需後端配合,從源頭攔截重複請求,核心思路是"後端基於唯一標識判斷是否為重複請求"。
實現原理
- 前端發起請求時,生成一個唯一標識(如UUID),存入請求頭(如
X-Request-ID) - 後端接收到請求後,檢查
X-Request-ID:
- 若Redis中不存在該ID:處理請求,並將ID存入Redis(設置過期時間,如5秒)
- 若Redis中已存在該ID:判定為重複請求,直接返回"重複請求"錯誤
- 前端接收到"重複請求"錯誤後,提示用户或忽略該響應
代碼示例(前後端配合)
前端部分:
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的過期邏輯,避免存儲膨脹
適用場景
- 對數據一致性要求極高的場景(如支付、訂單創建)
- 大型分佈式系統,前端攔截無法覆蓋所有場景
三、推薦組合方案實現"徹底解決"
單一方案無法覆蓋所有場景,實際項目中建議採用"組合方案",兼顧性能、體驗和數據一致性:
- 基礎層:方案1(UI控制)+ 方案2(緩存)
- 所有按鈕點擊添加loading狀態,防止重複觸發
- 所有讀操作接口添加緩存,減少服務器壓力
- 核心層:方案3(請求取消)+ 方案4(訂閲-發佈)
- 所有寫操作接口添加請求取消邏輯,避免重複提交
- 多組件共享的接口使用訂閲-發佈模式,提升性能
- 保障層:方案5(後端配合)
- 核心業務接口(如支付、訂單)添加後端重複攔截
- 前端傳遞唯一請求ID,後端基於Redis判斷重複
通過這種"三層防護",可徹底解決前端重複請求問題,同時兼顧開發效率和系統穩定性。