如何处理前端异常监控?
为什么要进行异常处理?
很多异常是不可控的,比如资源加载异常,ajax请求异常等,会影响最终的呈现效果,做好异常处理,有大致以下几点好处:
- 1.增强用户体验;
- 2.快速定位问题原因,及时发现问题。特别是移动端,机型、系统等不一样,有了异常处理并上报,定位快;
- 3.完善前端监控系统方案。
需要处理哪些异常?
- JS语法错误、代码异常
- ajax请求异常
- 静态资源加载异常
- promise异常
- iframe异常
- 跨域 script error
- 崩溃和卡顿
异常处理的方式
try-catch
try-catch只能捕获到同步运行时错误,无法捕获语法错误和异步错误。
示例:运行时错误(能捕获)
try { error; } catch(e) { console.log('捕获到错误了'); console.log(e); }
示例:语法错误(不能捕获)
try { var error = 'err; // 少一个单引号 } catch(e) { console.log('捕获不到错误了'); console.log(e); }
上面红色标记的错误大致意思为:无效或者意外的标记。但是这种语法错误会直接抛出来,使后面的程序代码无法运行,直接崩溃,一般在编码的时候就能发现这类错误。
示例:异步错误(不能捕获)
try { setTimeout(function() { error; // 异步错误,没有定义 }) } catch(e) { console.log('捕获不到错误了'); console.log(e); }
window.onerror
当JS运行发生错误时,window会触发一个errorEvent接口的error事件并执行window.onerror()。
window.onerror比try-catch强一些,在try-catch的基础上,它可以捕获异步错误。
/** * @param {String} message 错误信息 * @param {String} resource 出错文件 * @param {Number} row 行号 * @param {Number} col 列号 * @param {Object} error 错误详细信息error对象 * */ window.onerror = function(message,resource,row,col,error) { console.log('捕获到错误信息'); console.log({message,resource,row,col,error}); return true; }
示例:异步错误(能捕获)
setTimeout(function() { error; // 异步错误,没有定义 })
注意:
1.window.onerror也是不能捕获语法错误的;
2.window.onerror也不能捕获网络请求异常情况,如静态资源异常、接口异常等都是不行的;
3.特别注意的是,window.onerror函数在返回true的时候,异常才不会向上抛出,否则,控制台还是会显示Uncaught Error: xxxxx
如下示例,我们让window.onerror函数没有返回true.
/** * @param {String} message 错误信息 * @param {String} resource 出错文件 * @param {Number} row 行号 * @param {Number} col 列号 * @param {Object} error 错误详细信息error对象 * */ window.onerror = function(message,resource,row,col,error) { console.log('捕获到错误信息'); console.log({message,resource,row,col,error}); } setTimeout(function() { error; // 异步错误,没有定义 })
小结:从上面两种捕获错误的方式来看,window.onerror()函数主要用来捕获意料之外的错误,而try-catch主要是捕获可预见情况下的特定错误。
window.addEventListener
window.onerror函数不能捕获静态资源加载失败的异常情况,当资源(图片或脚本)加载失败,加载资源的元素会触发一个Event接口的error事件,并执行该元素上的onerror()处理函数,这些error事件不会向上冒泡到window上,但是可以被window.addEventListener捕获。
<script type="text/javascript"> window.addEventListener('error',function(error){ console.log('捕获到异常错误了'); console.log(error); },true) </script> <img src="./images/error.jpg"/>
由于网络请求异常不会事件冒泡,因此需要在捕获阶段将其捕捉到才行,这种方式虽然可以捕获到网络请求异常,但是无法判断HTTP的状态码是404还是其他的如500等,所有还需要配合服务端日志进行排查分析才可以。
unhandledrejection监听UnCaught Promise Error
在很多时候我们使用Promise的时候忘记了写catch,那么可以在全局增加一个unhandledrejection的监听,用来全局监听UnCaught Promise Error,使用方式如下:
window.addEventListener("unhandledrejection", function(e){ e.preventDefault(); // 去掉控制台的异常显示 console.log('捕获到异常:', e); return true; }); Promise.reject('promise error');
VUE errorHandler
Vue.config.errorHandler = (err, vm, info) => { console.error('通过vue errorHandler捕获的错误'); console.error(err); console.error(vm); console.error(info); }
如果在组件渲染时出现运行错误,错误将会被传递至全局 Vue.config.errorHandler
配置函数 (如果已设置)。利用这个钩子函数来配合错误跟踪服务是个不错的主意。比如 Sentry,它为 Vue 提供了官方集成。
iframe异常
iframe的异常捕获需要借助window.onerror:
<iframe src="./a.html" frameborder="0"></iframe> <script type="text/javascript"> /** * @param {String} message 错误信息 * @param {String} resource 出错文件 * @param {Number} row 行号 * @param {Number} col 列号 * @param {Object} error 错误详细信息error对象 * */ window.frames[0].onerror = function(message,resource,row,col,error) { console.log('捕获到错误信息'); console.log({message,resource,row,col,error}); return true; } </script>
我在a.html中添加了一句JS代码如下:
<script type="text/javascript"> var a= '1; // 缺少引号 </script>
那么在父窗口捕获到的错误是:
Script error
出现Script error的情况,基本上是跨域问题。例如我们的工程中的静态资源使用CDN,我们引入的CDN方式可能是有不同的域名,如果没有进行额外的配置,就会出现Script error。
解决思路:跨源资源共享机制(CORS),为 script 标签添加 crossOrigin 属性:
<script src="http://localhost:8081/index.js" crossorigin></script>
或者动态去添加 js 脚本:
const script = document.createElement('script'); script.crossOrigin = 'anonymous'; script.src = url; document.body.appendChild(script);
特别注意,服务器端需要设置:Access-Control-Allow-Origin
解决 Script Error 的另类思路:改写 EventTarget 的 addEventListener 方法;对传入的 listener 进行包装,返回包装过的 listener,对其执行进行 try-catch;浏览器不会对 try-catch 起来的异常进行跨域拦截,所以 catch 到的时候,是有堆栈信息的;重新 throw 出来异常的时候,执行的是同域代码,所以 window.onerror 捕获的时候不会丢失堆栈信息;利用包装 addEventListener,我们还可以达到「扩展堆栈」的效果:
(() => { const originAddEventListener = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = function (type, listener, options) { // 捕获添加事件时的堆栈 const addStack = new Error(`Event (${type})`).stack; const wrappedListener = function (...args) { try { return listener.apply(this, args); } catch (err) { // 异常发生时,扩展堆栈 err.stack += '\n' + addStack; throw err; } } return originAddEventListener.call(this, type, wrappedListener, options); } })()
崩溃和卡顿
卡顿也就是网页暂时响应比较慢, JS 可能无法及时执行。但是网页崩溃可以利用 window 对象的 load 和 beforeunload 事件实现了网页崩溃的监控。
window.addEventListener('load', function () { sessionStorage.setItem('good_exit', 'pending'); setInterval(function () { sessionStorage.setItem('time_before_crash', new Date().toString()); }, 1000); }); window.addEventListener('beforeunload', function () { sessionStorage.setItem('good_exit', 'true'); }); if(sessionStorage.getItem('good_exit') && sessionStorage.getItem('good_exit') !== 'true') { /* insert crash logging code here */ alert('Hey, welcome back from your crash, looks like you crashed on: ' + sessionStorage.getItem('time_before_crash')); }
错误上报
通过 Ajax 发送数据 因为 Ajax 请求本身也有可能会发生异常,而且有可能会引发跨域问题,一般情况下更推荐使用动态创建 img 标签的形式进行上报。
动态创建 img 标签的形式:
function report(error) { let reportUrl = 'http://jartto.wang/report'; new Image().src = `${reportUrl}?logs=${error}`; }
收集异常信息量太多,怎么办?实际中,我们不得不考虑这样一种情况:如果你的网站访问量很大,那么一个必然的错误发送的信息就有很多条,这时候,我们需要设置采集率,从而减缓服务器的压力:
Reporter.send = function(data) { // 只采集 30% if(Math.random() < 0.3) { send(data) // 上报错误信息 } }
采集率应该通过实际情况来设定,随机数,或者某些用户特征都是不错的选择。
也可以通过Sentry来实现前端的异常监控及上报。
总结
- 可疑区域增加 Try-Catch
- 全局监控 JS 异常 window.onerror
- 全局监控静态资源异常 window.addEventListener
- 捕获没有 Catch 的 Promise 异常:unhandledrejection
- VUE errorHandler
- 监控网页崩溃:window 对象的 load 和 beforeunload
- 跨域 crossOrigin 解决