vue3处理错误上报

61 阅读6分钟
// src/utils/errorReporter.ts
 
import { getErrorReportApi } from '@/api/common'
 
// 定义错误数据的类型,描述不同类型错误的字段
interface ErrorData {
  type: 'script' | 'resource' | 'promise' | 'manual' // 错误类型:脚本、资源、Promise、手动
  message?: string // 错误消息
  source?: string // 错误来源文件
  line?: number // 错误行号
  column?: number // 错误列号
  stack?: string // 错误堆栈
  url?: string // 资源错误的 URL(如图片 src)
  tag?: string // 资源错误的标签名(如 IMG)
  reason?: string // Promise 错误的拒绝原因
  [key: string]: any // 允许额外的自定义字段,增强扩展性
}
 
// 上报数据的完整类型,继承 ErrorData 并添加通用字段
interface ReportPayload extends ErrorData {
  timestamp: number // 错误发生的时间戳
  url: string // 当前页面 URL
  userAgent: string // 浏览器信息
  userId: string // 用户 ID
}
 
// 配置选项的类型,用于构造函数的可选参数
interface ReporterOptions {
  maxPerSecond?: number // 每秒最大上报次数(限流)
  sampleRate?: number // 采样率,0.0 到 1.0
}
 
// 错误上报接口,使用你的 request 方法
 
// 错误上报类,用于捕获和上报各种前端错误
class ErrorReporter {
  private queue: ReportPayload[] = [] // 待发送的错误队列,用于限流批量发送
 
  private errorCache: Map<string, boolean> = new Map() // 缓存已上报的错误,用于去重
 
  private maxPerSecond: number // 每秒最大上报次数
 
  private lastSent: number = 0 // 上次发送的时间戳,用于限流判断
 
  private sampleRate: number // 采样率,控制上报比例
 
  // 构造函数,接收可选配置并初始化默认值,去掉 reportUrl
  constructor({ maxPerSecond = 5, sampleRate = 1 }: ReporterOptions = {}) {
    this.maxPerSecond = maxPerSecond // 设置默认每秒最大上报次数
    this.sampleRate = sampleRate // 设置默认采样率
    this.init() // 初始化错误监听
  }
 
  // 初始化全局错误监听,设置各种错误捕获机制
  private init(): void {
    // 捕获全局脚本错误
    window.onerror = (
      message: string | Event, // 错误消息,可能为字符串或事件对象
      source?: string, // 脚本文件路径
      lineno?: number, // 行号
      colno?: number, // 列号
      error?: Error // 错误对象,包含堆栈信息
    ): boolean => {
      this.report({
        type: 'script',
        message: typeof message === 'string' ? message : String(message),
        source,
        line: lineno,
        column: colno,
        stack: error?.stack,
      })
      return true // 返回 true 阻止浏览器默认错误处理(可选)
    }
 
    // 捕获资源加载错误(如图片、脚本加载失败)
    window.addEventListener(
      'error',
      (e: ErrorEvent) => {
        if (e.target !== window) {
          const target = e.target as any
          this.report({
            type: 'resource',
            url: target.src || (target as HTMLLinkElement).href,
            tag: target.tagName,
          })
        }
      },
      true
    )
 
    // 捕获未处理的 Promise 拒绝错误
    window.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => {
      this.report({
        type: 'promise',
        reason: e.reason?.message || String(e.reason),
        stack: e.reason?.stack,
      })
    })
 
    // 页面卸载时发送队列中剩余的错误
    window.addEventListener('unload', () => {
      if (this.queue.length) {
        // 使用 sendBeacon 异步发送,确保页面关闭时数据不丢失
        navigator.sendBeacon('/api/error/report', JSON.stringify(this.queue))
      }
    })
 
