Vue3 + TypeScript项目异常处理体系建设:从组件到路由的完整实践

Frank575
Frank575 2026-03-10T16:01:10+08:00
0 0 0

在现代前端开发中,构建一个稳定可靠的异常处理体系是确保应用质量的关键环节。Vue3结合TypeScript的项目架构为开发者提供了更强大的类型安全和错误处理能力。本文将深入探讨如何在Vue3 + TypeScript项目中建立完整的异常处理体系,从组件级错误边界到路由守卫,再到全局错误监听,全方位保障前端应用的稳定性和用户体验。

一、Vue3异常处理体系概述

1.1 异常处理的重要性

前端应用的异常处理不仅仅是捕获错误信息那么简单。一个完善的异常处理体系应该能够:

  • 及时发现并定位问题
  • 提供友好的用户提示
  • 收集详细的错误日志
  • 实现优雅的降级机制
  • 保障应用的持续运行

在Vue3 + TypeScript环境中,我们可以通过类型系统来增强错误处理的准确性和可维护性。

1.2 Vue3异常处理特点

Vue3相比Vue2在异常处理方面有了显著改进:

  • 更好的错误追踪和调试支持
  • 支持更灵活的错误边界组件
  • TypeScript类型安全增强了错误处理的可靠性
  • 组件生命周期钩子提供了更多的异常捕获点

二、组件级错误边界实现

2.1 错误边界的必要性

在Vue应用中,单个组件的错误不应该影响整个应用的运行。通过实现错误边界,我们可以优雅地处理组件内部的异常。

// ErrorBoundary.vue
<template>
  <div class="error-boundary">
    <template v-if="hasError">
      <div class="error-content">
        <h3>发生错误</h3>
        <p>{{ errorMessage }}</p>
        <button @click="handleRetry">重试</button>
        <button @click="handleReset">重置</button>
      </div>
    </template>
    <template v-else>
      <slot></slot>
    </template>
  </div>
</template>

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

const hasError = ref(false)
const errorMessage = ref('')

onErrorCaptured((err, instance, info) => {
  console.error('Error captured in component:', err)
  console.error('Component instance:', instance)
  console.error('Error info:', info)
  
  hasError.value = true
  errorMessage.value = err.message || '未知错误'
  
  // 可以选择是否向上抛出错误
  return false
})

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

const handleReset = () => {
  hasError.value = false
  errorMessage.value = ''
}
</script>

2.2 高级错误边界组件

// AdvancedErrorBoundary.vue
<template>
  <div class="advanced-error-boundary">
    <template v-if="errorState === 'error'">
      <div class="error-display">
        <div class="error-icon">⚠️</div>
        <h3>应用出现异常</h3>
        <p class="error-message">{{ errorInfo?.message }}</p>
        <details v-if="errorInfo?.stack" class="error-stack">
          <summary>查看详细错误信息</summary>
          <pre>{{ errorInfo.stack }}</pre>
        </details>
        <div class="error-actions">
          <button @click="handleReload">刷新页面</button>
          <button @click="handleReport">报告问题</button>
        </div>
      </div>
    </template>
    <template v-else-if="errorState === 'loading'">
      <div class="loading-placeholder">加载中...</div>
    </template>
    <template v-else>
      <slot></slot>
    </template>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'
import { useErrorReporter } from '@/composables/useErrorReporter'

const props = defineProps<{
  fallbackComponent?: any
  reportError?: boolean
}>()

const errorState = ref<'idle' | 'loading' | 'error'>('idle')
const errorInfo = ref<Error | null>(null)

const { reportError } = useErrorReporter()

// 捕获子组件错误
const handleError = (error: Error, instance: any, info: string) => {
  console.error('Component error:', error)
  
  errorState.value = 'error'
  errorInfo.value = error
  
  if (props.reportError) {
    reportError(error, {
      component: instance?.constructor?.name || 'Unknown',
      info,
      timestamp: new Date().toISOString()
    })
  }
  
  return false
}

// 监听错误状态变化
watch(errorState, (newState) => {
  if (newState === 'error') {
    console.log('Error boundary activated')
  }
})

const handleReload = () => {
  window.location.reload()
}

const handleReport = () => {
  if (errorInfo.value && props.reportError) {
    reportError(errorInfo.value, {
      component: 'AdvancedErrorBoundary',
      info: 'User requested error reporting'
    })
  }
}

