如何设计一个 Vue 应用的错误监控系统,能够捕获组件渲染错误、异步请求错误,并进行精准的错误上报?

各位程序猿、攻城狮、代码艺术家们,晚上好!今天咱们不开会,搞个轻松的“错误逮捕计划”——聊聊怎么给 Vue 应用装个火眼金睛,精准捕捉那些偷偷摸摸的 Bug。

咱们的目标是:让 Vue 应用发生的错误,都能像犯了事儿的小偷一样,被我们抓个正着,然后乖乖上报,方便我们快速定位问题,提高开发效率,避免用户流失。

第一步:错误监控的“地基”——全局错误处理

Vue 提供了一个全局错误处理的钩子函数 Vue.config.errorHandler。这玩意儿就像是咱们的报警系统总开关,任何未被捕获的错误都会触发它。

import Vue from 'vue'

Vue.config.errorHandler = (err, vm, info) => {
  // `err`:错误对象
  // `vm`:发生错误的组件实例
  // `info`:Vue 特定的错误信息,例如错误发生在哪一个生命周期钩子中

  console.error('全局错误捕获:', err, vm, info)

  // 在这里可以进行错误上报,比如调用上报函数 reportError(err, vm, info)
  reportError(err, vm, info)
}

function reportError(err, vm, info) {
  // 封装你的错误上报逻辑,比如发送到服务器
  // 可以使用 fetch、axios 等工具

  const errorData = {
    message: err.message,
    stack: err.stack,
    component: vm ? vm.$options.name : 'Unknown Component',
    info: info,
    url: window.location.href, //当前页面url
    userAgent: navigator.userAgent, //用户浏览器信息
    timestamp: new Date().toISOString() // 错误发生时间
  }

  // 使用 fetch 发送错误信息到服务器
  fetch('/api/error-report', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(errorData)
  })
    .then(response => {
      if (!response.ok) {
        console.error('错误上报失败:', response.status, response.statusText)
      }
    })
    .catch(error => {
      console.error('错误上报请求失败:', error)
    })
}

第二步:异步错误的“天罗地网”——Promise 拒绝处理

Promise 就像是异步操作的“承诺”,但如果这个“承诺”没能兑现(Promise rejected),我们需要确保能抓住这个错误。

window.addEventListener('unhandledrejection', event => {
  // `event.reason`:Promise rejected 的原因(错误对象)
  // `event.promise`:发生 reject 的 Promise 对象

  console.error('未处理的 Promise 拒绝:', event.reason, event.promise)

  reportError(event.reason, null, 'Unhandled Promise Rejection') //同样上报
  event.preventDefault(); // 阻止控制台默认输出,避免刷屏
});

第三步:组件内部的“精确定位”——try...catcherrorCaptured

  1. try...catch 这是最简单粗暴的方式,直接把可能出错的代码包裹起来。

    <template>
      <div>
        <button @click="handleClick">点击我</button>
        <p>{{ message }}</p>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          message: ''
        }
      },
      methods: {
        handleClick() {
          try {
            this.message = this.undefinedVariable.toUpperCase(); // 故意制造一个错误
          } catch (error) {
            console.error('组件内部错误:', error)
            reportError(error, this, 'Component Method Error');
          }
        }
      }
    }
    </script>
  2. errorCaptured 这是 Vue 2.5+ 提供的组件选项,允许父组件捕获子组件的错误。它有点像“老大哥”的角色,时刻关注着“小弟”们的情况。

    // 父组件
    <template>
      <div>
        <ChildComponent />
      </div>
    </template>
    
    <script>
    import ChildComponent from './ChildComponent.vue'
    
    export default {
      components: {
        ChildComponent
      },
      errorCaptured(err, vm, info) {
        // `err`:错误对象
        // `vm`:发生错误的子组件实例
        // `info`:错误信息
    
        console.error('子组件错误捕获:', err, vm, info)
        reportError(err, vm, info)
    
        // 返回 `false` 阻止错误继续向上冒泡
        return false; // 通常阻止冒泡
      }
    }
    </script>
    
    // 子组件 (ChildComponent.vue)
    <template>
      <div>
        {{ undefinedVariable.toUpperCase() }}  <!-- 故意制造一个错误 -->
      </div>
    </template>

第四步:Vue 3 的 “升级版” 错误处理

Vue 3 在错误处理方面也做了一些改进,例如:

  • app.config.errorHandler 用法与 Vue 2 的 Vue.config.errorHandler 类似,但它是针对特定 Vue 应用实例的。

  • onErrorCaptured 用法和 Vue2 的 errorCaptured 类似,但是需要使用 composition API。

    <script setup>
    import { onErrorCaptured } from 'vue';
    
    onErrorCaptured((err, instance, info) => {
      // 处理错误
      console.log("onErrorCaptured", err, instance, info);
    });
    </script>

