JS—axios怎么封装请求包括取消请求

88 阅读6分钟

以下是一个 生产级 axios 封装方案,包含 请求 / 响应拦截器、统一错误处理、重复请求取消、请求缓存、超时控制 等核心功能,重点解决「取消请求」(重复请求自动取消 + 手动取消)的实际需求:

封装核心思路

  1. 基础配置:统一 baseURL、超时时间、请求头;

  2. 拦截器:请求拦截器添加 Token、请求 ID;响应拦截器统一处理成功 / 失败;

  3. 取消请求核心

    • 用 Map 维护「请求缓存池」,存储未完成的请求(key 为请求唯一标识,value 为取消控制器);
    • 重复请求触发时,先取消旧请求,再发送新请求;
    • 支持手动取消单个 / 全部请求;
  4. 错误处理:区分「取消错误」「网络错误」「业务错误」(如 401、403);

  5. 扩展性:支持请求缓存、自定义配置(如是否允许重复请求、是否缓存响应)。

完整封装代码(request.js

import axios from 'axios';
import { getToken, removeToken, router } from '@/utils/auth'; // 假设的 Token 工具和路由

// 1. 创建 axios 实例
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量(Vite/Env)
  timeout: 10000, // 超时时间 10s
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  }
});

// 2. 核心:请求缓存池(存储未完成的请求,用于取消重复请求)
/**
 * 缓存池结构:Map<requestKey, AbortController>
 * requestKey:请求唯一标识(method + url + 序列化参数)
 * AbortController:取消控制器(调用 abort() 可取消请求)
 */
const requestCache = new Map();

// 3. 辅助函数:生成请求唯一标识(避免重复请求)
function generateRequestKey(config) {
  const { method = 'get', url = '', params = {}, data = {} } = config;
  // 序列化参数(确保相同参数顺序生成相同 key)
  const paramsStr = JSON.stringify(params);
  const dataStr = JSON.stringify(data);
  return `${method.toLowerCase()}-${url}-${paramsStr}-${dataStr}`;
}

// 4. 辅助函数:添加请求到缓存池
function addRequestToCache(config) {
  const requestKey = generateRequestKey(config);
  // 若已存在相同请求,先取消旧请求
  if (requestCache.has(requestKey)) {
    const oldController = requestCache.get(requestKey);
    oldController.abort(`取消重复请求:${requestKey}`);
    requestCache.delete(requestKey);
  }
  // 创建新的取消控制器,绑定到请求配置
  const controller = new AbortController();
  config.signal = controller.signal;
  // 存入缓存池
  requestCache.set(requestKey, controller);
  return requestKey;
}

// 5. 辅助函数:从缓存池移除请求
function removeRequestFromCache(config) {
  const requestKey = generateRequestKey(config);
  if (requestCache.has(requestKey)) {
    requestCache.delete(requestKey);
  }
}

// 6. 手动取消请求的 API(对外暴露)
/**
 * 手动取消单个请求
 * @param {Object} config - 请求配置(需包含 method、url、params/data)
 */
export function cancelRequest(config) {
  const requestKey = generateRequestKey(config);
  if (requestCache.has(requestKey)) {
    const controller = requestCache.get(requestKey);
    controller.abort(`手动取消请求:${requestKey}`);
    requestCache.delete(requestKey);
    return true;
  }
  return false; // 未找到对应请求
}

/**
 * 手动取消所有未完成的请求
 */
export function cancelAllRequests() {
  requestCache.forEach((controller, key) => {
    controller.abort(`手动取消所有请求:${key}`);
  });
  requestCache.clear();
}

// 7. 请求拦截器:添加 Token、处理重复请求
service.interceptors.request.use(
  (config) => {
    // 忽略重复请求(通过自定义配置 skipDuplicateCancel 控制)
    if (!config.skipDuplicateCancel) {
      addRequestToCache(config); // 添加到缓存池(自动取消重复请求)
    }

    // 添加 Token(如 JWT)
    const token = getToken();
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }

    return config;
  },
  (error) => {
    // 请求初始化失败(如参数错误),移除缓存
    removeRequestFromCache(error.config);
    return Promise.reject(error);
  }
);

// 8. 响应拦截器:统一处理响应、移除缓存
service.interceptors.response.use(
  (response) => {
    // 请求完成(成功),从缓存池移除
    removeRequestFromCache(response.config);

    // 统一响应格式(假设后端返回 { code: 0, data: {}, msg: '' })
    const { code, data, msg } = response.data;
    if (code === 0) {
      // 业务成功:返回数据(支持缓存响应,通过 customCache 控制)
      if (response.config.customCache) {
        // 这里可添加缓存逻辑(如存入 localStorage/sessionStorage)
      }
      return data;
    } else {
      // 业务失败:抛出错误(如 code: 1001 代表参数错误)
      console.error(`业务错误 [${code}]:`, msg);
      return Promise.reject(new Error(msg || '请求失败'));
    }
  },
  (error) => {
    // 请求完成(失败/取消),从缓存池移除
    if (error.config) {
      removeRequestFromCache(error.config);
    }

    // 区分错误类型
    if (axios.isCancel(error)) {
      // 取消错误:不抛出(避免影响全局错误提示),返回自定义取消标识
      console.log('请求被取消:', error.message);
      return Promise.reject({ type: 'cancel', message: error.message });
    } else if (!window.navigator.onLine) {
      // 网络错误
      console.error('网络错误:请检查网络连接');
      return Promise.reject(new Error('网络错误:请检查网络连接'));
    } else {
      // HTTP 错误(如 401、403、500)
      const status = error.response?.status;
      switch (status) {
        case 401:
          // Token 过期/未授权:清除 Token 并跳转登录页
          removeToken();
          router.push('/login?redirect=' + encodeURIComponent(router.currentRoute.value.fullPath));
          return Promise.reject(new Error('登录已过期,请重新登录'));
        case 403:
          return Promise.reject(new Error('无权限访问,请联系管理员'));
        case 500:
          return Promise.reject(new Error('服务器错误,请稍后重试'));
        default:
          const errMsg = error.response?.data?.msg || '请求失败';
          return Promise.reject(new Error(errMsg));
      }
    }
  }
);