    // 每小时清理 errorCache,避免内存占用过大
    setInterval(() => this.errorCache.clear(), 60 * 60 * 1000)
  }
 
  // 公共方法:手动上报标准 Error 对象
  public reportError(error: Error, extra: Record<string, any> = {}): void {
    this.report({
      type: 'manual',
      message: error.message,
      stack: error.stack,
      ...extra,
    })
  }
 
  // 公共方法:上报非标准错误
  public reportManual(data: Omit<ErrorData, 'type'>): void {
    this.report({
      type: 'manual',
      ...data,
    })
  }
 
  // 私有方法:核心上报逻辑,将错误添加到队列并触发限流发送
  private report(data: ErrorData): void {
    // 采样:根据 sampleRate 随机决定是否上报
    if (Math.random() > this.sampleRate) return
 
    // 去重:生成唯一键,检查是否已上报过
    const errorKey = `${data.type}-${data.message}-${data.stack?.substring(0, 50) || ''}`
    if (this.errorCache.has(errorKey)) return
    this.errorCache.set(errorKey, true)
 
    // 构造完整的上报数据
    const payload: ReportPayload = {
      timestamp: Date.now(),
      url: window.location.href,
      userAgent: navigator.userAgent,
      userId: localStorage.getItem('userId') || 'anonymous',
      ...data,
    }
 
    // 开发环境下打印日志,便于调试
    if (process.env.NODE_ENV === 'development') {
      console.log('Error reported:', payload)
    }
 
    // 添加到发送队列
    this.queue.push(payload)
    this.throttleSend()
  }
 
  // 私有方法:限流发送逻辑,使用 getErrorReportApi 上报
  private throttleSend(): void {
    const now = Date.now()
    if (now - this.lastSent < 1000) {
      setTimeout(() => this.throttleSend(), 1000 - (now - this.lastSent))
      return
    }
 
    const batch = this.queue.splice(0, this.maxPerSecond)
    if (batch.length) {
      getErrorReportApi(batch) // 使用你的 API 上报
        .catch((err) => console.error('错误上报失败:', err))
      this.lastSent = now
    }
  }
}
 
// 单例模式导出
const reporter = new ErrorReporter()
export default reporter
 
 
// main.ts
import globalComponents from '@/components';
import ArcoVue from '@arco-design/web-vue';
import ArcoVueIcon from '@arco-design/web-vue/es/icon';
import { createApp } from 'vue';
import App from './App.vue';
import directive from './directive';
import i18n from './locale';
import './mock';
import router from './router';
import store from './store';
import errorReporter from './utils/errorReporter'; // 单例实例
 
import '@/assets/style/global.less';
import 'virtual:uno.css';
 
const app = createApp(App);
 
app.use(ArcoVue, {});
app.use(ArcoVueIcon);
app.use(router);
app.use(store);
app.use(i18n);
app.use(globalComponents);
app.use(directive);
 
// 配置 Vue 全局错误处理
app.config.errorHandler = (err: unknown, instance, info: string) => {
  if (err instanceof Error) {
    errorReporter.reportError(err, { vueInfo: info }); // 上报标准 Error
  } else {
    errorReporter.reportManual({ message: String(err), vueInfo: info }); // 上报非标准错误
  }
};
 
app.mount('#app');
组件中使用
 
<!-- src/components/TestComponent.vue -->
<template>
  <button @click="testError">测试错误</button>
</template>
 
<script lang="ts">
import { defineComponent } from 'vue';
import errorReporter from '@/utils/errorReporter';
 
export default defineComponent({
  name: 'TestComponent',
  methods: {
    testError(): void {
      try {
        throw new Error('手动测试错误');
      } catch (e) {
        // 手动上报,附加上下文信息
        errorReporter.reportError(e as Error, { context: '按钮点击' });
      }
    },
  },
});
</script>
自定义配置
// src/main.ts
import errorReporter from './utils/errorReporter';
 
// 自定义配置
const customReporter = new ErrorReporter({
  reportUrl: 'https://api.example.com/error',
  maxPerSecond: 10,
  sampleRate: 0.1, // 10% 采样率
});
测试 app.vue
 
