专栏名称: 前端大全
分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯
目录
相关文章推荐
前端早读课  ·  【第3452期】React 开发中使用开闭原则 ·  6 小时前  
启四说  ·  启四VIP策略网站,有哪些功能?如何使用? ·  17 小时前  
启四说  ·  启四VIP策略网站,有哪些功能?如何使用? ·  17 小时前  
前端早读课  ·  【第3451期】前端 TypeError ... ·  昨天  
江苏司法行政在线  ·  宿迁司法行政人、江苏监狱戒毒民警,给您拜年啦! ·  3 天前  
江苏司法行政在线  ·  宿迁司法行政人、江苏监狱戒毒民警,给您拜年啦! ·  3 天前  
51好读  ›  专栏  ›  前端大全

什么?原来前端错误上报这么简单!!

前端大全  · 公众号  · 前端  · 2024-08-16 11:50

正文

作者:用户不会像你这么操作的

https://juejin.cn/post/7383955685368086562

前言

项目上线之后,用户如果出现错误(代码报错、资源加载失败以及其他情况),基本上没有办法复现,如果用户出了问题但是不反馈或直接不用了,对开发者或公司来说都是损失。

由于我这个项目比较小,只是一个迷你商城,所以不需要收集很复杂的数据,只需要知道有没有资源加载失败、哪行代码报错就可以了,市面上有很多现成的监控平台比如sentry,在这里我选择通过nodejs自己搭一个服务。

概述

我的项目是使用Vue2写的,所以本文主要是讲Vue相关的部署过程

1、部署后台服务(使用express)

2、收集前端错误(主要是Vue)

3、提交信息到后台分析源码位置及记录日志

js异常处理

