# Axios 请求优化封装(去重与防抖)

核心目标是封装一个健壮的 request 函数,该函数在发起请求前能自动处理队列中的重复请求(去重),并能通过配置实现发送频率限制(防抖)

核心功能

  1. 请求去重:利用 Axios 拦截器,在发送请求前和响应接收后,通过 CancelToken 机制取消相同的待发请求
  2. 请求防抖:允许用户再请求配置中指定 debounce 延迟时间,从而限制请求的发送频率

# 请求去重机制

  1. 核心数据结构
    使用一个 Map 结构来存储待取消的请求:
const pendingRequests = new Map<string, Canceler>();
  • Key:唯一标识符,由请求方法、URL、参数和数据组成
  • Value:Axios 的 Cancelr 函数,用于取消请求
  1. 生成请求唯一 Key
    getRequestKey 函数负责生成一个稳定的请求标识符。为了确保请求无论何时发送都能匹配。
function getRequestKey(config: InternalAxiosRequestConfig): string {
  const { method, url, params, data } = config;
  // 将 JSON 对象转换为字符串以确保稳定性
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
}
  1. 拦截器实现逻辑
  • 请求拦截器( service.interceptors.request
    • removePendingRequest : 在发送新请求前,检查 pendingRequests 中是否存在相同 Key 的请求。如果存在,则立即执行 Canceler 函数取消旧请求,并从 Map 中删除
    • addPendingRequest :为当前请求创建一个新的 CancelToken ,并将其存储到 pendingRequests Map 中
  • 响应拦截器( service.interceptors.response
    • 无论请求成功还是失败,都需要调用 removePendingRequest(response.config) 将其从 pendingRequests 队列中移除

# 请求防抖机制

  1. 核心防抖函数 debounceRequest
    该函数是一个高阶函数,接收一个执行请求的函数 func 和延迟时间 delay ,并返回一个被防抖的新函数
let debounceTimer: ReturnType<typeof setTimeout>; // 使用全局计时器来确保所有防抖请求共享状态
function debounceRequest<T> (
  func: (config: CustomRequestConfig) => Promise<T>,
  delay: number
): (config: CustomRequestConfig) => Promise<T> {
  return function (config: CustomRequestConfig) {
    clearTimeout(debounceTimer); // 每次调用都清除上一个计时器
    return new Promise((resolve, reject) => {
      debounceTimer = setTimeout(() => {
        func(config).then(resolve).catch(reject); // 延迟后执行原始请求
      }, delay);
    })
  }
}
  • Promise 封装:关键在于使用 new Promise 封装了 setTimeout 内部的原始请求。这确保了外部调用者得到的 Promise ( lastRequestPromise ) 只有在延迟结束后、原始请求真正执行并完成时才会被 resolve /reject
  1. 统一入口 request
    request 函数作为封装的最终 API,负责判断用户是否需要开启防抖功能
function request<T = unknown>(options: CustomRequestConfig): Promise<T> {
  // 如果配置中存在 debounce 字段,则应用防抖包装
  if (options.debounce) {
    // 1. 创建一个防抖版本的 service.request 函数
    // 2. 立即执行这个防抖函数,并传入 options
    return debounceRequest(config => service.request(config), options.debounce)(options) as Promise<T>;
  }
  // 否则,立即发送请求
  return service.request(options);
}

# 完整代码实现

import axios from 'axios'; 
import type { InternalAxiosRequestConfig, AxiosRequestConfig, Canceler, AxiosResponse  } from 'axios';
interface CustomRequestConfig extends AxiosRequestConfig {
  debounce?: number;
}
const pendingRequests = new Map<string, Canceler>();
function getRequestKey(config: InternalAxiosRequestConfig) {
  const { method, url, params, data } = config;
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
}
function addPendingRequest(config: InternalAxiosRequestConfig) {
  const key = getRequestKey(config);
  config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
    if (!pendingRequests.has(key)) {
      pendingRequests.set(key, cancel);
    }
  })
}
function removePendingRequest(config: InternalAxiosRequestConfig) {
  const requestKey = getRequestKey(config);
  if (pendingRequests.has(requestKey)) {
    const cancel = pendingRequests.get(requestKey) as Canceler;
    cancel(`取消重复请求: ${requestKey}`);
    pendingRequests.delete(requestKey);
  }
}
export const service = axios.create({
  baseURL: '/',
  timeout: 10000,
})
service.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    removePendingRequest(config);
    addPendingRequest(config);
    return config;
  },
  error => {
    return Promise.reject(error);
  }
)
service.interceptors.response.use(
  (response: AxiosResponse) => {
    removePendingRequest(response.config);
    return response.data;
  },
  error => {
    if (error.config) {
      removePendingRequest(error.config);
    }
    if (axios.isCancel(error)) {
      console.log('请求已取消:', error.message);
    } else {
      console.error('请求发生错误:', error.message);
    }
    return Promise.reject(error);
  }
)
let debounceTimer: ReturnType<typeof setTimeout>;
function debounceRequest<T> (
  func: (config: CustomRequestConfig) => Promise<T>,
  delay: number
): (config: CustomRequestConfig) => Promise<T> {
  return function (config: CustomRequestConfig) {
    clearTimeout(debounceTimer);
    return new Promise((resolve, reject) => {
      debounceTimer = setTimeout(() => {
        console.log('qwer');
        func(config).then(resolve).catch(reject);
      }, delay);
    })
  }
}
function request<T = unknown>(options: CustomRequestConfig): Promise<T> {
  if (options.debounce) {
    return debounceRequest(config => service.request(config), options.debounce)(options) as Promise<T>;
  }
  return service.request(options);
}
export default request;