Vue3异常监控

简介: 本文主要记录 Vue3 项目部署后如果发生报错及异常,如何获取到详细的错误信息,并自动通知相关开发人员进行处理。

本文主要记录 Vue3 项目部署后如果发生报错及异常,如何获取到详细的错误信息,并自动通知相关开发人员进行处理。《项目地址》,让我们开始吧!


痛点


通常我们在本地进行开发的时候,如果发生错误或者异常,Vue3 会在浏览器给我们一个堆栈错误信息的提示


网络异常,图片无法展示
|


并且可以看到该错误发生在哪个组件,哪个文件的哪一行


网络异常,图片无法展示
|


点击文件会在控制台 Sources 打开该文件并定位到错误位置


网络异常,图片无法展示
|


这极大的方便了我们查找错误原因的过程,但是当我们的代码部署到服务器上的时候,通常为了项目优化,不会进行 sourcemap 文件的打包,这个时候,错误信息通常是这个样子的


网络异常,图片无法展示
|


是不是一脸茫然?点开对应的文件也是打包压缩后的代码,根本无从查看错误信息并对应到源代码的具体文件及具体发生错误位置。 不仅如此,当你收到业务反馈的问题,但是自己打开浏览器一番操作,并没有遇到问题的时候,是不是觉得无从下手了呢? 接下来我们要做的就是当线上环境报错的时候,收集错误信息,并及时通知开发人员。


浏览器中捕获异常通常通过以下方法


addEventListener('error', callback)||window.onerror= callback
复制代码

这里不再赘述。


Vue3 向我们提供了API errorHandler 进行异常捕获。


说明:我的后台服务是用 Node.jseggjs 框架创建的,你可以用任何自己熟悉的后端语言和框架编写,逻辑也很简单。


主要功能点:


1. 打包时生成sourcemap文件上传到后台


这里我采用了 Vite 创建 Vue3 项目,因为 Vue3 使用 rollup 作为模块打包器,所以这里需要编写一个 rollup 插件,作用是打包完成后读取所有sourcemap 上传到后台,并将打包输入目录中的 sourcemap 文件删除,减少线上环境的资源请求


vite.config.js 中引入及使用


import { loadEnv } from "vite";
import vue from '@vitejs/plugin-vue'
// 引入upload sourcemap rollup plugin
import uploadSourceMap from "./src/plugins/rollup-plugin-upload-sourcemap";
// mode 当前环境 development production
export default ({ mode }) => {
  const env = loadEnv(mode, process.cwd());
  return {
    server: {
      open: true,
      port: 3000,
      host: "0.0.0.0",
      proxy: {
        // 本地测试异常监控用
        "/mointor": {
          target: 'http://127.0.0.1:7001',
          changeOrigin: true,
        },
      },
    },
    plugins: [
      vue(),
      // 使用upload sourcemap rollup plugin
      uploadSourceMap({
        // 基本路径,判断当前环境使用
        baseUrl: env.VITE_BASE_API,
        // 处理目标文件夹接口地址
        handleTargetFolderUrl: `${env.VITE_MONITOR_UPLOAD_API}/emptyFolder`,
        // 上传sourcemap文件接口地址
        uploadUrl: `${env.VITE_MONITOR_UPLOAD_API}/uploadSourceMap`,
      }),
    ],
    build: {
      // 构建后是否生成 source map 文件
      sourcemap: true,
    }
  }}
复制代码


rollup-plugin-upload-sourcemap.js


import glob from "glob";
import path from "path";
import fs from "fs";
import http from "http";
export default function uploadSourceMap({
  // 基础接口地址
  baseUrl,
  // 处理目标文件夹接口地址
  handleTargetFolderUrl,
  // 上传sourcemap文件地址
  uploadUrl
}) {
  return {
    name: "upload-sourcemap",
    // 打包完成后钩子
    closeBundle() {
      console.log('closeBundle');
      // 获取当前环境
      let env = "uat";
      if (baseUrl === "production_base_api") {
        env = "prod";
      }
      // 上传文件方法
      function upload(url, file, env) {
        return new Promise((resolve) => {
          const req = http.request(
            `${url}?name=${path.basename(file)}&&env=${env}`,
            {
              method: "POST",
              headers: {
                "Content-Type": "application/octet-stream",
                Connection: "keep-alive",
                "Transfer-Encoding": "chunked",
              },
            }
          );
          // 读取文件并给到上送请求对象
          fs.createReadStream(file)
            .on("data", chunk => {
              req.write(chunk);
            })
            .on("end", () => {
              req.end();
              resolve("end");
            });
        });
      }
      // 处理目标文件夹(没有创建,有则清空)
      function handleTargetFolder() {
        http.get(`${handleTargetFolderUrl}?env=${env}`, () => {
            console.log("handleTargetFolderUrl success");
          })
          .on("error", (e) => {
            console.log(`handle folder error: ${e.message}`);
          });
      }
      handleTargetFolder();
      // 读取sourcemap文件 上传并删除
      async function uploadDel() {
        const list = glob.sync(path.join("./dist", "./**/*.{js.map,}"));
        for (const filename of list) {
          await upload(uploadUrl, filename, env);
          await fs.unlinkSync(filename);
        }
      }
      uploadDel();
    },
  };
}
复制代码