function test1 ({
    console.log('test1 Start');
    console.log(a);
    console.log('test1 End');
}

function test2 ({
    console.log('test2 Start');
    console.log('test2 End');
}

test1();
test2();

这里可以看到,当js运行报错后,代码就不往下执行了,这是因为js是单线程,具体可以看看事件循环,这里不做解释

接下来看看使用异步的方式执行,可以看到没有影响代码的继续运行

function test1 ({
    console.log('test1 Start');
    console.log(a);
    console.log('test1 End')
}

function test2 ({
    console.log('test2 Start');
    console.log('test2 End')
}

setTimeout(() => {
    test1();
}, 0)

setTimeout(() => {
    test2();
}, 0)

那报错之后我们如何收集错误呢?

try catch

function test1 ({
    console.log('test1 Start');
    console.log(a);
    console.log('test1 End')
}

try {
  test1();
catch (e) {
  console.log(e);
}

使用 try catch 将代码包裹起来之后,当运行报错时,会将收集到的错误传到catch的形参中,打印之后我们可以拿到错误信息和错误的堆栈信息,但是 try catch 无法捕获到异步的错误

function test1 ({
    console.log('test1 Start');
    console.log(a);
    console.log('test1 End')
}

try {
  setTimeout(function({
    test1();
  }, 100);
catch (e) {
  console.log(e);
}

可以看到 try catch 是无法捕获到异步错误的,这时候就要用到 window error 事件

监听error事件

window.addEventListener('error', args => {
  console.log(args);
  return  true;
}, true)

function test1 ({
    console.log('test1 Start');
    console.log(a);
    console.log('test1 End')
}

setTimeout(function({
  test1();
}, 100);

除了 window.addEventListener 可以监听 error 之后, window.onerror 也可以监听 error ,但是 window.onerror window.addEventListener 相比,无法监听网络异常

window.addEventListener

<img src="https://www.baidu.com/abcdefg.gif">
<script>
  window.addEventListener('error', args => {
    console.log(args);
    return true;
  }, true// 捕获
script>

window.onerror

"https://www.baidu.com/abcdefg.gif">
<script>
  window.onerror = function(...args{
    console.log(args);
  }
script>

由于无法监听到,这里就不放图了

unhandledrejection

到目前为止, Promise 已经成为了开发者的标配,加上新特性引入了 async await ,解决了回调地狱的问题,但 window.onerror window.addEventListener ,对 Promise 报错都是无法捕获

window.addEventListener('error', error => {
  console.log('window', error);
})
new Promise((resolve, reject) => {
  console.log(a);
}).catch(error => {
  console.log('catch', error);
})


可以看到,监听 window 上的 error 事件是没有用的,可以每一个 Promise 写一个 catch ,如果觉得麻烦,那么就要使用一个新的事件, unhandledrejection

window.addEventListener('unhandledrejection', error => {
  console.log('window', error);
})
new Promise((resolve, reject) => {
  console.log(a);
})

其中, reason 中存放着错误相关信息, reason.message 是错误信息, reason.stack 是错误堆栈信息

Promise错误也可以使用 try catch捕获到,这里就不做演示了

至此,js中 同步 异步 资源加载 Promise async/await 都有相对应的捕获方式

window.addEventListener('unhandledrejection', error => {
  console.log('window', error);
  throw error.reason;
})

window.addEventListener('error', error => {
  console.log(error);
  return true;
}, true)

vue异常处理

由于我的项目使用Vue2搭建的,所以还需要处理一下vue的报错

export default {
  name'App',
  mounted() {
    console.log(aaa);
  }
}

现在的项目基本上都是工程化的,通过工程化工具打包出来的代码长这样,上面的代码打包后运行

通过报错提示的js文件,查看后都是压缩混淆之后的js代码,这时候就需要打包时生成的 source map 文件了,这个文件中保存着打包后代码和源码对应的位置,我们只需要拿到报错的堆栈信息,通过转换,就能通过 source map 找到对应我们源码的文件及出错的代码行列信息

那我们怎么才能监听 error 事件呢?

使用Vue的全局错误处理函数 Vue.config.errorHandler

src/main.js 中写入以下代码

Vue.config.errorHandler = (err, vm, info) => {
  console.log('Error: ', err);
  console.log('vm', vm);
  console.log('info: ', info);
}

现在打包vue项目

打包vue之后然后通过端口访问 index.html ,不建议你双击打开,如果你没改过打包相关的东西,双击打开是不行的,可以通过 vs code 装插件 live server ,然后将打包文件夹通过 vs code 打开

上报错误数据

经过上述的异常处理后,我们需要将收集到的错误进行整理,将需要的信息发送到后台,我这里选择使用ajax发请求到后端,当然你也可以使用创建一个图片标签,将需要发送的数据拼接到src上

这里我选择使用 tracekit 库来解析错误的堆栈信息, axios 发请求, dayjs 格式化时间

npm i tracekit
npm i axios
npm i dayjs

安装完成后在 src/main.js 中引入 tracekit axios dayjs

上报Vue错误

import TraceKit from 'tracekit';
import axios from 'axios';
import dayjs from 'dayjs';

const protcol = window.location.protocol;
let errorMonitorUrl = `${protcol}//127.0.0.1:9999`;
const errorMonitorVueInterFace = 'reportVueError'// vue错误上报接口
TraceKit.report.subscribe((error) => {
  const { message, stack } = error || {};

  const obj = {
    message,
    stack: {
      column: stack[0].column,
      line: stack[0].line,
      func: stack[0].func,
      url: stack[0].url
    }
  };

  axios({
    method'POST',
    url`${errorMonitorUrl}/${errorMonitorVueInterFace}`,
    data: {
      error: obj,
      data: {
        errTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
        isMobile/iPhone|iPad|iPod|Android/i.test(navigator.userAgent), // 是否移动端
        isWechat/MicroMessenger/i.test(navigator.userAgent), // 是否微信浏览器
        isIOS/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream, // 两个都是false就是未知设备
        isAndroid/Android/.test(navigator.userAgent) && !/Windows Phone/.test(navigator.userAgent)
      },
      browserInfo: {
        userAgent: navigator.userAgent,
        protcol: protcol
      }
    }
  }).then(() => {
    console.log('错误上报成功');
  }).catch(() => {
    console.log('错误上报失败');
  });
});

Vue.config.errorHandler = (err, vm, info) => {
  TraceKit.report(err);
}

如果你还需要其他的数据就自己加

打包vue之后然后通过端口访问 index.html ,不建议你双击打开,如果你没改过打包相关的东西,双击打开是不行的,可以通过 vs code 装插件 live server ,然后将打包文件夹通过 vs code 打开

现在去项目中看看发出去的请求参数是什么

可以看到我们需要的数据都已经收集到了,上报失败是肯定的,因为我们还没有写好接口

上报window错误

接下来在监听 window error 事件,也向后台发送一个错误上报请求

const errorMonitorWindowInterFace = 'reportWindowError'// window错误上报接口
window.addEventListener('error', args => {
  const err = args.target.src || args.target.href;
  const obj = {
    message'加载异常' + err
  };
  if (!err) {
    return true;
  }
  axios({
    method'POST',
    url`${errorMonitorUrl}/${errorMonitorWindowInterFace}`,
    data: {
      error: obj,
      data: {
        errTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
        isMobile/iPhone|iPad|iPod|Android/i.test(navigator.userAgent), // 是否移动端
        isWechat/MicroMessenger/i.test(navigator.userAgent), // 是否微信浏览器
        isIOS/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream, // 两个都是false就是未知设备
        isAndroid/Android/.test(navigator.userAgent) && !/Windows Phone/.test(navigator.userAgent)
      },
      browserInfo: {
        userAgent: navigator.userAgent,
        protcol: protcol
      }
    }
  }).then(() => {
    console.log('错误上报成功');
  }).catch(() => {
    console.log('错误上报失败');
  });
  return true;
}, true);

搭建监控后台

创建一个文件夹,名字随便,然后在终端中打开文件夹,初始化npm

npm init -y

初始化完成后创建一个 server.js ,这里我使用 express 进行搭建后端, source-map 用于解析 js.map 文件,这些库后面会用到

npm i express
npm i nodemon
npm i source-map

下好包之后在 server.js 中输入以下代码,然后在终端输入 nodemon server.js

const express = require('express');
const path = require('path');
const fs = require('fs');

const PORT = 9999;

const app = express();
app.use(express.urlencoded({ extendedtrue }));
app.use(express.json());

app.get('/', (req, res) => {
  res.send('Hello World!').status(200);
})

app.listen(PORT, () => {
  console.log(`服务启动成功,端口号为:${PORT}`)
})


服务启动之后,访问本地的9999端口,查看是否生效,当看到屏幕上显示 Hello World! 表示我们的后端服务成功跑起来了,接下来就是写错误的上传接口

在这里我将为Vue和Window监控分别写一个接口(因为我懒得一个接口做判断区分,如果你觉得两个接口太麻烦,那你也可以自己优化成一个接口)

编写Vue错误上报接口

server.js 中继续添加

const SourceMap = require('source-map');

app.post('/reportVueError',async (req, res) => {
  const urlParams = req.body;
  console.log(`收到Vue错误报告`);
  console.log('urlParams', urlParams);

  const stack = urlParams.error.stack;
  // 获取文件名
  const fileName = path.basename(stack.url);
  // 查找map文件
  const filePath = path.join(__dirname, 'uploads', fileName + '.map');
  const readFile = function (filePath{
    return new Promise((resolve, reject) => {
      fs.readFile(filePath, { encoding'utf-8'}, (err, data) => {
        if (err) {
          console.log('readFileErr', err)
          return reject(err);
        }
        resolve(JSON.parse(data));
      })
    })
  }

  async function searchSource({ filePath, line, column }{
    const rawSourceMap = await readFile(filePath);
    const consumer = await new SourceMap.SourceMapConsumer(rawSourceMap);
    const res = consumer.originalPositionFor({ line, column })

    consumer.destroy();
    return res;
  }

  let sourceMapParseResult = '';
  try {
    // 解析sourceMap结果
    sourceMapParseResult = await searchSource({ filePath, line: stack.line, column: stack.column });
  } catch (err) {
    sourceMapParseResult = err;
  }
  console.log('解析结果', sourceMapParseResult)
  res.send({
    data'错误上报成功',
    status200,
  }).status(200);
})

然后 nodemon 会自动重启服务,如果你不是用 nodemon 启动的,那自己手动重启一下

打包vue之后然后通过端口访问 index.html ,不建议你双击打开,如果你没改过打包相关的东西,双击打开是不行的,可以通过 vs code 装插件 live server ,然后将打包文件夹通过 vs code 打开,通过 live server 运行,此时应该会报跨域问题

设置允许跨域

可以自己手动设置响应头实现跨域,我这里选择使用 cors

bash
npm i cors
const cors = require('cors');
app.use(cors()); // 这条需要放在 const app = express(); 后

此时重新运行后台,再观察

此时发现,解析map文件报错了,那是因为我们还没有上传map文件

server.js 同级目录下创建一个 uploads 文件夹

回到打包vue打包文件目录 dist ,将js文件夹中所有 js.map 结尾的文件剪切到创建的文件夹中,如果你打包文件中没有 js.map ,那是因为你没有打开生成 js.map 的开关,打开 vue.config.js ,在 defineConfig 中设置属性 productionSourceMap true ,然后重新打包就可以了

module.exports = defineConfig({
  productionSourceMaptrue// 设置为true,然后重新打包
  transpileDependenciestrue,
  lintOnSavefalse,
  configureWebpack: {
    devServer: {
      clientfalse
    }
  }
})

为什么是剪切?如果真正的项目上线时,你把 js.map 文件上传了,别人拿到之后是可以知道你的源码的,所以必须剪切,或者复制之后回到 dist 目录删掉所有 js.map


这时候我们再刷新网页,然后看后台的输出,显示 src/App.vue 的第10行有错



编写window错误上传接口

// 处理Window报错
app.post('/reportWindowError',async (req, res) => {
  const urlParams = req.body;
  console.log(`收到Window错误报告`);
  console.log('urlParams', urlParams);

  res.send({
    data'错误上报成功',
    status200,
  }).status(200);
})

此时我们去vue项目中添加一个img标签,获取一张不存在的图片即可出发错误,由于不用解析,所以这里就不再上传 js.map


写入日志

错误上报之后我们需要记录下来,接下来我们改造一下接口,收到报错之后写一下日志

我需要知道哪一天的日志报错了,所有我在node项目中也下载 dayjs 用来格式化时间

npm i dayjs

此处的日志记录内容只是我自己需要的格式,如果你需要其他格式请自己另外添加

vue错误写入日志

// let sourceMapParseResult = '';
// try {
//  // 解析sourceMap结果
//  sourceMapParseResult = await searchSource({ filePath, line: stack.line, column: //stack.column });
//} catch (err) {






请到「今天看啥」查看全文