前端批量请求失败重复弹窗的正确解决方案

前端批量请求失败重复弹窗的正确解决方案

问题本质分析

大部分前端开发者在处理批量请求错误时存在根本性误区:在拦截器中直接处理错误并弹窗。这种做法违背了错误处理的基本原则,导致了重复弹窗等用户体验问题。

常见错误做法

javascript 复制代码
// ❌ 错误的做法:在拦截器中直接处理错误
axios.interceptors.response.use(
  response => response,
  error => {
    // 这里直接弹窗是错误的!
    message.error('请求失败,请重试');
    return Promise.reject(error);
  }
);

问题分析:

  1. 职责混乱:拦截器应该负责错误收集,而非错误展示
  2. 无法区分场景:所有错误都会触发弹窗,无法根据业务场景定制
  3. 重复弹窗:并发请求失败时会出现多个弹窗
  4. 缺乏灵活性:业务层无法自定义错误处理逻辑

正确的解决思路

核心原则

  1. 分层处理:拦截器负责错误收集,业务层负责错误处理和展示
  2. 顶层统一:将错误处理逻辑提升到业务层统一管理
  3. 合理聚合:使用现代 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())
  )
  
  // 处理结果...
}

最佳实践总结

✅ 正确做法

  1. 使用 antd 的 message 组件:提供更好的用户体验和一致性
  2. 拦截器职责单一:只负责错误收集和标准化,不做UI操作
  3. 业务层统一处理:在业务逻辑中统一处理错误和用户提示
  4. 合理使用Promise API
    • Promise.allSettled() 用于并发请求
    • Promise.any() 用于竞态请求
    • AggregateError 用于聚合多个错误
  5. 错误分类处理:根据错误类型提供不同的处理策略
  6. 防抖聚合:短时间内的多个错误进行聚合处理

❌ 错误做法

  1. 在拦截器中直接使用 message 弹窗
  2. 每个请求失败都单独处理
  3. 忽略错误的具体类型和上下文
  4. 缺乏错误聚合机制
  5. 没有区分并发请求和依赖请求的处理方式

核心思想

错误处理应该遵循"收集-聚合-展示"的流程

  • 收集:拦截器负责捕获和标准化错误
  • 聚合:业务层使用合适的Promise API聚合处理
  • 展示:根据业务场景和错误类型使用 antd message 统一展示给用户

这样的架构不仅解决了重复弹窗问题,还提供了更好的可维护性和用户体验。记住:好的错误处理是无声的艺术------让用户感知到必要的信息,同时避免不必要的干扰。

相关推荐
狼性书生几秒前
uniapp实现的简约美观的星级评分组件
前端·uni-app·vue·组件
书语时3 分钟前
ES6 Promise 状态机
前端·javascript·es6
拉不动的猪31 分钟前
管理不同权限用户的左侧菜单展示以及权限按钮的启用 / 禁用之其中一种解决方案
前端·javascript·面试
西陵42 分钟前
前端框架渲染DOM的的方式你知道多少?
前端·javascript·架构
小九九的爸爸43 分钟前
我是如何让AI帮我还原设计稿的
前端·人工智能·ai编程
海的诗篇_1 小时前
前端开发面试题总结-JavaScript篇(一)
开发语言·前端·javascript·学习·面试
江城开朗的豌豆1 小时前
eval:JavaScript里的双刃剑,用好了封神,用不好封号!
前端·javascript·面试
Forever Nore1 小时前
前端技能包
前端
江城开朗的豌豆2 小时前
JavaScript篇:前端定时器黑科技:不用setInterval照样玩转循环任务
前端·javascript·面试
书中自有妍如玉2 小时前
.net 使用MQTT订阅消息
java·前端·.net