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

3 阅读2分钟

前言

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

错误分类

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

核心实现

1. 全局错误处理

// 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 错误处理器

// 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 错误处理

// 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 请求错误处理

// 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. 资源加载错误处理

// 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. 错误边界组件

<!-- 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

<!-- 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 集成

npm install @sentry/vue @sentry/tracing
// 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 包装异步代码
  • 提供用户友好的错误提示
  • 记录详细错误日志便于调试

🔗 相关资源