2. 后台服务接收上传的sourcemap文件并保存


// 前端打包时,上送sourcemap文件
async uploadSourceMap() {
  const { ctx } = this;
  const stream = ctx.req,
    filename = ctx.query.name,
    env = ctx.query.env;
  // 要上传的目标路径
  const dir = path.join(this.config.baseDir, `upload/${env}`);
  // 目标文件  const target = path.join(dir, filename);
  // 写入文件内容  const writeStream = fs.createWriteStream(target);
  stream.pipe(writeStream);
}
复制代码


3. 利用errorHandler进行异常捕获并上送报错信息到后台


main.js 中引入及使用


import handleError from "./utils/monitor";
const app = createApp(App)
// 异常监控上送报错信息    接口地址
handleError(app, import.meta.env.VITE_MONITOR_REPORT_API);
复制代码


mointor.js


import axios from "axios";
// 获取浏览器信息
function getBrowserInfo() {
  const agent = navigator.userAgent.toLowerCase();
  const regIE = /msie [\d.]+;/gi;
  const regIE11 = /rv:[\d.]+/gi;
  const regFireFox = /firefox/[\d.]+/gi;
  const regQQ = /qqbrowser/[\d.]+/gi;
  const regEdg = /edg/[\d.]+/gi;
  const regSafari = /safari/[\d.]+/gi;
  const regChrome = /chrome/[\d.]+/gi;
  // IE10及以下
  if (regIE.test(agent)) {
    return agent.match(regIE)[0];
  }
  // IE11
  if (regIE11.test(agent)) {
    return "IE11";
  }
  // firefox
  if (regFireFox.test(agent)) {
    return agent.match(regFireFox)[0];
  }
  // QQ
  if (regQQ.test(agent)) {
    return agent.match(regQQ)[0];
  }
  // Edg
  if (regEdg.test(agent)) {
    return agent.match(regEdg)[0];
  }
  // Chrome
  if (regChrome.test(agent)) {
    return agent.match(regChrome)[0];
  }
  // Safari
  if (regSafari.test(agent)) {
    return agent.match(regSafari)[0];
  }}
// 捕获报错方法
export default function handleError(Vue,baseUrl) {
  if (!baseUrl) {
    console.log("baseUrl", baseUrl);
    return;
  }
  Vue.config.errorHandler = (err, vm) => {
    // 获取当前环境
    let environment = "测试环境";
    if (import.meta.env.VITE_BASE_API === "production_base_api") {
      environment = "生产环境";
    }
    // 发送请求上送报错信息
    axios({
      method: "post",
      url: `${baseUrl}/reportError`,
      data: {
        environment,
        location: window.location.href,
        message: err.message,
        stack: err.stack,
        // 浏览器信息
        browserInfo: getBrowserInfo(),
        // 以下信息可以放在vuex store中维护
        // 用户ID
        userId:"001",
        // 用户名称
        userName:"张三",
        // 路由记录
        routerHistory:[
          {
            fullPath:"/login",
            name:"Login",
            query:{},
            params:{},
          },{
            fullPath:"/home",
            name:"Home",
            query:{},
            params:{},
          }
        ],
        // 点击记录
        clickHistory:[
          {
            pageX:50,
            pageY:50,
            nodeName:"div",
            className:"test",
            id:"test",
            innerText:"测试按钮"
          }
        ],
      },
    });
  };}
复制代码


4. 后台收到报错结合sourcemap文件解析错误信息通知开发人员