// 在组件中注册错误处理
defineExpose({
  handleError
})
</script>

<style scoped>
.error-display {
  padding: 20px;
  background-color: #fff5f5;
  border: 1px solid #fed7d7;
  border-radius: 4px;
  color: #c53030;
}

.error-message {
  margin: 10px 0;
  font-family: monospace;
}

.error-stack {
  margin: 10px 0;
  padding: 10px;
  background-color: #f7fafc;
  border-radius: 4px;
  overflow-x: auto;
}

.error-actions {
  margin-top: 15px;
}

.error-actions button {
  margin-right: 10px;
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

三、全局错误监听机制

3.1 Vue应用级错误处理

// errorHandler.ts
import { App } from 'vue'
import { ErrorInfo } from '@/types/error'

export class GlobalErrorHandler {
  private static instance: GlobalErrorHandler
  private errorQueue: ErrorInfo[] = []
  private maxQueueSize = 100
  
  constructor() {
    if (GlobalErrorHandler.instance) {
      return GlobalErrorHandler.instance
    }
    
    // 设置Vue全局错误处理器
    this.setupVueErrorHandler()
    // 设置JavaScript运行时错误监听
    this.setupRuntimeErrorListener()
    // 设置未处理的Promise拒绝监听
    this.setupUnhandledRejectionListener()
    
    GlobalErrorHandler.instance = this
  }
  
  private setupVueErrorHandler() {
    // Vue 3的全局错误处理器
    const originalErrorHandler = (error: Error, instance: any, info: string) => {
      console.error('Vue Error:', error)
      console.error('Component:', instance?.constructor?.name)
      console.error('Info:', info)
      
      this.handleError({
        type: 'vue',
        error,
        component: instance?.constructor?.name || 'Unknown',
        info,
        timestamp: new Date().toISOString()
      })
    }
    
    // 这里需要在Vue应用创建时调用
    // app.config.errorHandler = originalErrorHandler
  }
  
  private setupRuntimeErrorListener() {
    window.addEventListener('error', (event) => {
      console.error('Runtime Error:', event.error)
      
      this.handleError({
        type: 'runtime',
        error: event.error,
        component: 'Window',
        info: `Event: ${event.type}`,
        timestamp: new Date().toISOString(),
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno
      })
    })
  }
  
  private setupUnhandledRejectionListener() {
    window.addEventListener('unhandledrejection', (event) => {
      console.error('Unhandled Rejection:', event.reason)
      
      const error = event.reason instanceof Error 
        ? event.reason 
        : new Error(String(event.reason))
      
      this.handleError({
        type: 'promise',
        error,
        component: 'Promise',
        info: `Unhandled Promise Rejection`,
        timestamp: new Date().toISOString(),
        reason: event.reason
      })
      
      // 阻止默认的错误处理
      event.preventDefault()
    })
  }
  
  private handleError(errorInfo: ErrorInfo) {
    // 添加到错误队列
    this.errorQueue.push(errorInfo)
    
    // 限制队列大小
    if (this.errorQueue.length > this.maxQueueSize) {
      this.errorQueue.shift()
    }
    
    // 发送错误报告
    this.sendErrorReport(errorInfo)
    
    // 可以在这里添加其他处理逻辑
    this.handleErrorDisplay(errorInfo)
  }
  
  private sendErrorReport(errorInfo: ErrorInfo) {
    // 实现错误上报逻辑
    try {
      // 这里可以发送到监控系统或日志服务
      const reportData = {
        ...errorInfo,
        userAgent: navigator.userAgent,
        url: window.location.href,
        timestamp: new Date().toISOString()
      }
      
      console.log('Sending error report:', reportData)
      
      // 示例:发送到后端API
      /*
      fetch('/api/error-report', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(reportData)
      })
      */
    } catch (e) {
      console.error('Failed to send error report:', e)
    }
  }
  
  private handleErrorDisplay(errorInfo: ErrorInfo) {
    // 根据错误类型决定是否显示用户提示
    if (errorInfo.type === 'vue' || errorInfo.type === 'runtime') {
      // 可以通过全局状态管理器显示错误通知
      console.log('Showing user-friendly error message')
    }
  }
  
  public getErrorQueue(): ErrorInfo[] {
    return [...this.errorQueue]
  }
  
  public clearErrorQueue() {
    this.errorQueue = []
  }
}

// 在main.ts中初始化
export function setupGlobalErrorHandler(app: App) {
  const errorHandler = new GlobalErrorHandler()
  app.config.errorHandler = (error, instance, info) => {
    // 调用全局错误处理逻辑
    console.error('Global Vue error handler:', error)
  }
  
  return errorHandler
}

3.2 TypeScript错误类型定义

// types/error.ts
export interface ErrorInfo {
  type: 'vue' | 'runtime' | 'promise' | 'api' | 'network'
  error: Error
  component: string
  info: string
  timestamp: string
  filename?: string
  lineno?: number
  colno?: number
  reason?: any
  stack?: string
}

export interface ErrorReport {
  id: string
  type: string
  message: string
  stack: string
  component: string
  timestamp: string
  userAgent: string
  url: string
  context?: Record<string, any>
}

export interface ErrorBoundaryProps {
  fallbackComponent?: any
  onError?: (error: Error, info: string) => void
  reportError?: boolean
  showDetails?: boolean
}

四、路由守卫异常处理

4.1 路由守卫中的错误捕获

// router/errorHandler.ts
import { NavigationGuard, NavigationGuardNext } from 'vue-router'
import { useErrorReporter } from '@/composables/useErrorReporter'

export const routeErrorGuard: NavigationGuard = async (to, from, next) => {
  const { reportError } = useErrorReporter()
  
  try {
    // 在路由守卫中执行异步操作
    await handleRouteAsyncOperations(to, from)
    next()
  } catch (error) {
    console.error('Route guard error:', error)
    
    // 上报错误
    reportError(error, {
      component: 'RouteGuard',
      info: `Failed to navigate to ${to.path}`,
      route: to.path
    })
    
    // 显示错误页面或重定向到错误页面
    next({
      path: '/error',
      query: { 
        error: encodeURIComponent(String(error)),
        redirect: to.fullPath 
      }
    })
  }
}

const handleRouteAsyncOperations = async (to: any, from: any) => {
  // 模拟异步操作
  const promises = [
    fetchUserData(),
    loadComponentData(to),
    validatePermissions(to)
  ]
  
  try {
    await Promise.all(promises)
  } catch (error) {
    throw new Error(`Route guard failed: ${error.message}`)
  }
}

const fetchUserData = async () => {
  // 模拟用户数据获取
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.8) {
        reject(new Error('Failed to fetch user data'))
      } else {
        resolve({ id: 1, name: 'John Doe' })
      }
    }, 1000)
  })
}

