全栈系列(16) 当有多个错误时,前端如何只显示一个错误弹框

50 阅读3分钟

为什么这么做很有意义?

  1. 用户体验(UX)灾难:想象一下用户打开“仪表盘”页面,页面同时触发了 5 个接口(获取用户信息、获取图表数据、获取通知、获取菜单等)。如果服务器挂了(500)或者用户断网了,瞬间弹出 5 个红色的“网络错误”弹框,像刷屏一样,用户体验极差。
  2. 401 登录过期:这是最经典的场景。当 Token 过期时,页面并发的 3 个请求都会返回 401。如果不做防抖,用户会看到 3 个“登录已过期,请重新登录”的弹窗(或者被重定向逻辑触发 3 次),这在逻辑上是冗余的。

这里分两种情况:

  1. Toast 轻提示(ElMessage) :防止满屏飘红。
  2. Modal 确认框(ElMessageBox) :防止 401 时弹出多个确认窗口。

我们可以设置一个“锁”,当第一个错误弹出后,几秒钟内不再弹出新的错误。

修改 src/utils/request.ts:

import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '@/store/modules/user'

// 1. 定义一个变量来控制错误提示的频率
let isErrorMessageShowing = false

const service = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 5000
})

// ... 请求拦截器保持不变 ...

// 响应拦截器
service.interceptors.response.use(
  (response) => {
    // ... 正常逻辑保持不变 ...
    const res = response.data
    if (res.code === 200) {
      return res.data
    } else {
      // 调用统一错误处理
      showError(res.message || '系统错误')
      return Promise.reject(new Error(res.message))
    }
  },
  (error) => {
    let msg = '网络请求失败'
    if (error.response) {
      // 根据状态码判断
      switch (error.response.status) {
        case 401:
          handle401() // 单独处理 401
          return Promise.reject(error)
        case 403:
          msg = '拒绝访问'
          break
        case 404:
          msg = '请求资源不存在'
          break
        case 500:
          msg = '服务器内部错误'
          break
        default:
          msg = error.response.data?.message || '未知错误'
      }
    }
    showError(msg)
    return Promise.reject(error)
  }
)

/**
 * 核心优化:错误提示防抖/节流
 * 规则:如果当前有错误正在显示(或在3秒内显示过),则不再弹出新错误
 */
function showError(msg: string) {
  if (isErrorMessageShowing) {
    return // 如果锁住了,直接返回,不弹窗
  }

  isErrorMessageShowing = true // 加锁
  ElMessage.error({
    message: msg,
    duration: 3000,
    onClose: () => {
      // 这里的 onClose 是 Element Plus 消息关闭时的回调
      // 你可以选择在这里解锁,或者用 setTimeout 解锁
    }
  })

  // 设置一个冷却时间,比如 3秒内只弹一次错误
  setTimeout(() => {
    isErrorMessageShowing = false // 解锁
  }, 3000)
}

// ...

针对 401 (Token过期) 的“单例模式”

对于 401,我们通常会弹出一个 ElMessageBox.confirm 模态框让用户去登录。这个绝对不能弹多次。

// 定义一个变量标记是否正在重新登录
let isReloginShow = false

function handle401() {
  // 如果已经弹出了框,就不要再弹了
  if (isReloginShow) return

  isReloginShow = true // 加锁

  ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
    confirmButtonText: '重新登录',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    // 点击确定
    isReloginShow = false // 解锁
    const userStore = useUserStore()
    userStore.logout() // 登出并跳转
  }).catch(() => {
    // 点击取消
    isReloginShow = false // 解锁
  })
}

代码逻辑解释:

  1. 并发场景模拟
    假设页面初始化同时发了 5 个请求,Token 刚好过期了,这 5 个请求几乎同时返回 401。

  2. 第一个 401 到达

    • isErrorMessageShowing 为 false。
    • 执行 showError -> 上锁 -> 弹出“请先登录”。
    • 执行 logout()。
  3. 后续 4 个 401 到达

    • isErrorMessageShowing 已经是 true 了。
    • 执行 showError -> 直接 return。
    • 用户界面上只看到一条红色的错误提示,体验非常清爽。
  4. 解锁

    • 3秒后,setTimeout 执行,锁打开,允许下一次错误提示弹出。