🛡️ Vue 3 错误处理完全指南:全局异常捕获、前端监控、用户反馈

前言

一个健壮的错误处理机制可以提升用户体验,帮助开发者快速定位问题。今天分享如何实现完善的错误处理和监控!

错误分类

javascript 复制代码
错误类型
├── Vue 渲染错误
│   ├── 组件渲染错误
│   ├── 生命周期钩子错误
│   └── 模板语法错误
├── JavaScript 运行时错误
│   ├── TypeError
│   ├── ReferenceError
│   └── 自定义业务错误
├── 异步错误
│   ├── Promise rejection
│   ├── setTimeout/setInterval
│   └── Web API 错误
└── 资源加载错误
    ├── 图片加载失败
    ├── 脚本加载失败
    └── API 请求失败

核心实现

1. 全局错误处理

typescript 复制代码
// src/utils/errorHandler.ts
import { isDev } from './env'

// 错误日志服务
class ErrorLogger {
  private logs: ErrorLog[] = []
  private maxLogs = 100
  
  log(error: Error, context?: string) {
    const log: ErrorLog = {
      id: `err_${Date.now()}`,
      message: error.message,
      stack: error.stack,
      context,
      timestamp: Date.now(),
      userAgent: navigator.userAgent,
      url: window.location.href
    }
    
    this.logs.unshift(log)
    
    // 只保留最近 N 条
    if (this.logs.length > this.maxLogs) {
      this.logs = this.logs.slice(0, this.maxLogs)
    }
    
    // 开发环境输出到控制台
    if (isDev) {
      console.error('[Error]', context, error)
    }
    
    // 上报到服务器(生产环境)
    if (!isDev) {
      this.reportToServer(log)
    }
  }
  
  private async reportToServer(log: ErrorLog) {
    // 可以发送到你的日志服务
    try {
      await fetch('/api/error-report', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(log)
      })
    } catch (e) {
      // 存储到 localStorage 作为后备
      this.saveToLocalStorage(log)
    }
  }
  
  private saveToLocalStorage(log: ErrorLog) {
    const key = 'error_logs'
    const existing = JSON.parse(localStorage.getItem(key) || '[]')
    existing.push(log)
    localStorage.setItem(key, JSON.stringify(existing.slice(-50)))
  }
  
  getLogs(): ErrorLog[] {
    return this.logs
  }
}

interface ErrorLog {
  id: string
  message: string
  stack?: string
  context?: string
  timestamp: number
  userAgent: string
  url: string
}

export const errorLogger = new ErrorLogger()

2. Vue 错误处理器

typescript 复制代码
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { errorLogger } from '@/utils/errorHandler'

const app = createApp(App)

// Vue 渲染错误处理
app.config.errorHandler = (err, instance, info) => {
  // err: 错误对象
  // instance: 发生错误的组件实例
  // info: 额外的错误信息(如生命周期钩子名称)
  
  console.error('Vue Error:', err)
  console.error('Component:', instance)
  console.error('Info:', info)
  
  errorLogger.log(err, `Vue Error [${info}]`)
  
  // 可以在这里显示用户友好的错误提示
  // showErrorToast('发生了一些问题,请稍后重试')
}

// 组件警告处理(开发环境)
if (import.meta.env.DEV) {
  app.config.warnHandler = (msg, instance, trace) => {
    console.warn('Vue Warn:', msg)
    console.warn('Trace:', trace)
  }
}

// 异步错误处理
app.config.asyncErrorHandler = (err, instance, info) => {
  errorLogger.log(err, `Async Error [${info}]`)
}

app.mount('#app')

3. Promise 错误处理

typescript 复制代码
// src/utils/promiseHandler.ts

// 全局未处理的 Promise rejection
window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled Promise Rejection:', event.reason)
  
  errorLogger.log(
    event.reason instanceof Error 
      ? event.reason 
      : new Error(String(event.reason)),
    'Unhandled Promise Rejection'
  )
  
  // 阻止默认行为(显示控制台错误)
  event.preventDefault()
})

// 安全执行 Promise
export async function safeAsync<T>(
  promise: Promise<T>,
  fallback?: T
): Promise<T | undefined> {
  try {
    return await promise
  } catch (error) {
    errorLogger.log(
      error instanceof Error ? error : new Error(String(error)),
      'Safe Async'
    )
    return fallback
  }
}

// Promise 包装函数
export function to<T>(promise: Promise<T>): Promise<[Error | null, T | null]> {
  return promise
    .then<[null, T]>((data) => [null, data])
    .catch<[Error, null]>((err) => [err, null])
}

4. API 请求错误处理

typescript 复制代码
// src/utils/request.ts
import { errorLogger } from './errorHandler'
import { ElMessage } from 'element-plus'

interface RequestOptions {
  showError?: boolean
  errorContext?: string
}

export async function request<T>(
  url: string,
  options: RequestOptions = {}
): Promise<T> {
  const { showError = true, errorContext = 'API Request' } = options
  
  try {
    const response = await fetch(url)
    
    if (!response.ok) {
      const error = new Error(`HTTP ${response.status}: ${response.statusText}`)
      errorLogger.log(error, errorContext)
      
      if (showError) {
        ElMessage.error(`请求失败: ${error.message}`)
      }
      
      throw error
    }
    
    return await response.json()
  } catch (error) {
    if (error instanceof Error) {
      errorLogger.log(error, errorContext)
      
      if (showError && !error.message.includes('HTTP')) {
        ElMessage.error('网络请求失败,请检查网络连接')
      }
    }
    
    throw error
  }
}

