前言
项目上线之后,用户如果出现错误(代码报错、资源加载失败以及其他情况),基本上没有办法复现,如果用户出了问题但是不反馈或直接不用了,对开发者或公司来说都是损失。
由于我这个项目比较小,只是一个迷你商城,所以不需要收集很复杂的数据,只需要知道有没有资源加载失败、哪行代码报错就可以了,市面上有很多现成的监控平台比如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({ extended: true }));
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: '错误上报成功',
status: 200,
}).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({
productionSourceMap: true, // 设置为true,然后重新打包
transpileDependencies: true,
lintOnSave: false,
configureWebpack: {
devServer: {
client: false
}
}
})
为什么是剪切?如果真正的项目上线时,你把
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: '错误上报成功',
status: 200,
}).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) {