onMounted(() => {
  throw new Error('测试全局脚本错误')
})
上报结果
 
标准错误(reportError)
 
errorReporter.reportError(err, { vueInfo: info }, customData);
 
err:一个 Error 对象(如 new Error('测试错误'))。
{ vueInfo: info }:附加上下文。
customData:可选的自定义数据。
返回的数据结构(后端收到的)
 
假设 err = new Error('测试错误'),info = 'mounted',customData = { requestId: 'abc123' },后端收到的可能是:
 
 
[
  {
    "timestamp": 16987654321,
    "url": "http://localhost:3000/test",
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
    "userId": "anonymous",
    "type": "manual",
    "message": "测试错误",
    "stack": "Error: 测试错误\n    at testFunction (file:///src/main.ts:10:15)\n    at ...",
    "vueInfo": "mounted",
    "customData": {
      "requestId": "abc123"
    }
  }
]
 
说明:由于限流设计,后端收到的是批量数据(数组),这里示例中只有一个错误。
字段含义
 
timestamp:错误发生的时间戳(毫秒),如 16987654321,表示错误的具体时间。
url:当前页面地址,如 "http://localhost:3000/test",告诉后端错误发生在哪个页面。
userAgent:浏览器信息,标识客户端环境,便于排查浏览器相关问题。
userId:用户 ID,默认从 localStorage 获取,若无则为 "anonymous",用于关联用户。
type:错误类型,这里是 "manual",表示手动上报(区别于自动捕获的 script、resource 等)。
message:错误消息,从 err.message 提取,如 "测试错误",描述错误内容。
stack:错误堆栈,从 err.stack 提取,提供调用栈信息,帮助定位代码位置。
vueInfo(来自 extra):Vue 上下文,如 "mounted",表示错误发生在 mounted 生命周期。
customData:自定义数据,如 { "requestId": "abc123" },告诉后端前端传的具体业务数据。


自定义错误(report)
 
errorReporter.report({ type: 'manual', message: String(err), vueInfo: info, customData });
 
直接传入一个 ErrorData 对象,手动指定字段。
返回的数据结构(后端收到的)
 
假设 err = '自定义异常',info = 'fetchData',customData = { status: 404 },后端收到的可能是:
 
[
  {
    "timestamp": 16987654322,
    "url": "http://localhost:3000/test",
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
    "userId": "anonymous",
    "type": "manual",
    "message": "自定义异常",
    "vueInfo": "fetchData",
    "customData": {
      "status": 404
    }
  }
]
 
字段含义
 
timestamp:错误时间戳,如 16987654322,同上。
url:当前页面 URL,同上。
userAgent:浏览器信息,同上。
userId:用户 ID,同上。
type:错误类型,手动指定为 "manual",表示自定义上报。
message:错误消息,手动转为字符串,如 "自定义异常",描述错误内容。
vueInfo:自定义附加信息,如 "fetchData",表示错误发生在数据获取逻辑中。
customData:自定义数据,如 { "status": 404 },告诉后端相关业务信息。
(注意):没有 stack,因为自定义错误不依赖 Error 对象,除非手动传入。
错误上报
 
错误传递的是两种
1 标准的错误(前端正常的err)
2 自定义的错误
 
“标准错误” vs “自定义错误”
 
标准错误(reportError):
指的是 JavaScriptError 对象(或其子类,如 TypeErrorReferenceError)。
优点:自带 message 和 stack,提供详细的堆栈跟踪。
使用 reportError 方法,自动解析这些属性。
适用于:try-catch 捕获的异常、Vue 的 errorHandler 抛出的错误。
 
自定义错误(report):
指的是开发者手动构造的错误数据,不一定是 Error 对象。
优点:完全灵活,可以上报任何格式的数据,不依赖堆栈。
使用 report 方法,需手动指定字段。
适用于:非标准异常(如字符串提示)、业务逻辑错误、自定义日志。