第五步:错误上报的“姿势”——数据格式和上报策略

  1. 数据格式: 咱们前面 reportError 函数里已经定义了一个基本的数据格式,但实际情况可能需要更丰富的信息。 比如说,你可以添加用户信息、页面 URL、浏览器信息等等。

    const errorData = {
      message: err.message,
      stack: err.stack,
      component: vm ? vm.$options.name : 'Unknown Component',
      info: info,
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: new Date().toISOString(),
      userId: getUserId(), // 获取用户 ID
      route: this.$route ? this.$route.fullPath : 'Unknown Route' // 当前路由
    }
  2. 上报策略: 频繁的上报可能会给服务器带来压力,所以我们需要制定一些上报策略。

    • 防抖/节流: 避免短时间内重复上报同一个错误。
    • 抽样上报: 只上报一部分错误,例如 10% 的错误。
    • 合并上报: 将多个错误合并成一个上报。
    • 错误级别: 根据错误级别来决定是否上报。
    // 简单的防抖实现
    let timer = null;
    function debouncedReportError(err, vm, info) {
      if (timer) {
        clearTimeout(timer);
      }
      timer = setTimeout(() => {
        reportError(err, vm, info);
        timer = null;
      }, 500); // 500ms 防抖时间
    }

第六步:错误监控的“武器库”——第三方工具

当然,咱们也可以借助一些强大的第三方工具来简化错误监控流程。

工具名称 优点 缺点
Sentry 功能强大,支持多种语言和框架,提供详细的错误报告和上下文信息。 商业产品,需要付费。
Bugsnag 界面友好,易于使用,提供实时错误报告和用户影响分析。 商业产品,需要付费。
Raven.js (Sentry 的前端 SDK) 专门为 JavaScript 错误监控设计,与 Sentry 无缝集成。 依赖 Sentry 服务。
Fundebug 国内的错误监控平台,提供符合国内用户习惯的服务和支持。 功能相对较少,不如 Sentry 和 Bugsnag 强大。

第七步:代码示例,一个完整的 “错误逮捕计划”

// error-handler.js

import Vue from 'vue'

// 获取用户 ID (假设你有这个函数)
function getUserId() {
  // 从本地存储、cookie 或者其他地方获取用户 ID
  return localStorage.getItem('userId') || '游客';
}

// 错误上报函数
function reportError(err, vm, info) {
  const errorData = {
    message: err.message,
    stack: err.stack,
    component: vm ? vm.$options.name : 'Unknown Component',
    info: info,
    url: window.location.href,
    userAgent: navigator.userAgent,
    timestamp: new Date().toISOString(),
    userId: getUserId(),
    route: window.location.pathname + window.location.search // 获取当前路由
  };

  // 使用 fetch 发送错误信息到服务器
  fetch('/api/error-report', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(errorData)
  })
    .then(response => {
      if (!response.ok) {
        console.error('错误上报失败:', response.status, response.statusText);
      }
    })
    .catch(error => {
      console.error('错误上报请求失败:', error);
    });
}

// 防抖函数
function debounce(func, delay) {
  let timer;
  return function (...args) {
    const context = this;
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
}

// 防抖后的上报函数
const debouncedReportError = debounce(reportError, 500);

// 全局错误处理
Vue.config.errorHandler = (err, vm, info) => {
  console.error('全局错误捕获:', err, vm, info);
  debouncedReportError(err, vm, info);
};

// Promise 拒绝处理
window.addEventListener('unhandledrejection', event => {
  console.error('未处理的 Promise 拒绝:', event.reason, event.promise);
  debouncedReportError(event.reason, null, 'Unhandled Promise Rejection');
  event.preventDefault(); // 阻止控制台默认输出
});

export { reportError }; // 导出 reportError 函数,方便在组件中使用

然后在 main.js 中引入这个文件:

// main.js
import Vue from 'vue'
import App from './App.vue'
import './error-handler' // 引入错误处理文件

new Vue({
  render: h => h(App),
}).$mount('#app')

最后,在组件中使用 try...catcherrorCaptured 来捕获组件内部的错误,并调用 reportError 函数进行上报。

第八步:锦上添花——Source Map

当咱们在生产环境中使用压缩后的代码时,错误堆栈信息会变得难以阅读。 这时候,Source Map 就派上用场了。 它可以将压缩后的代码映射回原始代码,让咱们能够轻松定位错误发生的位置。

  1. 配置 Webpack:webpack.config.js 中配置 devtool 选项。

    module.exports = {
      // ...
      devtool: 'source-map', // 或者 'hidden-source-map' (生产环境推荐)
      // ...
    };
  2. 上传 Source Map: 将生成的 Source Map 文件上传到错误监控平台,例如 Sentry 或 Bugsnag。

总结:

一个完善的 Vue 错误监控系统,需要包括全局错误处理、Promise 拒绝处理、组件内部错误捕获、错误上报策略和 Source Map 等多个方面。 选择合适的第三方工具,可以大大简化开发流程。

希望今天的“错误逮捕计划”能帮助大家构建更健壮、更可靠的 Vue 应用! 散会!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注