Ant Design Pro实战:Umi-request的401无感刷新与文件下载全解析
最近在重构公司内部的管理系统时,遇到了两个让人头疼的问题:Token过期后的无感刷新和文件下载的异常处理。作为基于Ant Design Pro搭建的项目,这些问题看似简单,实则暗藏玄机。本文将分享我在解决这些问题时积累的实战经验,希望能帮助遇到类似困境的开发者少走弯路。
1. 理解Umi-request的核心机制
Umi-request作为Umi框架内置的HTTP客户端,兼具了fetch的现代API和axios的易用性。但在深入使用前,有几个关键特性需要明确:
- 拦截器机制:与axios类似,提供了请求和响应拦截能力
- 错误处理:内置了基础错误处理,但业务场景需要自定义
- 响应类型:默认处理JSON响应,其他类型需要特殊处理
// 基础request实例创建 import { extend } from 'umi-request'; const request = extend({ timeout: 10000, credentials: 'include' });在Ant Design Pro的生态中,Umi-request已经预先配置了基础拦截器,但针对特定业务场景,我们需要进行深度定制。
2. 401状态的无感刷新方案
Token过期是后台管理系统常见的问题,传统方案是直接跳转登录页,但这样会中断用户操作流程。更优雅的方式是实现无感刷新。
2.1 基础拦截器配置
首先,我们需要在响应拦截器中识别401状态:
request.interceptors.response.use(async (response) => { if (response.status === 401) { // 处理Token过期逻辑 } return response; });2.2 刷新Token的并发控制
当多个请求同时返回401时,需要避免重复刷新Token:
let isRefreshing = false; let requests: ((token: string) => void)[] = []; request.interceptors.response.use(async (response) => { if (response.status === 401 && !isRefreshing) { isRefreshing = true; try { const newToken = await refreshToken(); requests.forEach(cb => cb(newToken)); requests = []; return request(response.url, response.options); } finally { isRefreshing = false; } } else if (response.status === 401 && isRefreshing) { return new Promise((resolve) => { requests.push((token) => { response.options.headers.Authorization = `Bearer ${token}`; resolve(request(response.url, response.options)); }); }); } return response; });注意:刷新Token接口本身不能加入401拦截逻辑,否则会导致死循环
2.3 用户无感体验优化
除了Token刷新,还需要考虑以下场景:
- 刷新Token失败后的降级处理
- 长时间无操作后的自动登出
- 关键操作前的Token有效性检查
const refreshToken = async () => { try { const result = await request('/api/auth/refresh', { method: 'POST' }); setNewToken(result.token); return result.token; } catch (error) { // 刷新失败跳转登录页 redirectToLogin(); throw error; } };3. 文件下载的Blob处理实战
文件下载是另一个常见需求,但处理不当容易导致页面崩溃或内存泄漏。
3.1 基础文件下载实现
request.interceptors.response.use(async (response) => { if (response.url.includes('/download/')) { const blob = await response.clone().blob(); const filename = getFilenameFromHeaders(response.headers); downloadFile(blob, filename); return null; // 中断后续处理 } return response; }); function downloadFile(blob: Blob, filename: string) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }3.2 大文件下载优化
对于大文件下载,需要考虑以下问题:
- 内存管理:使用流式处理避免内存暴涨
- 进度显示:通过ReadableStream实现下载进度
- 错误恢复:支持断点续传
const downloadLargeFile = async (url: string, filename: string) => { const response = await fetch(url); const reader = response.body.getReader(); const contentLength = +response.headers.get('Content-Length'); let receivedLength = 0; const chunks = []; while(true) { const {done, value} = await reader.read(); if (done) break; chunks.push(value); receivedLength += value.length; updateProgress(receivedLength / contentLength); } const blob = new Blob(chunks); downloadFile(blob, filename); };3.3 常见问题排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 下载文件损坏 | 未正确处理Blob类型 | 检查Content-Type和文件扩展名 |
| 内存泄漏 | 未释放ObjectURL | 确保调用URL.revokeObjectURL |
| 进度不准确 | 未获取Content-Length | 服务端需返回正确头信息 |
| 大文件失败 | 内存不足 | 改用流式处理方案 |
4. 全局请求的最佳实践
4.1 请求重试机制
对于网络波动导致的失败,合理的重试策略能提升用户体验:
const MAX_RETRY = 3; const RETRY_DELAY = 1000; const fetchWithRetry = async (url: string, options = {}, retries = 0) => { try { return await request(url, options); } catch (error) { if (retries < MAX_RETRY && shouldRetry(error)) { await delay(RETRY_DELAY * (retries + 1)); return fetchWithRetry(url, options, retries + 1); } throw error; } }; function shouldRetry(error) { return error.response?.status >= 500 || error.message === 'Network Error'; }4.2 性能监控集成
在拦截器中加入性能统计:
request.interceptors.request.use((url, options) => { const startTime = Date.now(); options.metadata = { startTime }; return { url, options }; }); request.interceptors.response.use((response, options) => { const duration = Date.now() - options.metadata.startTime; trackApiPerformance(response.url, duration); return response; });4.3 安全防护措施
- CSRF防护:确保正确配置credentials
- 参数过滤:拦截器中过滤敏感参数
- 请求限流:防止接口被滥用
request.interceptors.request.use((url, options) => { // 过滤敏感参数 if (options.data?.password) { options.data.password = '[FILTERED]'; } // 添加CSRF Token options.headers['X-CSRF-TOKEN'] = getCsrfToken(); return { url, options }; });5. 异常处理的艺术
5.1 错误分类处理
不同业务场景需要不同的错误处理策略:
const errorHandler = (error) => { if (error.timeout) { showToast('请求超时,请检查网络'); } else if (error.response) { switch (error.response.status) { case 401: handleUnauthorized(); break; case 403: showForbiddenModal(); break; case 500: logServerError(error); break; default: showGenericError(error); } } else { showNetworkError(); } };5.2 错误上报集成
结合Sentry等监控工具实现错误上报:
request.interceptors.response.use(null, (error) => { if (isCriticalError(error)) { Sentry.captureException(error, { extra: { url: error.config.url, params: error.config.params } }); } return Promise.reject(error); });5.3 用户友好的错误展示
避免直接显示技术性错误信息:
function showUserFriendlyError(error) { const userMessages = { ECONNABORTED: '请求超时,请稍后重试', ERR_NETWORK: '网络连接异常,请检查网络设置', default: '系统繁忙,请稍后再试' }; const message = userMessages[error.code] || userMessages.default; notification.error({ message }); }在Ant Design Pro项目中,这些技术细节的合理处理能显著提升应用稳定性和用户体验。经过多个项目的实践验证,这套方案在保证功能完整性的同时,也兼顾了性能和可维护性。