const loadComponentData = async (route: any) => {
  // 模拟组件数据加载
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.9) {
        reject(new Error('Failed to load component data'))
      } else {
        resolve({ data: 'component data' })
      }
    }, 500)
  })
}

const validatePermissions = async (route: any) => {
  // 模拟权限验证
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.7) {
        reject(new Error('Permission denied'))
      } else {
        resolve(true)
      }
    }, 300)
  })
}

4.2 路由错误处理组件

// components/RouteErrorBoundary.vue
<template>
  <div class="route-error-boundary">
    <div v-if="errorState === 'error'" class="error-container">
      <div class="error-icon">⚠️</div>
      <h2>页面加载失败</h2>
      <p>{{ errorMessage }}</p>
      
      <div class="error-details" v-if="errorDetails">
        <details>
          <summary>查看详情</summary>
          <pre>{{ errorDetails }}</pre>
        </details>
      </div>
      
      <div class="error-actions">
        <button @click="handleRetry">重试</button>
        <button @click="goHome">返回首页</button>
        <button @click="handleReport">报告问题</button>
      </div>
    </div>
    
    <div v-else-if="errorState === 'loading'" class="loading-container">
      <div class="spinner"></div>
      <p>正在加载页面...</p>
    </div>
    
    <div v-else>
      <slot></slot>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useErrorReporter } from '@/composables/useErrorReporter'

const props = defineProps<{
  error?: Error
}>()

const router = useRouter()
const { reportError } = useErrorReporter()

const errorState = ref<'idle' | 'loading' | 'error'>('idle')
const errorMessage = ref('')
const errorDetails = ref('')

watch(
  () => props.error,
  (newError) => {
    if (newError) {
      handleError(newError)
    }
  },
  { immediate: true }
)

const handleError = (error: Error) => {
  console.error('Route error:', error)
  
  errorState.value = 'error'
  errorMessage.value = error.message || '页面加载失败,请稍后重试'
  errorDetails.value = error.stack || ''
  
  // 上报错误
  reportError(error, {
    component: 'RouteErrorBoundary',
    info: 'Route loading error'
  })
}

