一、完整增强版 request 配置(src/utils/request.ts)
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
运行
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
运行
// 单独处理文件下载请求(不适用全局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 的未完成请求,避免重复接口调用(如快速点击按钮) |
| 内存泄漏防护 | 页面卸载时取消所有未完成请求,避免组件销毁后请求仍在执行 |
总结
- 全局拦截器实现了错误统一提示、token 自动携带、重复请求防抖,减少业务代码冗余;
AbortController是现代浏览器原生 API,可安全用于取消fetch底层的请求;- 页面卸载时调用
cancelAllRequests是避免内存泄漏的关键最佳实践。