javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > 前端防止接口重复请求

前端防止接口重复请求的四种方案

作者:DEMO派

文章介绍了几种常见的防止接口重复请求的方案,并推荐了分层防重复策略,建议根据业务场景选择合适的方案组合,以确保用户体验和系统稳定性,需要的朋友可以参考下

一、常见方案分析

1.1 Loading状态方案

// 方案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
  }
}

优点

缺点

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);
    }

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

优点

缺点

1.3 防抖/节流方案

// 方案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 请求拦截器方案

// 方案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方案

// 方案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>
  );
}

二、综合方案建议

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

// 综合方案:分层防重复
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 最佳实践总结

分层防御策略

关键注意事项

性能优化建议

3.3 总结

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

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

以上就是前端防止接口重复请求的四种方案的详细内容,更多关于前端防止接口重复请求的资料请关注脚本之家其它相关文章!

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