const handleRetry = () => {
  // 重新导航到当前路由
  router.go(0)
}

const goHome = () => {
  router.push('/')
}

const handleReport = () => {
  if (props.error) {
    reportError(props.error, {
      component: 'RouteErrorBoundary',
      info: 'User reported route error'
    })
  }
}
</script>

<style scoped>
.route-error-boundary {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.error-container {
  text-align: center;
  padding: 2rem;
  max-width: 500px;
  margin: 1rem;
  background-color: #fff5f5;
  border-radius: 8px;
  border: 1px solid #fed7d7;
}

.error-icon {
  font-size: 3rem;
  margin-bottom: 1rem;
}

.error-details {
  margin: 1rem 0;
  text-align: left;
}

.error-actions {
  margin-top: 1.5rem;
}

.error-actions button {
  margin: 0 0.5rem;
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
  background-color: #4299e1;
  color: white;
  cursor: pointer;
}

.loading-container {
  text-align: center;
  padding: 2rem;
}

.spinner {
  border: 4px solid #f3f3f3;
  border-top: 4px solid #4299e1;
  border-radius: 50%;
  width: 40px;
  height: 40px;
  animation: spin 1s linear infinite;
  margin: 0 auto 1rem;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
</style>

五、API错误处理最佳实践

5.1 HTTP客户端错误处理

// services/apiClient.ts
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
import { useErrorReporter } from '@/composables/useErrorReporter'

export class ApiClient {
  private client: typeof axios
  
  constructor() {
    this.client = axios.create({
      baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json'
      }
    })
    
    this.setupInterceptors()
  }
  
  private setupInterceptors() {
    // 请求拦截器
    this.client.interceptors.request.use(
      (config) => {
        // 添加认证token等
        const token = localStorage.getItem('auth_token')
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
        return config
      },
      (error) => {
        console.error('Request error:', error)
        return Promise.reject(error)
      }
    )
    
    // 响应拦截器
    this.client.interceptors.response.use(
      (response: AxiosResponse) => {
        return response.data
      },
      async (error: AxiosError) => {
        const { reportError } = useErrorReporter()
        
        console.error('API Error:', error)
        
        // 上报错误
        await reportError(error, {
          component: 'ApiClient',
          info: `HTTP ${error.response?.status} - ${error.message}`,
          url: error.config?.url,
          method: error.config?.method
        })
        
        // 根据状态码处理不同类型的错误
        if (error.response) {
          switch (error.response.status) {
            case 401:
              // 未授权,跳转到登录页
              window.location.href = '/login'
              break
            case 403:
              // 禁止访问
              throw new Error('您没有权限执行此操作')
            case 404:
              // 资源不存在
              throw new Error('请求的资源不存在')
            case 500:
              // 服务器内部错误
              throw new Error('服务器内部错误,请稍后重试')
            default:
              throw new Error(`请求失败: ${error.response.status}`)
          }
        } else {
          // 网络错误
          if (error.code === 'ECONNABORTED') {
            throw new Error('请求超时')
          } else {
            throw new Error('网络连接失败,请检查网络设置')
          }
        }
        
        return Promise.reject(error)
      }
    )
  }
  
  async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    try {
      const response = await this.client.get<T>(url, config)
      return response
    } catch (error) {
      throw this.handleError(error)
    }
  }
  
  async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    try {
      const response = await this.client.post<T>(url, data, config)
      return response
    } catch (error) {
      throw this.handleError(error)
    }
  }
  
  async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    try {
      const response = await this.client.put<T>(url, data, config)
      return response
    } catch (error) {
      throw this.handleError(error)
    }
  }
  
  async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    try {
      const response = await this.client.delete<T>(url, config)
      return response
    } catch (error) {
      throw this.handleError(error)
    }
  }
  
  private handleError(error: any): Error {
    if (error instanceof Error) {
      return error
    }
    
    return new Error('Unknown API error occurred')
  }
}

export const apiClient = new ApiClient()

5.2 错误处理组合式函数

// composables/useErrorReporter.ts
import { ref, reactive } from 'vue'
import { ErrorReport } from '@/types/error'