// 前端报错,上报
errorasync reportError() {
  const { ctx } = this;
  const { environment, location, message, stack, browserInfo, userId, userName, routerHistory, clickHistory } = ctx.request.body;
  let env = '';
  if (environment === '测试环境') {
    env = 'uat';
  } else if (environment === '生产环境') {
    env = 'prod';
  }
  // 组合sourcemap文件路径
  const sourceMapDir = path.join(this.config.baseDir, `upload/${env}`);
  // 解析报错信息
  const stackParser = new StackParser(sourceMapDir);
  let routerHistoryStr = '<h3>router history</h3>',
    clickHistoryStr = '<h3>click history</h3>';
    // 组合路由历史信息
  routerHistory && routerHistory.length && routerHistory.forEach(item => {
    routerHistoryStr += `<p>name:${item.name} | fullPath:${item.fullPath}</p>`;
    routerHistoryStr += `<p>params:${JSON.stringify(item.params)} | query:${JSON.stringify(item.query)}</p><p>--------------------</p>`;
  });
  // 组合点击历史信息
  clickHistory && clickHistory.length && clickHistory.forEach(item => {
    clickHistoryStr += `<p>pageX:${item.pageX} | pageY:${item.pageY}</p>`;
    clickHistoryStr += `<p>nodeName:${item.nodeName} | className:${item.className} | id:${item.id}</p>`;
    clickHistoryStr += `<p>innerText:${item.innerText}</p><p>--------------------</p>`;
  });
  // 通过上送的sourcemap文件,配合error信息,解析报错信息
  const errInfo = await stackParser.parseStackTrack(stack, message);
  // 获取当前时间
  const now = new Date();
  const time = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()} ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`;
  // 组织邮件正文
  const mailMsg = `
  <h3>message:${message}</h3>
  <h3>location:${location}</h3>
  <p>source:${errInfo.source}</p>
  <p>line::${errInfo.lineNumber}</p>
  <p>column:${errInfo.columnNumber}</p>
  <p>fileName:${errInfo.fileName}</p>
  <p>functionName:${errInfo.functionName}</p>
  <p>time::${time}</p>
  <p>browserInfo::${browserInfo}</p>
  <p>userId::${userId}</p>
  <p>userName::${userName}</p>
  ${routerHistoryStr}
  ${clickHistoryStr}
  `;
  // sendMail('发件箱地址', '发件箱授权码', '收件箱地址', 主题 environment, 正文 mailMsg);
  sendMail('发件箱地址', '发件箱授权码', '收件箱地址', environment, mailMsg);
  ctx.body = {
    header: {
      code: 0,
      message: 'OK',
    }
  };
  ctx.status = 200;
}
复制代码


5. 发送邮件方法


'use strict';
const nodemailer = require('nodemailer');
// 发送邮件方法
function sendMail(from, fromPass, receivers, subject, msg) {
  const smtpTransport = nodemailer.createTransport({
    host: 'smtp.qq.email',
    service: 'qq',
    secureConnection: true,
    // use SSL
    secure: true,
    port: 465,
    auth: {
      user: from,
      pass: fromPass,
    },
  });
  smtpTransport.sendMail({
    from,
    // 收件人邮箱,多个邮箱地址间用英文逗号隔开
    to: receivers,
    // 邮件主题
    subject,
    // 邮件正文
    html: msg,
  }, err => {
    if (err) {
      console.log('send mail error: ', err);
    }
  });
}
module.exports = sendMail;
复制代码


End


至此,Vue3 的异常监控就完成了,后续会写 Vue3 项目从零到一创建及搭建一个基于 Vite+Vue3+Vue Router+Vuex+TS+Element3+axios+Jest+Cypress 的后台管理系统,感兴趣的点个关注吧!


关于本文有任何问题或建议,欢迎留言讨论!

相关文章
|
26天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
128 64
|
26天前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
108 60
|
1天前
|
JavaScript API 数据处理
vue3使用pinia中的actions,需要调用接口的话
通过上述步骤,您可以在Vue 3中使用Pinia和actions来管理状态并调用API接口。Pinia的简洁设计使得状态管理和异步操作更加直观和易于维护。无论是安装配置、创建Store还是在组件中使用Store,都能轻松实现高效的状态管理和数据处理。
12 3
|
26天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
30 8
|
26天前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
28 1
|
26天前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
35 1
|
26天前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
1月前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
1月前
|
JavaScript 索引
Vue 3.x 版本中双向数据绑定的底层实现有哪些变化
从Vue 2.x的`Object.defineProperty`到Vue 3.x的`Proxy`,实现了更高效的数据劫持与响应式处理。`Proxy`不仅能够代理整个对象,动态响应属性的增删,还优化了嵌套对象的处理和依赖追踪,减少了不必要的视图更新,提升了性能。同时,Vue 3.x对数组的响应式处理也更加灵活,简化了开发流程。
|
1月前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
78 7