基于 @umijs/max 的 request 补充常见错误统一处理、请求取消、重复请求防抖的完整方案

一、完整增强版 request 配置(src/utils/request.ts)

typescript

运行

typescript 复制代码
import { request } from '@umijs/max';
import { message, Modal } from 'antd'; // 假设项目用Antd,可替换为其他UI库提示

// 全局默认配置
request.defaults.baseURL = 'https://api.example.com';
request.defaults.timeout = 10000;
request.defaults.credentials = 'include';

// 存储请求取消令牌,用于取消请求/防抖
const cancelTokenMap = new Map<string, AbortController>();

// 生成请求唯一key(用于防抖)
const getRequestKey = (url: string, options: any) => {
  const { method = 'GET', params, data } = options;
  return `${method}_${url}_${JSON.stringify(params || {})}_${JSON.stringify(data || {})}`;
};

// ====================== 1. 统一错误处理 ======================
const errorHandler = (error: any) => {
  const { response, message: errMsg } = error;

  // 1.1 取消请求错误(主动取消,不提示)
  if (error.name === 'AbortError') {
    console.warn('请求已取消:', errMsg);
    return Promise.reject(error);
  }

  // 1.2 HTTP状态码错误
  if (response) {
    const { status } = response;
    let errorText = '';
    switch (status) {
      case 400:
        errorText = '请求参数错误';
        break;
      case 401:
        errorText = '登录失效,请重新登录';
        // 登录失效:清空token + 跳转登录页(仅触发一次)
        Modal.confirm({
          title: '提示',
          content: errorText,
          okText: '去登录',
          cancelText: '取消',
          onOk: () => {
            localStorage.removeItem('token');
            window.location.href = '/login';
          },
          maskClosable: false,
        });
        break;
      case 403:
        errorText = '暂无权限访问该资源';
        break;
      case 404:
        errorText = '请求资源不存在';
        break;
      case 500:
        errorText = '服务器内部错误';
        break;
      case 502:
        errorText = '网关错误';
        break;
      case 503:
        errorText = '服务器维护中';
        break;
      default:
        errorText = `请求失败(${status})`;
    }
    message.error(errorText);
  } else {
    // 1.3 网络错误(无响应)
    message.error('网络异常,请检查网络连接');
  }

  return Promise.reject(error);
};

// ====================== 2. 请求拦截器(防抖+取消请求) ======================
request.interceptors.request.use((url, options) => {
  // 2.1 统一添加token
  const token = localStorage.getItem('token');
  const headers = {
    ...options.headers,
    'Authorization': token ? `Bearer ${token}` : '',
    'Content-Type': 'application/json',
  };

  // 2.2 重复请求防抖:取消相同的未完成请求
  const requestKey = getRequestKey(url, options);
  // 取消已有请求
  if (cancelTokenMap.has(requestKey)) {
    cancelTokenMap.get(requestKey)?.abort();
    cancelTokenMap.delete(requestKey);
  }
  // 创建新的取消令牌
  const controller = new AbortController();
  cancelTokenMap.set(requestKey, controller);
  options.signal = controller.signal;

  return {
    url,
    options: { ...options, headers },
  };
});

// ====================== 3. 响应拦截器(统一解析+错误处理) ======================
request.interceptors.response.use(
  async (response) => {
    // 3.1 移除已完成请求的取消令牌
    const requestKey = getRequestKey(response.url, response.options);
    cancelTokenMap.delete(requestKey);

    // 3.2 统一解析响应数据
    const data = await response.clone().json();
    // 业务错误码处理(非200均视为错误)
    if (data.code !== 200) {
      message.error(data.message || '请求失败');
      return Promise.reject(new Error(data.message || '业务请求失败'));
    }
    return data;
  },
  (error) => {
    // 3.3 统一捕获所有错误
    return errorHandler(error);
  }
);

// 暴露取消请求的方法(手动取消指定请求)
export const cancelRequest = (url: string, options: any = {}) => {
  const requestKey = getRequestKey(url, options);
  if (cancelTokenMap.has(requestKey)) {
    cancelTokenMap.get(requestKey)?.abort();
    cancelTokenMap.delete(requestKey);
    message.info('请求已取消');
  }
};

