前端防止接口重復請求的四種方案
一、常見方案分析
1.1 Loading狀態(tài)方案
// 方案1: 按鈕Loading狀態(tài)
class RequestController {
constructor() {
this.loading = false;
}
async fetchData(params) {
if (this.loading) {
console.log('請求正在進行中,請稍后...');
return;
}
try {
this.loading = true;
// 顯示loading UI
this.showLoading();
const response = await axios.get('/api/data', { params });
return response.data;
} catch (error) {
console.error('請求失敗:', error);
throw error;
} finally {
this.loading = false;
// 隱藏loading UI
this.hideLoading();
}
}
showLoading() {
// 顯示全局loading或按鈕loading
}
hideLoading() {
// 隱藏loading
}
}
優(yōu)點:
- 實現(xiàn)簡單,用戶體驗直觀
- 能有效防止重復點擊
- 適用于按鈕點擊場景
缺點:
- 無法防止并行多個不同請求
- 需要手動管理loading狀態(tài)
- 頁面多個按鈕需要分別處理
1.2 請求標記方案
// 方案2: 請求唯一標識
class RequestMarker {
constructor() {
this.pendingRequests = new Map();
}
async request(url, config = {}) {
const requestKey = this.generateKey(url, config.params || config.data);
// 檢查是否已有相同請求
if (this.pendingRequests.has(requestKey)) {
// 可選:返回已有請求的Promise
return this.pendingRequests.get(requestKey);
}
// 創(chuàng)建新的請求
const requestPromise = axios({
url,
...config,
}).finally(() => {
// 請求完成后移除標記
this.pendingRequests.delete(requestKey);
});
// 存儲請求
this.pendingRequests.set(requestKey, requestPromise);
return requestPromise;
}
generateKey(url, params) {
const sortedParams = params ?
Object.keys(params).sort().map(key => `${key}=${params[key]}`).join('&') : '';
return `${url}?${sortedParams}`;
}
// 手動取消特定請求
cancelRequest(url, params) {
const key = this.generateKey(url, params);
if (this.pendingRequests.has(key)) {
// 實際項目中可能需要使用axios的cancel token
this.pendingRequests.delete(key);
}
}
}
優(yōu)點:
- 精確控制重復請求
- 可復用已有請求Promise
- 支持請求取消
缺點:
- 實現(xiàn)相對復雜
- 需要維護請求映射表
- 內(nèi)存占用需要考慮
1.3 防抖/節(jié)流方案
// 方案3: 防抖請求
class DebounceRequest {
constructor(wait = 500) {
this.timer = null;
this.lastRequestTime = 0;
}
// 防抖版本
debounceRequest(url, params, wait = 500) {
return new Promise((resolve, reject) => {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(async () => {
try {
const response = await axios.get(url, { params });
resolve(response.data);
} catch (error) {
reject(error);
}
}, wait);
});
}
// 節(jié)流版本
throttleRequest(url, params, interval = 1000) {
return new Promise((resolve, reject) => {
const now = Date.now();
if (now - this.lastRequestTime < interval) {
console.log('請求過于頻繁');
reject(new Error('請求過于頻繁'));
return;
}
this.lastRequestTime = now;
axios.get(url, { params })
.then(response => resolve(response.data))
.catch(error => reject(error));
});
}
}
優(yōu)點:
- 有效控制請求頻率
- 減少服務器壓力
- 適用于搜索框等高頻場景
缺點:
- 可能延遲必要請求
- 不適合所有場景
- 需要合理設置時間間隔
1.4 請求攔截器方案
// 方案4: 攔截器全局控制
class AxiosInterceptor {
constructor() {
this.pendingRequests = new Map();
this.setupInterceptors();
}
setupInterceptors() {
// 請求攔截器
axios.interceptors.request.use(config => {
// 生成請求唯一標識
const requestKey = this.generateRequestKey(config);
// 取消重復請求
if (this.pendingRequests.has(requestKey)) {
config.cancelToken = new axios.CancelToken(cancel => {
cancel('重復請求,已取消');
});
} else {
// 存儲cancel函數(shù)
config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
this.pendingRequests.set(requestKey, cancel);
});
}
return config;
});
// 響應攔截器
axios.interceptors.response.use(
response => {
// 請求完成后移除
const requestKey = this.generateRequestKey(response.config);
this.pendingRequests.delete(requestKey);
return response;
},
error => {
if (axios.isCancel(error)) {
console.log('請求被取消:', error.message);
return Promise.reject(error);
}
// 錯誤時也移除請求
if (error.config) {
const requestKey = this.generateRequestKey(error.config);
this.pendingRequests.delete(requestKey);
}
return Promise.reject(error);
}
);
}
generateRequestKey(config) {
const { method, url, params, data } = config;
let key = `${method}-${url}`;
if (params) {
key += `-${JSON.stringify(params)}`;
}
if (data) {
key += `-${JSON.stringify(data)}`;
}
return key;
}
// 取消所有pending請求
cancelAllRequests() {
this.pendingRequests.forEach(cancel => {
cancel();
});
this.pendingRequests.clear();
}
}
優(yōu)點:
- 全局統(tǒng)一處理
- 與業(yè)務代碼解耦
- 支持精細化控制
缺點:
- 配置復雜
- 可能影響正常請求
- 需要處理邊界情況
1.5 React Hooks方案
// 方案5: React自定義Hook
import { useRef, useState, useCallback } from 'react';
function usePreventDuplicateRequest() {
const [loading, setLoading] = useState(false);
const requestRef = useRef(null);
const pendingRequests = useRef(new Map());
// 帶loading的請求
const requestWithLoading = useCallback(async (requestFn, ...args) => {
if (loading) {
console.log('已有請求在進行中');
return;
}
setLoading(true);
try {
const result = await requestFn(...args);
return result;
} finally {
setLoading(false);
}
}, [loading]);
// 防重復請求
const requestWithPrevention = useCallback(async (key, requestFn, ...args) => {
// 檢查是否有相同請求
if (pendingRequests.current.has(key)) {
console.log('重復請求,使用緩存');
return pendingRequests.current.get(key);
}
try {
const requestPromise = requestFn(...args);
pendingRequests.current.set(key, requestPromise);
const result = await requestPromise;
return result;
} finally {
pendingRequests.current.delete(key);
}
}, []);
// 防抖請求
const debounceRequest = useCallback((requestFn, delay = 500) => {
return debounce(requestFn, delay);
}, []);
return {
loading,
requestWithLoading,
requestWithPrevention,
debounceRequest,
};
}
// 使用示例
function SearchComponent() {
const { loading, requestWithLoading } = usePreventDuplicateRequest();
const handleSearch = async (keyword) => {
return requestWithLoading(
() => axios.get('/api/search', { params: { q: keyword } })
);
};
return (
<div>
<button
onClick={handleSearch}
disabled={loading}
>
{loading ? '搜索中...' : '搜索'}
</button>
</div>
);
}
二、綜合方案建議
推薦實現(xiàn):分層防重復策略
// 綜合方案:分層防重復
class ComprehensiveRequestManager {
constructor(options = {}) {
this.options = {
enableLoading: true,
enableDebounce: true,
enableRequestDedupe: true,
debounceTime: 300,
...options
};
this.loadingStates = new Map();
this.pendingRequests = new Map();
this.debounceTimers = new Map();
}
// 核心請求方法
async request(config) {
const {
url,
method = 'GET',
params,
data,
requestId,
showLoading = true,
useDebounce = false,
} = config;
// 1. 生成請求唯一標識
const requestKey = requestId || this.generateRequestKey({ url, method, params, data });
// 2. 防抖處理
if (useDebounce && this.options.enableDebounce) {
return this.debounceRequest(requestKey, () =>
this.executeRequest(requestKey, config, showLoading)
);
}
// 3. 直接執(zhí)行請求
return this.executeRequest(requestKey, config, showLoading);
}
// 執(zhí)行請求
async executeRequest(requestKey, config, showLoading) {
// 檢查重復請求
if (this.options.enableRequestDedupe && this.pendingRequests.has(requestKey)) {
console.log(`請求 ${requestKey} 已存在,返回已有Promise`);
return this.pendingRequests.get(requestKey);
}
// 顯示loading
if (showLoading && this.options.enableLoading) {
this.setLoading(requestKey, true);
}
try {
// 創(chuàng)建請求Promise
const requestPromise = axios({
...config,
cancelToken: new axios.CancelToken(cancel => {
// 存儲cancel函數(shù)
this.pendingRequests.set(requestKey, { promise: requestPromise, cancel });
})
});
const response = await requestPromise;
return response.data;
} catch (error) {
if (!axios.isCancel(error)) {
console.error('請求失敗:', error);
}
throw error;
} finally {
// 清理工作
this.pendingRequests.delete(requestKey);
if (showLoading && this.options.enableLoading) {
this.setLoading(requestKey, false);
}
}
}
// 防抖請求
debounceRequest(key, requestFn) {
return new Promise((resolve, reject) => {
// 清除已有定時器
if (this.debounceTimers.has(key)) {
clearTimeout(this.debounceTimers.get(key));
}
// 設置新定時器
const timer = setTimeout(async () => {
try {
const result = await requestFn();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.debounceTimers.delete(key);
}
}, this.options.debounceTime);
this.debounceTimers.set(key, timer);
});
}
// 工具方法
generateRequestKey(config) {
const { url, method, params, data } = config;
return `${method.toUpperCase()}:${url}:${JSON.stringify(params)}:${JSON.stringify(data)}`;
}
setLoading(key, isLoading) {
this.loadingStates.set(key, isLoading);
// 觸發(fā)loading狀態(tài)變化事件
this.onLoadingChange(key, isLoading);
}
onLoadingChange(key, isLoading) {
// 可以在這里實現(xiàn)loading UI更新
console.log(`Loading狀態(tài)變化: ${key} -> ${isLoading}`);
}
// 取消請求
cancelRequest(key) {
if (this.pendingRequests.has(key)) {
const { cancel } = this.pendingRequests.get(key);
cancel(`手動取消請求: ${key}`);
this.pendingRequests.delete(key);
}
}
// 取消所有請求
cancelAllRequests() {
this.pendingRequests.forEach(({ cancel }, key) => {
cancel(`取消所有請求: ${key}`);
});
this.pendingRequests.clear();
}
}
三、使用建議和總結(jié)
3.1 方案選擇建議