// 9. 对外暴露核心请求方法(get/post/put/delete)
export const request = {
  get(url, params = {}, config = {}) {
    return service.get(url, { params, ...config });
  },

  post(url, data = {}, config = {}) {
    return service.post(url, data, config);
  },

  put(url, data = {}, config = {}) {
    return service.put(url, data, config);
  },

  delete(url, params = {}, config = {}) {
    return service.delete(url, { params, ...config });
  }
};

// 导出默认请求方法
export default request;

封装功能说明

1. 基础使用(无特殊配置)

import request from '@/utils/request';

// GET 请求
async function fetchData() {
  try {
    const data = await request.get('/api/data', { id: 1 });
    console.log('请求成功:', data);
  } catch (err) {
    // 区分取消错误和其他错误
    if (err.type === 'cancel') {
      console.log('请求被取消:', err.message);
    } else {
      console.error('请求失败:', err.message);
    }
  }
}

// POST 请求
async function submitData() {
  try {
    const data = await request.post('/api/submit', { name: '张三' });
    console.log('提交成功:', data);
  } catch (err) {
    console.error('提交失败:', err.message);
  }
}

2. 取消请求相关用法

(1)自动取消重复请求(默认开启)

当快速多次调用同一请求(相同 method、url、params/data)时,会自动取消前一次未完成的请求,仅保留最后一次:

// 快速调用 3 次相同请求,前 2 次会被自动取消
fetchData();
fetchData();
fetchData(); // 仅最后一次有效
(2)跳过重复请求取消(自定义配置)

某些场景(如并发提交多个不同参数的请求)需允许重复请求,通过 skipDuplicateCancel: true 关闭自动取消:

// 允许重复请求(即使参数相同,也不取消旧请求)
request.get('/api/data', { id: 1 }, { skipDuplicateCancel: true });
(3)手动取消单个请求

需传入与请求完全一致的配置(method、url、params/data):

import { request, cancelRequest } from '@/utils/request';

// 发送请求
request.get('/api/data', { id: 1 });

// 手动取消该请求(需匹配配置)
setTimeout(() => {
  cancelRequest({
    method: 'get',
    url: '/api/data',
    params: { id: 1 }
  });
}, 500);
(4)手动取消所有未完成请求

适用于「页面卸载」「退出登录」等场景,取消所有 pending 状态的请求:

import { cancelAllRequests } from '@/utils/request';

// 页面卸载时取消所有请求(React/Vue 生命周期)
// React:useEffect 清理函数
useEffect(() => {
  return () => cancelAllRequests();
}, []);

// Vue:onUnmounted
onUnmounted(() => {
  cancelAllRequests();
});

3. 其他实用功能

(1)请求缓存(自定义配置)

通过 customCache: true 开启响应缓存(需在响应拦截器中扩展缓存逻辑,如存入本地存储):

// 开启缓存,相同请求会优先读取缓存(需自行实现缓存逻辑)
request.get('/api/data', { id: 1 }, { customCache: true });
(2)超时控制(全局 / 局部)
  • 全局超时:在 axios.create 中配置 timeout: 10000(10 秒);

  • 局部超时:单个请求覆盖全局配置:

    // 该请求超时时间为 5 秒(覆盖全局 10 秒)
    request.get('/api/slow-data', {}, { timeout: 5000 });
    

关键细节说明

  1. 请求唯一标识生成

    • 用 method + url + 序列化参数 作为 key,确保相同请求(参数顺序不影响)生成相同 key;
    • 若请求参数包含非序列化值(如函数),需额外处理(或排除该参数),避免 key 不一致。
  2. 取消错误处理

    • 取消错误通过 err.type === 'cancel' 区分,避免与业务错误、网络错误混淆;
    • 取消错误不会触发全局错误提示(如 Toast),提升用户体验。
  3. 内存泄漏防护

    • 所有请求完成(成功 / 失败 / 取消)后,都会从缓存池移除,避免内存泄漏;
    • 页面卸载时调用 cancelAllRequests(),确保销毁未完成请求。
  4. 兼容性

    • 基于 AbortController 实现取消(axios v0.22.0+ 支持),对齐原生 fetch 标准;
    • 若需兼容旧版 axios(v0.22.0 以下),可替换为 CancelToken(但已废弃,不推荐)。

扩展场景(可选)

  1. 添加请求加载状态

    • 在请求拦截器中触发加载状态(如显示 Loading);
    • 在响应拦截器中关闭加载状态(需处理同时多个请求的 Loading 合并)。
  2. 接口重试机制

    • 对 500、503 等服务器错误,添加自动重试逻辑(通过 config.retry 配置重试次数)。
  3. 全局错误提示

    • 结合 UI 库(如 Element Plus、Ant Design),在响应拦截器中统一弹出错误提示(如 ElMessage)。

总结

该封装方案的核心优势:

  • 取消请求:自动取消重复请求 + 手动取消单个 / 全部请求,覆盖绝大多数场景;
  • 通用性:统一请求 / 响应处理,减少重复代码;
  • 扩展性:支持自定义配置(跳过重复取消、缓存、超时),便于后续扩展;
  • 稳定性:完善的错误处理和内存泄漏防护,适合生产环境使用。

使用时只需按 request.get/post 调用,无需关心底层取消逻辑,大幅提升开发效率。