// 暴露取消所有请求的方法(如页面卸载时)
export const cancelAllRequests = () => {
  for (const controller of cancelTokenMap.values()) {
    controller.abort();
  }
  cancelTokenMap.clear();
  message.info('所有请求已取消');
};

export default request;

二、核心功能使用示例

1. 业务组件中调用(含手动取消请求)

typescript

运行

javascript 复制代码
import React, { useEffect } from 'react';
import request, { cancelRequest, cancelAllRequests } from '@/utils/request';

const DemoComponent = () => {
  // 示例1:普通请求调用
  const fetchData = async () => {
    try {
      const res = await request.get('/api/list', {
        params: { id: '123' },
      });
      console.log('请求成功:', res);
    } catch (err) {
      // 错误已在全局拦截器处理,无需重复提示
      console.error('请求失败:', err);
    }
  };

  // 示例2:手动取消指定请求
  const handleCancelRequest = () => {
    cancelRequest('/api/list', {
      method: 'GET',
      params: { id: '123' },
    });
  };

  // 示例3:页面卸载时取消所有请求(防止内存泄漏)
  useEffect(() => {
    return () => {
      cancelAllRequests();
    };
  }, []);

  return (
    <div>
      <button onClick={fetchData}>发起请求</button>
      <button onClick={handleCancelRequest}>取消请求</button>
    </div>
  );
};

export default DemoComponent;

2. 特殊场景:文件下载(blob 响应类型)

typescript

运行

javascript 复制代码
// 单独处理文件下载请求(不适用全局JSON解析)
export async function downloadFile(id: string) {
  try {
    const response = await request.get(`/api/download/${id}`, {
      responseType: 'blob', // 指定响应类型为blob
      // 禁用全局响应拦截器的JSON解析(避免报错)
      skipResponseInterceptors: true,
    });
    // 处理文件下载
    const blob = await response.blob();
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `文件-${id}.xlsx`;
    a.click();
    window.URL.revokeObjectURL(url);
  } catch (err) {
    message.error('文件下载失败');
  }
}

三、关键功能说明

功能 实现逻辑
统一错误处理 按 HTTP 状态码(401/404/500)和业务错误码(code≠200)分类提示,401 自动跳转登录
请求取消 基于 AbortController 创建取消令牌,存储到 Map 中,支持手动 / 批量取消
重复请求防抖 发起请求前取消相同 key 的未完成请求,避免重复接口调用(如快速点击按钮)
内存泄漏防护 页面卸载时取消所有未完成请求,避免组件销毁后请求仍在执行

总结

  1. 全局拦截器实现了错误统一提示、token 自动携带、重复请求防抖,减少业务代码冗余;
  2. AbortController 是现代浏览器原生 API,可安全用于取消 fetch 底层的请求;
  3. 页面卸载时调用 cancelAllRequests 是避免内存泄漏的关键最佳实践。
相关推荐
拖拉斯旋风2 小时前
深入浅出 RAG:从网页爬取到智能问答的完整链路解析
前端
Mintopia2 小时前
Vite 发展现状与回顾:从“极致开发体验”到生态基础设施
前端
前端双越老师2 小时前
前端面试常见的 10 个场景题
前端·面试·求职
孟祥_成都3 小时前
【全网最通俗!新手到AI全栈开发必读】 AI 是如何进化到大模型的
前端·人工智能·全栈
牛奶4 小时前
AI辅助开发的基础概念
前端·人工智能·ai编程
摸鱼的春哥4 小时前
Agent教程15:认识LangChain,Agent框架的王(上)
前端·javascript·后端
明月_清风5 小时前
自定义右键菜单:在项目里实现“选中文字即刻生成新提示”
前端·javascript
明月_清风5 小时前
告别后端转换:高质量批量导出实战
前端·javascript
刘发财9 小时前
弃用html2pdf.js,这个html转pdf方案能力是它的几十倍
前端·javascript·github