3.2 最佳實踐總結(jié)
分層防御策略:
- UI層:按鈕loading和禁用狀態(tài)
- 網(wǎng)絡層:請求攔截和取消
- 業(yè)務層:防抖/節(jié)流控制
關鍵注意事項:
- 區(qū)分重復請求和并行請求的業(yè)務需求
- 考慮請求失敗的重試機制
- 注意內(nèi)存泄漏,及時清理請求緩存
- 提供手動取消請求的接口
性能優(yōu)化建議:
- 對于相同請求,考慮使用Promise緩存
- 合理設置防抖時間(通常300-500ms)
- 監(jiān)控并限制最大并發(fā)請求數(shù)
3.3 總結(jié)
防止接口重復請求是一個系統(tǒng)工程,需要根據(jù)具體業(yè)務場景選擇合適的方案組合。建議:
- 基礎防護:使用按鈕loading狀態(tài),這是最直接的用戶反饋
- 核心防護:實現(xiàn)請求攔截和去重,這是技術(shù)保障
- 增強防護:根據(jù)業(yè)務特點添加防抖/節(jié)流等優(yōu)化策略
- 監(jiān)控反饋:添加請求日志,便于問題排查和優(yōu)化
在實際項目中,建議采用綜合分層方案,既保證用戶體驗,又確保系統(tǒng)穩(wěn)定性,同時為后續(xù)擴展留出空間。
以上就是前端防止接口重復請求的四種方案的詳細內(nèi)容,更多關于前端防止接口重復請求的資料請關注腳本之家其它相關文章!
相關文章
window.onbeforeunload方法在IE下無法正常工作的解決辦法
下面的代碼可以做到不管用戶是點擊了關閉,或者是在任務欄關閉、點擊后退、刷新、按F5鍵,都可以檢測到用戶即將離開的消息。2010-01-01
JS開發(fā)接入?deepseek?實現(xiàn)AI智能問診
本文介紹了如何使用DeepSeek?API在UniApp項目中實現(xiàn)智能問診功能,代碼示例展示了如何構(gòu)建請求并處理響應,本文給大家介紹的非常詳細,感興趣的朋友一起看看吧2025-02-02
javascript 動態(tài)生成css代碼的兩種方法
這篇文章主要介紹了javascript 動態(tài)生成css代碼的兩種方法,有時候我們需要利用js來動態(tài)生成頁面上style標簽中的css代碼,下面就給大家介紹兩種方法,需要的朋友可以參考下2017-03-03
JavaScript箭頭函數(shù)與普通函數(shù)的區(qū)別示例詳解
這篇文章主要為大家介紹了JavaScript箭頭函數(shù)與普通函數(shù)的區(qū)別示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10
全面了解構(gòu)造函數(shù)繼承關鍵apply call
下面小編就為大家?guī)硪黄媪私鈽?gòu)造函數(shù)繼承關鍵apply call。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-07-07
JavaScript編程設計模式之構(gòu)造器模式實例分析
這篇文章主要介紹了JavaScript編程設計模式之構(gòu)造器模式,簡單講述了構(gòu)造器模式的概念、原理,并結(jié)合實例形式分析了構(gòu)造器模式的定義與使用方法,需要的朋友可以參考下2017-10-10

