前言
做好错误监控,将用户使用时的错误日志上报,可以帮助我们更快的解决一些问题。目前开源的比较好的前端监控有
那前端监控是怎么实现的呢?要想了解这个,需要知道前端错误大概分为哪些以及如何捕获处理。
前端错误分为JS运行时错误、资源加载错误和接口错误三种。
一、JS运行时错误
JS运行时错误一般使用window.onerror捕获,但是有一种特殊情况就是promise被reject并且错误信息没有被处理的时候抛出的错误
1.1 一般情况的JS运行时错误
使用window.onerror和window.addEventListener('error')捕获。
window.onerror = function (msg, url, lineNo, columnNo, error)
{
// 处理error信息
}
window.addEventListener('error', event =>
{
console.log('addEventListener error:' + event.target);
}, true);
// true代表在捕获阶段调用,false代表在冒泡阶段捕获。使用true或false都可以
例子:https://jsbin.com/lujahin/edit?html,console,output 点击button抛出错误,分别被window.onerror和window.addEventListener('error')捕获
1.2 Uncaught (in promise)
当promise被reject并且错误信息没有被处理的时候,会抛出一个unhandledrejection,并且这个错误不会被window.onerror以及window.addEventListener('error')捕获,需要用专门的window.addEventListener('unhandledrejection')捕获处理
window.addEventListener('unhandledrejection', event =>
{
console.log('unhandledrejection:' + event.reason); // 捕获后自定义处理
});
https://developer.mozilla.org...
例子:https://jsbin.com/jofomob/edit?html,console,output 点击button抛出unhandledrejection错误,并且该错误仅能被window.addEventListener('unhandledrejection')捕获
1.3 console.error
一些特殊情况下,还需要捕获处理console.error,捕获方式就是重写window.console.error
var consoleError = window.console.error;
window.console.error = function () {
alert(JSON.stringify(arguments)); // 自定义处理
consoleError && consoleError.apply(window, arguments);
};
1.4 特别说明跨域日志
什么是跨域脚本error?
https://developer.mozilla.org...
当加载自不同域的脚本中发生语法错误时,为避免信息泄露(参见bug 363897),语法错误的细节将不会报告,而代之简单的"Script error."。在某些浏览器中,通过在<script>使用crossorigin属性并要求服务器发送适当的 CORS HTTP 响应头,该行为可被覆盖。一个变通方案是单独处理"Script error.",告知错误详情仅能通过浏览器控制台查看,无法通过JavaScript访问。
window.onerror = function (msg, url, lineNo, columnNo, error) {
var string = msg.toLowerCase();
var substring = "script error";
if (string.indexOf(substring) > -1){
alert('Script Error: See Browser Console for Detail');
} else {
var message = [
'Message: ' + msg,
'URL: ' + url,
'Line: ' + lineNo,
'Column: ' + columnNo,
'Error object: ' + JSON.stringify(error)
].join(' - ');
alert(message);
}
return false;
};
例子: http://sandbox.runjs.cn/show/... 请打开页面打开控制台。该页面分别加载了两个不同域的js脚本,配置了crossorigin的window.onerror可以报出详细的错误,没有配置crossorigin只能报出'script error',并且没有错误信息
为了跨域捕获JavaScript异常,可执行以下两个解法:
解法一、1.添加crossorigin="anonymous"属性。
<script src="http://another-domain.com/app.js" crossorigin="anonymous"></script>
此步骤的作用是告知浏览器以匿名方式获取目标脚本。这意味着请求脚本时不会向服务端发送潜在的用户身份信息(例如Cookies、HTTP证书等)。
2.添加跨域HTTP响应头。
Access-Control-Allow-Origin: *
或者 Access-Control-Allow-Origin: http://test.com
解法二 、try catch
<!doctype html>
<html>
<head>
<title>Test page in http://test.com</title>
</head>
<body>
<script src="http://another-domain.com/app.js"></script>
<script>
window.onerror = function (message, url, line, column, error) {
console.log(message, url, line, column, error);
}
try {
foo(); // 调用app.js中定义的foo方法
} catch (e) {
console.log(e);
throw e;
}
</script>
</body>
</html>
再次运行,输出结果如下:
=> ReferenceError: bar is not defined
at foo (http://another-domain.com/app.js:2:3)
at http://test.com/:15:3
=> "Script error.", "", 0, 0, undefined
可见try catch
中的Console语句输出了完整的信息,但window.onerror
中只能捕获“Script error”。根据这个特点,可以在catch语句中手动上报捕获的异常,详情请参见API 使用指南。
1.5 特别说明sourceMap
在线上由于JS一般都是被压缩或者打包(webpack)过,打包后的文件只有一行,因此报错会出现第一行第5000列出现JS错误,给排查带来困难。sourceMap存储打包前的JS文件和打包后的JS文件之间一个映射关系,可以根据打包后的位置快速解析出对应源文件的位置。
但是出于安全性考虑,线上设置sourceMap会存在不安全的问题,因为网站使用者可以轻易的看到网站源码,此时可以设置.map文件只能通过公司内网访问降低隐患
sourceMap配置devtool: 'inline-source-map'
如果使用了uglifyjs-webpack-plugin 必须把 sourceMap设置为true
https://doc.webpack-china.org...
1.6 其它
1.6.1 sentry把所有的回调函数使用try catch封装一层
https://github.com/getsentry/raven-js/blob/master/src/raven.js
1.6.2 vue errorHandler
https://vuejs.org/v2/api/#errorHandler
其原理也是使用try catch封装了nextTick,$emit, watch,data等
https://github.com/vuejs/vue/blob/dev/dist/vue.runtime.js
二、资源加载错误
使用window.addEventListener('error')捕获,window.onerror捕获不到资源加载错误
https://jsbin.com/rigasek/edit?html,console 图片资源加载错误。此时只有window.addEventListener('error')可以捕获到
window.onerror和window.addEventListener('error')的异同:相同点是都可以捕获到window上的js运行时错误。区别是1.捕获到的错误参数不同 2.window.addEventListener('error')可以捕获资源加载错误,但是window.onerror不能捕获到资源加载错误
window.addEventListener('error')捕获到的错误,可以通过target?.src || target?.href区分是资源加载错误还是js运行时错误
三、接口错误
所有http请求都是基于xmlHttpRequest或者fetch封装的。所以要捕获全局的接口错误,方法就是封装xmlHttpRequest或者fetch
3.1 封装xmlHttpRequest
if(!window.XMLHttpRequest) return;
var xmlhttp = window.XMLHttpRequest;
var _oldSend = xmlhttp.prototype.send;
var _handleEvent = function (event) {
if (event && event.currentTarget && event.currentTarget.status !== 200) {
// 自定义错误上报 }
}
xmlhttp.prototype.send = function () {
if (this['addEventListener']) {
this['addEventListener']('error', _handleEvent);
this['addEventListener']('load', _handleEvent);
this['addEventListener']('abort', _handleEvent);
} else {
var _oldStateChange = this['onreadystatechange'];
this['onreadystatechange'] = function (event) {
if (this.readyState === 4) {
_handleEvent(event);
}
_oldStateChange && _oldStateChange.apply(this, arguments);
};
}
return _oldSend.apply(this, arguments);
}
3.2 封装fetch
if(!window.fetch) return;
let _oldFetch = window.fetch;
window.fetch = function () {
return _oldFetch.apply(this, arguments)
.then(res => {
if (!res.ok) { // True if status is HTTP 2xx
// 上报错误
}
return res;
})
.catch(error => {
// 上报错误
throw error;
})
}
结论
- 使用window.onerror捕获JS运行时错误
- 使用window.addEventListener('unhandledrejection')捕获未处理的promise reject错误
- 重写console.error捕获console.error错误
- 在跨域脚本上配置
crossorigin="anonymous"
捕获跨域脚本错误 - window.addEventListener('error')捕获资源加载错误。因为它也能捕获js运行时错误,为避免重复上报js运行时错误,此时只有event.srcElement inatanceof HTMLScriptElement或HTMLLinkElement或HTMLImageElement时才上报
- 重写window.XMLHttpRequest和window.fetch捕获请求错误
利用以上原理,简单写了一个JS监控,只处理了一些JS错误,暂时没有做和性能相关的监控
https://github.com/Lie8466/better-js
如果发现文章有错误,欢迎指正。