// 使用示例
async function fetchArticle(id: string) {
  return request<Article>(`/api/articles/${id}`, {
    showError: true,
    errorContext: 'fetchArticle'
  })
}

5. 资源加载错误处理

typescript 复制代码
// src/composables/useResourceError.ts
import { onMounted, onErrorCaptured } from 'vue'

export function useResourceError() {
  // 图片加载失败
  function handleImageError(event: Event) {
    const img = event.target as HTMLImageElement
    img.src = '/default-image.png' // 默认图片
    img.classList.add('error-loaded')
  }
  
  // 脚本加载失败
  function loadScript(src: string): Promise<void> {
    return new Promise((resolve, reject) => {
      const script = document.createElement('script')
      script.src = src
      script.onload = () => resolve()
      script.onerror = () => {
        reject(new Error(`Failed to load script: ${src}`))
      }
      document.head.appendChild(script)
    })
  }
  
  // Vue 组件内错误捕获
  onErrorCaptured((err, instance, info) => {
    console.error('Captured in component:', err)
    console.error('Component:', instance)
    console.error('Info:', info)
    
    // 返回 false 阻止错误继续传播
    return false
  })
  
  return {
    handleImageError,
    loadScript
  }
}

6. 错误边界组件

vue 复制代码
<!-- src/components/ErrorBoundary.vue -->
<template>
  <slot v-if="!hasError" />
  
  <div v-else class="error-boundary">
    <div class="error-content">
      <div class="error-icon">😢</div>
      <h2>页面出现了一些问题</h2>
      <p>别担心,这只是一个小插曲</p>
      
      <div class="error-actions">
        <el-button type="primary" @click="handleRetry">
          重试一下
        </el-button>
        <el-button @click="handleGoHome">
          返回首页
        </el-button>
      </div>
      
      <details v-if="showDetails" class="error-details">
        <summary>查看错误详情</summary>
        <pre>{{ errorMessage }}</pre>
      </details>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue'
import { useRouter } from 'vue-router'

const router = useRouter()
const hasError = ref(false)
const errorMessage = ref('')
const showDetails = ref(false)

onErrorCaptured((err, instance, info) => {
  hasError.value = true
  errorMessage.value = `${err.message}\n\nComponent: ${instance?.$options?.name || 'Unknown'}\nInfo: ${info}`
  
  console.error('ErrorBoundary caught:', err)
  
  // 返回 false 阻止错误传播
  return false
})

function handleRetry() {
  hasError.value = false
  errorMessage.value = ''
}

function handleGoHome() {
  hasError.value = false
  router.push('/')
}
</script>

<style scoped>
.error-boundary {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 400px;
  padding: 40px;
}

.error-content {
  text-align: center;
  max-width: 400px;
}

.error-icon {
  font-size: 64px;
  margin-bottom: 20px;
}

.error-content h2 {
  margin: 0 0 8px;
  font-size: 24px;
}

.error-content p {
  color: #666;
  margin-bottom: 24px;
}

.error-actions {
  display: flex;
  gap: 12px;
  justify-content: center;
  margin-bottom: 24px;
}

.error-details {
  text-align: left;
  margin-top: 20px;
}

.error-details pre {
  background: #f5f5f5;
  padding: 16px;
  border-radius: 8px;
  overflow-x: auto;
  font-size: 12px;
}
</style>

7. 使用 ErrorBoundary

vue 复制代码
<!-- src/App.vue -->
<template>
  <ErrorBoundary>
    <router-view />
  </ErrorBoundary>
</template>

<script setup lang="ts">
import ErrorBoundary from '@/components/ErrorBoundary.vue'
</script>

错误监控服务

推荐工具

  1. Sentry - 功能强大的错误追踪服务
  2. FunDebug - 国产前端监控工具
  3. Badjs - 腾讯开源的前端监控

Sentry 集成

bash 复制代码
npm install @sentry/vue @sentry/tracing
typescript 复制代码
// src/utils/sentry.ts
import * as Sentry from '@sentry/vue'
import { BrowserTracing } from '@sentry/tracing'

export function initSentry(app: any) {
  Sentry.init({
    app,
    dsn: 'YOUR_SENTRY_DSN',
    integrations: [
      new BrowserTracing({
        routingInstrumentation: Sentry.vueRouterInstrumentation(router),
      }),
    ],
    environment: import.meta.env.MODE,
    beforeSend(event) {
      // 过滤非关键错误
      if (event.exception?.values?.[0]?.type === 'AbortError') {
        return null
      }
      return event
    }
  })
}

💡 最佳实践

  • 始终在 Promise 后添加 .catch()

  • 使用 try-catch 包装异步代码

  • 提供用户友好的错误提示

  • 记录详细错误日志便于调试
    🔗 相关资源

  • Sentry:sentry.io

  • FunDebug:fundebug.com

相关推荐
mldong3 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排4 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60614 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角5 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!5 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰6 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi7 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen8 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马8 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒9 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端