我们的需求点?
- 有些灵异问题只存在于线上特定的环境
- 前端没有一个紧急问题报警的系统
异常分类
- javascript异常
- 语法错误
- 运行时错误
- script文件内错误(跨域和未跨域)
- JS文件、CSS文件、img图片等(资源)的404错误(其实是有onerror事件的dom)
- promise的异常捕获
- ajax请求错误
异常级别
一般而言,我们会将收集信息的级别分为info,warn,error等,并在此基础上进行扩展。
当我们监控到异常发生时,可以将该异常划分到“重要——紧急”模型中分为A、B、C、D四个等级。有些异常,虽然发生了,但是并不影响用户的正常使用,用户其实并没有感知到,虽然理论上应该修复,但是实际上相对于其他异常而言,可以放在后面进行处理。
一般而言,越靠近右上角的异常会越快通知,保证相关人员能最快接收到信息,并进行处理。A级异常需要快速响应,甚至需要相关负责人知悉。
错误捕获
- 主动捕获(try catch / promise catch)
- 全局捕获(onerror / addEventListener)
try catch
通常,为了判断一段代码中是否存在异常,我们会这一写:
try {
var a = 1;
var b = a + c;
} catch (e) {
// 捕获处理
console.log(e); // ReferenceError: c is not defined
}
使用try catch能够很好的捕获异常并对应进行相应处理,不至于让页面挂掉,但是其存在一些弊端,比如需要在捕获异常的代码上进行包裹,会导致页面臃肿不堪,不适用于整个项目的异常捕获。
onerror事件
/** *
@param {String} msg 错误信息 *
@param {String} url 出错文件 *
@param {Number} row 行号 *
@param {Number} col 列号 *
@param {Object} error 错误详细信息 */
window.onerror = function (msg, url, row, col, error) {
console.log({ msg, url, row, col, error })
return true; // 注意,在返回 true 的时候,异常才不会继续向上抛出error;
};
打印如下: {
msg: “Uncaught ReferenceError: error is not defined”,
url: “file:///Users/Desktop/test.html”,
row: 25,
col: 5,
error: ReferenceError: error is not defined at
}
onerror事件
是无法捕获到网络异常的错误(资源加载失败,裸奔,图片显示异常等)。当我们遇到 <img src="./404.png">
报 404 网络请求异常的时候,onerror 是无法帮助我们捕获到异常的。
window.addEventListener监听error事件
由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP 的状态是 404 还是其他比如 500 等等,所以还需要配合服务端日志才进行排查分析才可以。
<script>
/** * @param {String} event 监听事件 *
@param {function} function 出错文件 *
@param {Boolean}
useCapture 指定事件是否在捕获或冒泡阶段执行。 * true – 事件句柄在捕获阶段执行 * false- false- 默认。
事件句柄在冒泡阶段执行 */
window.addEventListener(‘error’, (error) => {
console.log(‘我知道 404 错误了’);
console.log( error ); return true; }, true);
</script>
<img src=”./404.png” alt=””>
promise异常捕获
现代的浏览器其实已经能够支持promise语法了,所以在promise异常捕获这一块我们也还是要注意一下.
- 人工手动catch捕获(这个是基本的,和try…catch…是一样的).
- 通过浏览器自带的unhandledrejection事件来监听全局没有catch的promise执行.但是这个的兼容性不是很好,具体可以看下unhandledrejection
<script>
window.addEventListener('unhandledrejection', function(err) {
console.log(err);
});
</script>
new Promise(function(resolve, reject) {
reject(new Error('haha'))
})
打印如下:
bubbles:false
cancelBubble:false
cancelable:true
composed:false
currentTarget:Window
defaultPrevented:false
eventPhase:0
isTrusted:true
path:Array(1)
promise:Promise // 捕获到的错误promise
reason:Error: haha at http://localhost:3000/promise_error:21:12 at Promise (<anonymous>) at http://localhost:3000/promise_error:20:3 // 其实就是错误栈
message: "haha"
stack: "Error: haha↵ at http://localhost:3000/promise_error:21:12↵ at Promise (<anonymous>)↵ at http://localhost:3000/promise_error:20:3"
returnValue:true
srcElement:Window
target:Window
timeStamp:55.190000000000005
type:"unhandledrejection"
异常如何上报
监控拿到报错信息之后,接下来就需要将捕捉到的错误信息发送到信息收集平台上,常用的发送形式主要有两种:
- 通过 Ajax 发送数据(xhr和jquery)
- 动态创建 img 标签的形式
function report(error) { var reportUrl = 'http://xxxx/report'; new Image().src = reportUrl + 'error=' + error; }
sourceMap
例如:webpack中开启source-map功能:
module.exports = {
…
devtool: ‘#source-map’,
…
}
打包压缩的文件末尾会带上这样的注释:
!function(e){var o={};function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}}…;
//# sourceMappingURL=bundle.js.map
意思是该文件对应的map文件为bundle.js.map。下面便是一个source-map文件的内容,是一个JSON对象:
version: 3, // Source map的版本
sources: [“webpack:///webpack/bootstrap”, …], // 转换前的文件
names: [“installedModules”, “__webpack_require__”, …], // 转换前的所有变量名和属性名
mappings: “aACA,IAAAA,KAGA,SAAAC…”, // 记录位置信息的字符串
file: “bundle.js”, // 转换后的文件名
sourcesContent: [“// The module cache var installedModules = {};…”], // 源代码
sourceRoot: “” // 转换前的文件所在的目录
因为前端解析速度较慢,所以这里不做推荐,我们还是使用服务器解析。如果你的应用有node中间层,那么你完全可以将异常信息提交到中间层,然后解析map文件后将数据传递给后台服务器;
通过mozilla/source-map的SourceMapConsumer接口,可以通过将转换后的行号列号传入Consumer得到原始错误位置信息。相应的node代码如下
var fs = require(‘fs’)
var sourceMap = require(‘source-map’) // map文件
var rawSourceMapJsonData = fs.readFileSync(‘./dist/index.min.js.map’, ‘utf-8’)
rawSourceMapJsonData = JSON.parse(rawSourceMapJsonData)
var consumer = new sourceMap.SourceMapConsumer(rawSourceMapJsonData); // 打印出真实错误位置
console.log(consumer.originalPositionFor({line: 1, column: 220}))
通过上面方式得到错误行号。
MVVM框架
在MVVM框架中如果无法使用window.onerror来捕获异常,因为你的异常信息被框架自身的异常机制捕获了。比如Vue 2.x中我们应该这样捕获全局异常:
Vue.config.errorHandler = function (err, vm, info) {
let {
message, // 异常信息
name, // 异常名称
script, // 异常脚本url
line, // 异常行号
column, // 异常列号
stack // 异常堆栈信息
} = err;
// vm为抛出异常的 Vue 实例
// info为 Vue 特定的错误信息,比如错误所在的生命周期钩子
}
目前script、line、column这3个信息打印出来是undefined,不过这些信息在stack中都可以找到,可以通过正则匹配去进行获取,然后进行上报。
注意点
以上是异常捕获和上报的主要知识点和流程,还有一些需要注意的地方,比如你的应用访问量很大,那么一个小异常都可能会把你的服务器搞挂,所以上报的时候可以进行信息过滤和采样等,设置一个调控开关,服务器也可以对相似的异常进行过滤,在一个时间段内不进行多次存储。
后话
目前也有一些非常完善的前端监控系统存在,如sentry、bugsnag等,针对目前我们的情况,拿来开源的第三方应该更方便一下,但是了解原理,摸清逻辑还是很有必要滴~~~~