前端批量请求失败重复弹窗的正确解决方案
问题本质分析
大部分前端开发者在处理批量请求错误时存在根本性误区:在拦截器中直接处理错误并弹窗。这种做法违背了错误处理的基本原则,导致了重复弹窗等用户体验问题。
常见错误做法
javascript
// ❌ 错误的做法:在拦截器中直接处理错误
axios.interceptors.response.use(
response => response,
error => {
// 这里直接弹窗是错误的!
message.error('请求失败,请重试');
return Promise.reject(error);
}
);
问题分析:
- 职责混乱:拦截器应该负责错误收集,而非错误展示
- 无法区分场景:所有错误都会触发弹窗,无法根据业务场景定制
- 重复弹窗:并发请求失败时会出现多个弹窗
- 缺乏灵活性:业务层无法自定义错误处理逻辑
正确的解决思路
核心原则
- 分层处理:拦截器负责错误收集,业务层负责错误处理和展示
- 顶层统一:将错误处理逻辑提升到业务层统一管理
- 合理聚合:使用现代 JavaScript API 正确处理并发请求
技术方案
1. 正确的拦截器设计
javascript
// ✅ 正确的做法:拦截器只负责错误收集和标准化
axios.interceptors.response.use(
response => response,
error => {
// 只做错误信息标准化,不做UI操作
const standardError = {
code: error.response?.data?.code || 'NETWORK_ERROR',
message: error.response?.data?.message || '网络请求失败',
status: error.response?.status,
url: error.config?.url
};
// 直接抛出,交给业务层处理
return Promise.reject(standardError);
}
);
2. 使用 Promise.allSettled() 处理并发请求
javascript
// ✅ 正确处理并发请求的方式
const handleBatchRequests = async () => {
const requests = [
checkStock(),
validateCoupon(),
checkAddress(),
calculateTax(),
getShippingFee()
];
// 使用 allSettled 等待所有请求完成
const results = await Promise.allSettled(requests);
// 分离成功和失败的结果
const successes = results
.filter(result => result.status === 'fulfilled')
.map(result => result.value);
const failures = results
.filter(result => result.status === 'rejected')
.map(result => result.reason);
// 业务层统一处理错误
if (failures.length > 0) {
handleBatchErrors(failures);
}
return { successes, failures };
};
3. 使用 AggregateError 聚合多个错误
javascript
// ✅ 使用 AggregateError 聚合错误信息
const handleBatchRequestsWithAggregateError = async () => {
try {
const results = await Promise.allSettled(requests);
const errors = results
.filter(result => result.status === 'rejected')
.map(result => result.reason);
if (errors.length > 0) {
// 使用 AggregateError 包装多个错误
throw new AggregateError(errors, '批量请求部分失败');
}
// 处理成功逻辑
const successResults = results
.filter(result => result.status === 'fulfilled')
.map(result => result.value);
return successResults;
} catch (error) {
if (error instanceof AggregateError) {
// 统一处理聚合错误
handleAggregateError(error);
} else {
// 处理单个错误
handleSingleError(error);
}
throw error;
}
};
// 处理聚合错误
const handleAggregateError = (aggregateError) => {
const errorMessages = aggregateError.errors
.map(error => error.message)
.filter((msg, index, arr) => arr.indexOf(msg) === index); // 去重
const errorMessage = errorMessages.length === 1
? errorMessages[0]
: `操作失败:${errorMessages.join('、')}`;
message.error(errorMessage);
// 可选:上报错误统计
console.error('Batch request errors:', aggregateError.errors);
};
Vue 实际运用示例
1. Vue 3 + Composition API + Ant Design Vue
vue:src/views/CheckoutPage.vue
<template>
<div class="checkout-page">
<a-button
type="primary"
:loading="loading"
@click="handleCheckout"
>
提交订单
</a-button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import { useErrorHandler } from '@/composables/useErrorHandler'
import {
checkStock,
validateCoupon,
checkAddress,
calculateTax,
getShippingFee
} from '@/api/checkout'
const loading = ref(false)
const { handleBatchRequests } = useErrorHandler()
const handleCheckout = async () => {
loading.value = true
try {
const requests = [
checkStock(),
validateCoupon(),
checkAddress(),
calculateTax(),
getShippingFee()
]
const { successes, failures } = await handleBatchRequests(requests)
if (failures.length === 0) {
message.success('订单提交成功!')
// 处理成功逻辑
} else if (failures.length < requests.length) {
message.warning('部分校验失败,请检查后重试')
}
} catch (error) {
console.error('结算失败:', error)
} finally {
loading.value = false
}
}
</script>
2. Vue 错误处理 Composable
javascript:src/composables/useErrorHandler.js
import { ref } from 'vue'
import { message } from 'ant-design-vue'
export function useErrorHandler() {
const isHandlingError = ref(false)
const errorQueue = ref([])
let errorTimer = null
// 错误聚合处理
const handleError = (error, context = '') => {
errorQueue.value.push({ error, context, timestamp: Date.now() })
if (!isHandlingError.value) {
isHandlingError.value = true
// 延迟聚合错误,避免重复弹窗
errorTimer = setTimeout(() => {
showAggregatedErrors()
clearErrors()
}, 300)
}
}
// 显示聚合错误
const showAggregatedErrors = () => {
if (errorQueue.value.length === 0) return
const uniqueMessages = [...new Set(
errorQueue.value.map(item => item.error.message || '请求失败')
)]
const errorMessage = uniqueMessages.length === 1
? uniqueMessages[0]
: `操作失败:${uniqueMessages.join('、')}`
message.error(errorMessage)
}
// 清理错误队列
const clearErrors = () => {
errorQueue.value = []
isHandlingError.value = false
if (errorTimer) {
clearTimeout(errorTimer)
errorTimer = null
}
}
// 批量请求处理
const handleBatchRequests = async (requests, options = {}) => {
const { showSuccess = false, successMessage = '操作成功' } = options
try {
const results = await Promise.allSettled(requests)
const errors = results
.filter(result => result.status === 'rejected')
.map(result => result.reason)
const successes = results
.filter(result => result.status === 'fulfilled')
.map(result => result.value)
if (errors.length > 0) {
if (errors.length === requests.length) {
// 全部失败
throw new AggregateError(errors, '所有请求都失败了')
} else {
// 部分失败
errors.forEach(error => handleError(error))
}
} else if (showSuccess) {
// 全部成功
message.success(successMessage)
}
return { successes, failures: errors }
} catch (error) {
if (error instanceof AggregateError) {
handleError(error)
} else {
handleError(error)
}
throw error
}
}
return {
handleError,
handleBatchRequests,
clearErrors
}
}
React 实际运用示例
1. React + Hooks + Ant Design
jsx:src/components/CheckoutPage.jsx
import React, { useState } from 'react'
import { Button, message } from 'antd'
import { useErrorHandler } from '../hooks/useErrorHandler'
import {
checkStock,
validateCoupon,
checkAddress,
calculateTax,
getShippingFee
} from '../api/checkout'
const CheckoutPage = () => {
const [loading, setLoading] = useState(false)
const { handleBatchRequests } = useErrorHandler()
const handleCheckout = async () => {
setLoading(true)
try {
const requests = [
checkStock(),
validateCoupon(),
checkAddress(),
calculateTax(),
getShippingFee()
]
const { successes, failures } = await handleBatchRequests(requests)
if (failures.length === 0) {
message.success('订单提交成功!')
// 处理成功逻辑
} else if (failures.length < requests.length) {
message.warning('部分校验失败,请检查后重试')
}
} catch (error) {
console.error('结算失败:', error)
} finally {
setLoading(false)
}
}
return (
<div className="checkout-page">
<Button
type="primary"
loading={loading}
onClick={handleCheckout}
>
提交订单
</Button>
</div>
)
}
export default CheckoutPage
2. React 错误处理 Hook
javascript:src/hooks/useErrorHandler.js
import { useState, useCallback, useRef } from 'react'
import { message } from 'antd'
export function useErrorHandler() {
const [isHandlingError, setIsHandlingError] = useState(false)
const errorQueueRef = useRef([])
const errorTimerRef = useRef(null)
// 错误聚合处理
const handleError = useCallback((error, context = '') => {
errorQueueRef.current.push({
error,
context,
timestamp: Date.now()
})
if (!isHandlingError) {
setIsHandlingError(true)
// 延迟聚合错误,避免重复弹窗
errorTimerRef.current = setTimeout(() => {
showAggregatedErrors()
clearErrors()
}, 300)
}
}, [isHandlingError])
// 显示聚合错误
const showAggregatedErrors = useCallback(() => {
if (errorQueueRef.current.length === 0) return
const uniqueMessages = [...new Set(
errorQueueRef.current.map(item => item.error.message || '请求失败')
)]
const errorMessage = uniqueMessages.length === 1
? uniqueMessages[0]
: `操作失败:${uniqueMessages.join('、')}`
message.error(errorMessage)
}, [])
// 清理错误队列
const clearErrors = useCallback(() => {
errorQueueRef.current = []
setIsHandlingError(false)
if (errorTimerRef.current) {
clearTimeout(errorTimerRef.current)
errorTimerRef.current = null
}
}, [])
// 批量请求处理
const handleBatchRequests = useCallback(async (requests, options = {}) => {
const { showSuccess = false, successMessage = '操作成功' } = options
try {
const results = await Promise.allSettled(requests)
const errors = results
.filter(result => result.status === 'rejected')
.map(result => result.reason)
const successes = results
.filter(result => result.status === 'fulfilled')
.map(result => result.value)
if (errors.length > 0) {
if (errors.length === requests.length) {
// 全部失败
throw new AggregateError(errors, '所有请求都失败了')
} else {
// 部分失败
errors.forEach(error => handleError(error))
}
} else if (showSuccess) {
// 全部成功
message.success(successMessage)
}
return { successes, failures: errors }
} catch (error) {
if (error instanceof AggregateError) {
handleError(error)
} else {
handleError(error)
}
throw error
}
}, [handleError])
return {
handleError,
handleBatchRequests,
clearErrors
}
}
3. React 文件上传批量处理示例
jsx:src/components/FileUpload.jsx
import React, { useState } from 'react'
import { Upload, Button, message, Progress } from 'antd'
import { UploadOutlined } from '@ant-design/icons'
import { useErrorHandler } from '../hooks/useErrorHandler'
const FileUpload = () => {
const [fileList, setFileList] = useState([])
const [uploading, setUploading] = useState(false)
const [progress, setProgress] = useState(0)
const { handleBatchRequests } = useErrorHandler()
const handleUpload = async () => {
if (fileList.length === 0) {
message.warning('请先选择文件')
return
}
setUploading(true)
setProgress(0)
try {
// 创建上传请求数组
const uploadRequests = fileList.map(file => {
const formData = new FormData()
formData.append('file', file)
return fetch('/api/upload', {
method: 'POST',
body: formData
}).then(res => {
if (!res.ok) {
throw new Error(`${file.name} 上传失败`)
}
return res.json()
})
})
// 使用批量请求处理
const { successes, failures } = await handleBatchRequests(
uploadRequests,
{ showSuccess: false }
)
// 根据结果显示不同的提示
if (failures.length === 0) {
message.success(`所有文件上传成功!共 ${successes.length} 个文件`)
setFileList([])
} else if (successes.length > 0) {
message.warning(
`部分文件上传成功:${successes.length} 个成功,${failures.length} 个失败`
)
}
} catch (error) {
console.error('批量上传失败:', error)
} finally {
setUploading(false)
setProgress(0)
}
}
const props = {
onRemove: (file) => {
const index = fileList.indexOf(file)
const newFileList = fileList.slice()
newFileList.splice(index, 1)
setFileList(newFileList)
},
beforeUpload: (file) => {
setFileList([...fileList, file])
return false
},
fileList,
}
return (
<div>
<Upload {...props}>
<Button icon={<UploadOutlined />}>选择文件</Button>
</Upload>
<Button
type="primary"
onClick={handleUpload}
disabled={fileList.length === 0}
loading={uploading}
style={{ marginTop: 16 }}
>
{uploading ? '上传中' : '开始上传'}
</Button>
{uploading && (
<Progress percent={progress} style={{ marginTop: 16 }} />
)}
</div>
)
}
export default FileUpload
高级优化方案
1. 错误分类处理
javascript
// 错误分类器
class ErrorClassifier {
static classify(error) {
const status = error.response?.status
const code = error.response?.data?.code
if (status >= 500) return 'server_error'
if (status === 401) return 'auth_error'
if (status === 403) return 'permission_error'
if (status === 404) return 'not_found'
if (code === 'STOCK_INSUFFICIENT') return 'business_error'
return 'unknown_error'
}
static getErrorMessage(errorType, error) {
const messages = {
'server_error': '服务器异常,请稍后重试',
'auth_error': '登录已过期,请重新登录',
'permission_error': '权限不足,请联系管理员',
'not_found': '请求的资源不存在',
'business_error': error.response?.data?.message || '业务异常',
'unknown_error': '未知错误'
}
return messages[errorType] || messages['unknown_error']
}
static handleErrorByType(errorType, errors) {
const errorMessage = this.getErrorMessage(errorType, errors[0])
// 根据错误类型使用不同的 message 方法
switch (errorType) {
case 'auth_error':
message.error(errorMessage, 5) // 显示5秒
// 可以添加跳转登录页的逻辑
break
case 'server_error':
message.error(errorMessage, 3)
break
case 'business_error':
message.warning(errorMessage, 4)
break
default:
message.error(errorMessage)
}
}
}
2. 请求重试机制
javascript
// 带重试的请求函数
const fetchWithRetry = async (requestFn, retries = 2, delay = 1000) => {
try {
return await requestFn()
} catch (error) {
if (retries > 0 && isRetriableError(error)) {
await new Promise(resolve => setTimeout(resolve, delay))
return fetchWithRetry(requestFn, retries - 1, delay * 2)
}
throw error
}
}
// 判断是否可重试的错误
const isRetriableError = (error) => {
const retriableCodes = [408, 429, 500, 502, 503, 504]
return retriableCodes.includes(error.response?.status)
}
// 在批量请求中使用重试
const handleBatchRequestsWithRetry = async () => {
const requests = [
() => fetchWithRetry(() => checkStock()),
() => fetchWithRetry(() => validateCoupon()),
() => fetchWithRetry(() => checkAddress())
]
const results = await Promise.allSettled(
requests.map(requestFn => requestFn())
)
// 处理结果...
}
最佳实践总结
✅ 正确做法
- 使用 antd 的 message 组件:提供更好的用户体验和一致性
- 拦截器职责单一:只负责错误收集和标准化,不做UI操作
- 业务层统一处理:在业务逻辑中统一处理错误和用户提示
- 合理使用Promise API :
Promise.allSettled()
用于并发请求Promise.any()
用于竞态请求AggregateError
用于聚合多个错误
- 错误分类处理:根据错误类型提供不同的处理策略
- 防抖聚合:短时间内的多个错误进行聚合处理
❌ 错误做法
- 在拦截器中直接使用 message 弹窗
- 每个请求失败都单独处理
- 忽略错误的具体类型和上下文
- 缺乏错误聚合机制
- 没有区分并发请求和依赖请求的处理方式
核心思想
错误处理应该遵循"收集-聚合-展示"的流程:
- 收集:拦截器负责捕获和标准化错误
- 聚合:业务层使用合适的Promise API聚合处理
- 展示:根据业务场景和错误类型使用 antd message 统一展示给用户
这样的架构不仅解决了重复弹窗问题,还提供了更好的可维护性和用户体验。记住:好的错误处理是无声的艺术------让用户感知到必要的信息,同时避免不必要的干扰。