前端如何防止接口重复请求方案解析

目录

  • 一、常见方案分析
    • [1.1 Loading状态方案](#1.1 Loading状态方案)
    • [1.2 请求标记方案](#1.2 请求标记方案)
    • [1.3 防抖/节流方案](#1.3 防抖/节流方案)
    • [1.4 请求拦截器方案](#1.4 请求拦截器方案)
    • [1.5 React Hooks方案](#1.5 React Hooks方案)
  • 二、综合方案建议
  • 三、使用建议和总结
    • [3.1 方案选择建议](#3.1 方案选择建议)
    • [3.2 最佳实践总结](#3.2 最佳实践总结)
    • [3.3 总结](#3.3 总结)

一、常见方案分析

1.1 Loading状态方案

c 复制代码
// 方案1: 按钮Loading状态
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
  }
}

优点

  • 实现简单,用户体验直观

  • 能有效防止重复点击

  • 适用于按钮点击场景

缺点

  • 无法防止并行多个不同请求

  • 需要手动管理loading状态

  • 页面多个按钮需要分别处理

1.2 请求标记方案

c 复制代码
// 方案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);
    }

    // 创建新的请求
    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);
    }
  }
}

优点

  • 精确控制重复请求

  • 可复用已有请求Promise

  • 支持请求取消

缺点

  • 实现相对复杂

  • 需要维护请求映射表

  • 内存占用需要考虑

1.3 防抖/节流方案

c 复制代码
// 方案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);
    });
  }

  // 节流版本
  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));
    });
  }
}

优点

  • 有效控制请求频率

  • 减少服务器压力

  • 适用于搜索框等高频场景

缺点

  • 可能延迟必要请求

  • 不适合所有场景

  • 需要合理设置时间间隔

1.4 请求拦截器方案

c 复制代码
// 方案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函数
        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();
  }
}

优点

  • 全局统一处理

  • 与业务代码解耦

  • 支持精细化控制

缺点

  • 配置复杂

  • 可能影响正常请求

  • 需要处理边界情况

1.5 React Hooks方案

c 复制代码
// 方案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>
  );
}

二、综合方案建议

推荐实现:分层防重复策略

c 复制代码
// 综合方案:分层防重复
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. 直接执行请求
    return this.executeRequest(requestKey, config, showLoading);
  }

  // 执行请求
  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 {
      // 创建请求Promise
      const requestPromise = axios({
        ...config,
        cancelToken: new axios.CancelToken(cancel => {
          // 存储cancel函数
          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);
    // 触发loading状态变化事件
    this.onLoadingChange(key, isLoading);
  }

  onLoadingChange(key, isLoading) {
    // 可以在这里实现loading UI更新
    console.log(`Loading状态变化: ${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();
  }
}

三、使用建议和总结

3.1 方案选择建议

3.2 最佳实践总结

分层防御策略

  • UI层:按钮loading和禁用状态

  • 网络层:请求拦截和取消

  • 业务层:防抖/节流控制

关键注意事项

  • 区分重复请求和并行请求的业务需求

  • 考虑请求失败的重试机制

  • 注意内存泄漏,及时清理请求缓存

  • 提供手动取消请求的接口

性能优化建议

  • 对于相同请求,考虑使用Promise缓存

  • 合理设置防抖时间(通常300-500ms)

  • 监控并限制最大并发请求数

3.3 总结

防止接口重复请求是一个系统工程,需要根据具体业务场景选择合适的方案组合。建议:

  • 基础防护:使用按钮loading状态,这是最直接的用户反馈

  • 核心防护:实现请求拦截和去重,这是技术保障

  • 增强防护:根据业务特点添加防抖/节流等优化策略

  • 监控反馈:添加请求日志,便于问题排查和优化

在实际项目中,建议采用综合分层方案,既保证用户体验,又确保系统稳定性,同时为后续扩展留出空间。

相关推荐
Filotimo_2 小时前
Vue3 + Element Plus 表格复选框踩坑记录
javascript·vue.js·elementui
宁然也2 小时前
浏览器的多进程架构
react.js
大风起兮云飞扬丶2 小时前
web前端缓存命中监控方案
前端·缓存
pas1362 小时前
32-mini-vue 更新element的children-双端对比 diff 算法
javascript·vue.js·算法
恋爱绝缘体12 小时前
CSS3 多媒体查询实例【1】
前端·css·css3
写bug的可宋2 小时前
【Electron】解决Electron使用阿里iconfont不生效问题(react+vite)
javascript·react.js·electron
小二·3 小时前
Python Web 开发进阶实战:无障碍深度集成 —— 构建真正包容的 Flask + Vue 应用
前端·python·flask
niucloud-admin11 小时前
web 端前端
前端
摘星编程14 小时前
React Native for OpenHarmony 实战:Linking 链接处理详解
javascript·react native·react.js