export function useErrorReporter() {
  const errorQueue = ref<ErrorReport[]>([])
  const isReporting = ref(false)
  
  const reportError = async (error: Error | any, context?: Record<string, any>) => {
    if (!error) return
    
    try {
      isReporting.value = true
      
      const errorReport: ErrorReport = {
        id: generateId(),
        type: error.constructor?.name || 'Unknown',
        message: error.message || String(error),
        stack: error.stack || '',
        component: context?.component || 'Unknown',
        timestamp: new Date().toISOString(),
        userAgent: navigator.userAgent,
        url: window.location.href,
        context: context || {}
      }
      
      // 添加到队列
      errorQueue.value.push(errorReport)
      
      // 限制队列大小
      if (errorQueue.value.length > 100) {
        errorQueue.value.shift()
      }
      
      // 实际上报逻辑
      await sendErrorReport(errorReport)
      
      console.log('Error reported:', errorReport)
    } catch (reportError) {
      console.error('Failed to report error:', reportError)
    } finally {
      isReporting.value = false
    }
  }
  
  const sendErrorReport = async (report: ErrorReport) => {
    // 这里实现实际的错误上报逻辑
    try {
      // 可以发送到监控服务或日志系统
      /*
      await fetch('/api/error-logs', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(report)
      })
      */
      
      // 模拟上报
      console.log('Sending error report:', report)
    } catch (e) {
      console.error('Error reporting failed:', e)
    }
  }
  
  const clearErrors = () => {
    errorQueue.value = []
  }
  
  const getRecentErrors = (limit: number = 10) => {
    return errorQueue.value.slice(-limit).reverse()
  }
  
  const generateId = () => {
    return Math.random().toString(36).substr(2, 9)
  }
  
  return {
    reportError,
    errorQueue,
    isReporting,
    clearErrors,
    getRecentErrors
  }
}

六、异常处理的监控与分析

6.1 错误统计和监控

// utils/errorMonitor.ts
import { ErrorReport } from '@/types/error'

export class ErrorMonitor {
  private static instance: ErrorMonitor
  private errorCounts: Map<string, number> = new Map()
  private errorHistory: ErrorReport[] = []
  private maxHistorySize = 1000
  
  constructor() {
    if (ErrorMonitor.instance) {
      return ErrorMonitor.instance
    }
    
    ErrorMonitor.instance = this
  }
  
  recordError(errorReport: ErrorReport) {
    // 统计错误类型
    const errorType = `${errorReport.type}_${errorReport.component}`
    const count = this.errorCounts.get(errorType) || 0
    this.errorCounts.set(errorType, count + 1)
    
    // 记录错误历史
    this.errorHistory.push(errorReport)
    
    if (this.errorHistory.length > this.maxHistorySize) {
      this.errorHistory.shift()
    }
    
    // 触发错误统计更新事件
    this.emitErrorEvent(errorReport)
  }
  
  getErrorStatistics() {
    return {
      totalErrors: this.errorHistory.length,
      errorTypes: Array.from(this.errorCounts.entries()).map(([type, count]) => ({
        type,
        count
      })),
      recentErrors: this.errorHistory.slice(-10).reverse()
    }
  }
  
  getErrorCount(type: string, component: string) {
    return this.errorCounts.get(`${type}_${component}`) || 0
  }
  
  private emitErrorEvent(errorReport: ErrorReport) {
    // 可以通过事件总线或其他方式通知监控系统
    const event = new CustomEvent('errorRecorded', {
      detail: errorReport
    })
    
    window.dispatchEvent(event)
  }
  
  clearHistory() {
    this.errorHistory = []
    this.errorCounts.clear()
  }
}

// 全局错误监控初始化
export function setupErrorMonitor() {
  const monitor = new ErrorMonitor()
  
  // 监听全局错误事件
  window.addEventListener('errorRecorded', (event: any) => {
    const errorReport = event.detail as ErrorReport
    monitor.recordError(errorReport)
    
    console.log('Error monitored:', errorReport)
  })
  
  return monitor
}

6.2 错误可视化监控面板

<!-- components/ErrorMonitorPanel.vue -->
<template>
  <div class="error-monitor-panel">
    <h2>错误监控面板</h2>
    
    <div class="stats-summary">
      <div class="stat-card">
        <h3>总错误数</h3>
        <p>{{ stats.totalErrors }}</p>
      </div>
      
      <div class="stat-card">
        <h3>今日错误</h3>
        <p>{{ todayErrorCount }}</p>
      </div>
      
      <div class="stat-card">
        <h3>错误类型</
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000