最近在写错误上报,记录一下,若是对你有所帮助,荣幸之至;第一次写,有点啰嗦,见谅!
大概分为三个部分:javascript
1、错误收集
js的错误通常分为:运行时错误、资源加载错误、网络请求错误;
对于语法错误、资源加载错误,供咱们选择的错误收集方式通常是:css
window.addEventListener('error', e => {}, true); window.onerror = function (msg, url, line, col, error) {}
**划重点:**
html
所以咱们能够这样收集错误:java
window.addEventListener("error", e => { if (!e) { return; } console.log(e.message);//错误信息 conosle.log(e.filename);//发生错误的文件名 console.log(e.lineno);//发生错误的行号(代码压缩合并对值有影响) console.log(e.colno);//发生错误的列号(代码压缩合并对值有影响) const _target = e.target || e.srcElement; if (!_target) { return; } if (_target === window) { //语法错误 let _error = e.error; if (_error) { console.log(_error.stack);//错误的堆栈信息 } } else { // 元素错误,好比引用资源报错 let _src = _target.src; console.log(_src);//_src: 错误的资源路径 } }, true);
当是运行时的语法错误时,咱们能够拿到报错的行号,列号,错误信息,错误堆栈,以及发生错误的脚本的路径及名字。
当是资源路径错误时,咱们能够拿到错误资源的路径及名字。jquery
至此,咱们就拿到了想要的资源加载错误、运行时语法错误的信息,那ajax网络请求错误怎么办呢?ajax
此时:有两个方式能够选择后端
throw new Error('抛出的一个错误'); console.error('打印一个错误');//下面会讲
咱们前面定义的方法能够收集到throw new Error
抛出的错误,可是要注意,抛出错误一样也会阻断后续的程序,使用的时候要当心;若是你的项目中也封装了http请求的话,可参照下面代码:跨域
//基于jquery function ajaxFun (params) { var _d = { type: params.type || 'POST', url: params.url || '', data: params.data || null, dataType: params.dataType || 'JSON', contentType: params.contentType || 'application/x-www-form-urlencoded', beforeSend: function (request) { }, success: function (data, status, xhr) { }, error: function (xhr, type, error) { throw new Error(params.url + '请求失败'); } } $.ajax(_d); }
上面的代码是用jquery
封装的请求,我在error
方法里面抛出了这个ajax请求的错误,由于抛出错误后面没有其余业务逻辑,不会有什么问题,这里我只要求收集ajax的error
方法错误,若是你的项目要求处理全部异常错误,好比token失效致使的登录失败,就须要在success函数里面也作处理了。可是,要注意throw new Error('抛出的一个错误')
与console.error('打印一个错误')
的区别。数组
当使用console.error打印错误时,前面的window.addEventListener
方式无法收集到,可是咱们能够经过其余方式收集到错误,下面是一个更特殊的例子;promise
**特例:**
js运用范围很广,有些状况,这样是不可以收集到咱们想要的错误的;
打个比方,咱们用 cocos creator
引擎写游戏时,加载资源是使用引擎的方法,当发生资源不存在的错误时,咱们是不知道的,可是,咱们发现 cocos creator
引擎会将错误打印到控制台,那也是引擎作的操做,咱们一番顺藤摸瓜,会发现,cocos creator
引擎在底层报错都是用cc.error
,翻看cc.error
的源码,咱们就看见了咱们想看见的东西了console.error()
,这样一来,知道错误是怎么来的,就好办了。(具体状况,具体对待,这里只是恰巧cocos是这么处理的,其余引擎可能不太同样)
let _windowError = window.console.error; window.console.error = function () { let _str = JSON.stringify(arguments); console.log(_str); _windowError && _windowError.apply(window, arguments); }
复写console.error
后,不管和人在何处使用这个函数,咱们均可以保证这个打印被咱们处理过,
记住,必定要先将原来的console.error
接收一下,而且在实现咱们须要的业务后,执行原来console.error
,
保证不会影响到其余的逻辑。
2、错误筛选
也许你会疑惑?不是全部的错误都上报么,为何要筛选呢?
大多数状况,咱们收集到错误,而后上报便可,
可是,有时候,会有循环报错
、资源加载失败一直重试,一直失败
等种种特殊状况,若是按照正常的上报流程,那么可能会发生在短短几秒的时间内,收集到了上千、上万条数据,致使程序卡顿,甚至是崩溃。
所以,咱们须要对错误进行筛选。
let _errorMap = {};//用于错误筛选的对象; let _errorArg = [];//存放错误信息的数组;
全局维护一个_errorMap,用于错误筛选的对象,每当有错误时,咱们按照约定好的规则,组成一个key,和_errorMap已经存在的key进行比对,若是不存在,证实是新的错误,须要上报,若是是已经上报的错误,就再也不处理。
固然,为了防止_errorMap无限大、以及错误漏报,当_errorMap的key的数量大于必定数量时,咱们须要将_errorMap的key清空,这时候可能出现前面已经上报的错误再次上报,可是没关系,这个重复能够接受。
这个临界值能够根据实际状况定,我项目中最大值为100。
对于上面这个约定好的规则,其实就是根据咱们上面收集到的有关错误的信息,组成的一个惟一key值,能实现惟一性且越短越好便可
//上面的代码,复制下来,方便看 window.addEventListener("error", e => { if (!e) { return; } console.log(e.message);//错误信息 conosle.log(e.filename);//发生错误的文件名 console.log(e.lineno);//发生错误的行号(代码压缩合并对值有影响) console.log(e.colno);//发生错误的列号(代码压缩合并对值有影响) const _target = e.target || e.srcElement; if (!_target) { return; } if (_target === window) { //语法错误 let _error = e.error; if (_error) { console.log(_error.stack);//错误的堆栈信息 } } else { // 元素错误,好比引用资源报错 let _src = _target.src; console.log(_src);//_src: 错误的资源路径 } }, true);
对于语法错误,能够根据报错的文件名,行号,列号,组成key let _key = `${e.filename}_${e.lineno}_${e.colno}`; 对于资源加载错误,能够根据错误资源的路径做为key: let _key = e.src;
拿到key以后,咱们就能够存贮错误了,
下面是存储的完整代码:
function _sendErr(key, errType, errMsg) { //筛选 if (_ErrorMap.hasOwnProperty(key)) { //筛选到相同的错误,可将值加一,能够判断错误出现的次数 _ErrorMap[key] += 1; return; } //阈值 if (_ErrorArg.length >= 100) { return; } //存储错误 //对于要发给后端的数据,可根据需求组织,数据结构 _ErrorArg.push({ errType: errType,//错误类型 errMsg: errMsg || '',//错误信息 ver: _ver || '',//版本号 timestamp: new Date().getTime(),//时间戳 }); //存放错误信息的数组的阈值 if (Object.keys(_ErrorMap).length >= 100) { //达到阈值以后,清空去重对象 _ErrorMap = {}; } _ErrorMap[key] = 1; }
存储错误的数组也须要阈值,实际运用中,咱们能够控制每次上报的错误条数,可是,必定得记得已经上报的错误必定要从数组中移出。此外,上报的数据结构根据需求能够调整,通常包含错误信息、堆栈信息、加载失败资源的路径。
3、错误上报
难道不是一收集到错误就上报?
同时出现一个两个错误,固然能够当即上报,
可是若是千百个错误在短短的几秒钟出现,就会出现网络拥堵,甚至是程序崩溃。
所以,通常都会全局维护一个计时器,延迟上报;
let _ErrorTimer = null; timerError(); function timerError() { clearTimeout(_ErrorTimer); let _ErrorArg = g.IndexGlobal.ErrorArg;//前面提到的全局错误存贮数组 let _ErrorArgLength = _ErrorArg.length; if (_ErrorArgLength > 0) { let _data = [];//要发送的错误信息,由于是一次性发5条,放零时数组中。 //组织要发送的错误信息 for (let i = 0; i < _ErrorArgLength; i++) { if (_data.length >= 5) { break; } _data.push(_ErrorArg.shift()); } if (_data.length) { //发送错误信息 //jq ajax g.IndexGlobal.errorSend(_data, function (p) { //失败 //若是发送失败,将未发送的数据,从新放入存储错误信息的数组中 if (p && p.data && p.data.data) { if (_ErrorArg.length >= 100) { return; } let _ag = p.data.data; try { g.IndexGlobal.ErrorArg.push(...JSON.parse(_ag)); } catch (error) { } } }); } } //计时器间隔,当数组长度大于20时,一秒执行一次,默认2秒一次 let _ti = _ErrorArgLength >= 20 ? 1000 : 2000; _ErrorTimer = setTimeout(timerError, _ti); }
咱们能够根据错误的数量,调整错误上报的频率。可是这个间隔通常不要过小,否则容易出问题。
4、注意事项
1.不管是window.addEventLister
仍是console.error
,在咱们定义这些方法以前报的全部错误,咱们是收集不到的,
怎么处理呢,很简单,js顺序执行,咱们能够将相关代码放在最前头,
<!DOCTYPE html> <html> <script> //处理错误的代码 window.addEventLister; console.error = function(){} </script> <head> <meta charset="utf-8"> <link rel="stylesheet" href=""> <link rel="stylesheet" href=""> </head> <body> </body> <script src="js/zepto.min.js"></script> <script src="js/a.js"></script> <script> //开始错误上报计数器 </script> </html>
可是,要注意,放在最前面的是处理错误的逻辑,上报的计时器不能当即开启,由于,此时jquery 还没加载,
计时器开启放在至少jquery加载完成以后。
2.必定要作好处理错误部分代码的容错处理,否则业务逻辑代码还没报错,处理错误的部分反而报错就很差了。
3.当你直接双击html,在浏览器打开时,错误收集机制可能不会正确工做,例如没有行号,列号,文件名,错误信息仅仅是Script Error
,这是由于onerror MDN
当加载自 不一样域的脚本中发生语法错误时,为避免信息泄露(参见 bug 363897),语法错误的细节将不会报告,而代之简单的**"Script error."**
。在某些浏览器中,经过在<script>
使用`[crossorigin](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script#attr-crossorigin)`
属性并要求服务器发送适当的 CORS HTTP 响应头,该行为可被覆盖。一个变通方案是单独处理"Script error.",告知错误详情仅能经过浏览器控制台查看,没法经过JavaScript访问。
处理方式为:服务端添加Access-Control-Allow-Origin
,页面在script
标签中配置 crossorigin="anonymous"
。这样,便解决了由于跨域而带来的问题。
5、完整代码
<!DOCTYPE html> <html> <script> //处理错误的命名空间 window['errorSpace'] = { ErrorTimer: null, //全局错误上报计时器 ErrorArg: [], //全局错误存储数组 ErrorMap: {}, //用于错误筛选的对象 //存储错误信息 PushError: function (key, errMsg) { let _ErrorMap = window.errorSpace.ErrorMap; let _ErrorArg = window.errorSpace.ErrorArg; //筛选 if (_ErrorMap.hasOwnProperty(key)) { //筛选到相同的错误,可将值加一,能够判断错误出现的次数 _ErrorMap[key] += 1; return; } //阈值 if (_ErrorArg.length >= 100) { return; } //存储错误 //对于要发给后端的数据,可根据需求组织,数据结构 _ErrorArg.push({ errMsg: errMsg || '', //错误信息 ver: '', //版本号 timestamp: new Date().getTime(), //时间戳 }); //存放错误信息的数组的阈值 if (Object.keys(_ErrorMap).length >= 100) { //达到阈值以后,清空去重对象 _ErrorMap = {}; } _ErrorMap[key] = 1; }, //错误上报函数 ErrorSend: function () { clearTimeout(window.errorSpace.ErrorTimer); let _ErrorArg = window.errorSpace.ErrorArg; //前面提到的全局错误存贮数组 let _ErrorArgLength = _ErrorArg.length; if (_ErrorArgLength > 0) { let _data = []; //要发送的错误信息,由于是一次性发5条,放零时数组中。 //组织要发送的错误信息 for (let i = 0; i < _ErrorArgLength; i++) { if (_data.length >= 5) { break; } _data.push(_ErrorArg.shift()); } if (_data.length) { //发送错误信息 //jq ajax var _d = { type: 'POST', url: '', data: _data || null, dataType: 'JSON', contentType: 'application/x-www-form-urlencoded', success: function (data, status, xhr) { //上报失败,将错误从新存储 //这是假设服务端返回的数据结构是{status: 200} if (data.status !== 200) { //失败 try { //直接存入 //此处没有对_ErrorArg的长度进行判断,因此会溢出一次,使得错误错误尽量的保留,问题不大,也能够不让溢出 _ErrorArg.push(..._data); } catch (error) { console.log(error); } } }, error: function (xhr, type, error) { //上报失败,将错误从新存储 try { //直接存入 //此处没有对_ErrorArg的长度进行判断,因此会溢出一次,使得错误错误尽量的保留,问题不大,也能够不让溢出 _ErrorArg.push(..._data); } catch (error) { console.log(error); } } } $.ajax(_d); } } //计时器间隔,当数组长度大于20时,一秒执行一次,默认2秒一次 let _ti = _ErrorArgLength >= 20 ? 1000 : 2000; window.errorSpace.ErrorTimer = setTimeout(window.errorSpace.ErrorSend, _ti); }, }; //错误收集 window.addEventListener("error", e => { if (!e) { return; } let _err_msg = ''; //要上报的错误信息 let _r = 0; //发生错误的行号 let _l = 0; //发生错误的列号 let _fileName = ''; //发生错误的文件名 const srcElement = e.target || e.srcElement; if (!srcElement) { return; } if (srcElement === window) { //语法错误 let _error = e.error; if (_error) { _err_msg = _error.message + _error.stack; _r = e.lineno || 0; _l = e.colno || 0; _fileName = e.filename || ''; } } else { // 元素错误,好比引用资源报错 if (srcElement.src) { _err_msg = srcElement.src; _fileName = srcElement.src; } } let _key = `${_fileName}_${_r}_${_l}`; window.errorSpace.PushError(_key, _err_msg); }, true); //处理console.error; let _windowError = window.console.error; window.console.error = function () { let _str = JSON.stringify(arguments); window.errorSpace.PushError(_str, _str); _windowError && _windowError.apply(window, arguments); } </script> <head> <meta charset="utf-8"> <link rel="stylesheet" href=""> <link rel="stylesheet" href=""> </head> <body> </body> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script> //开始错误上报计数器 window.errorSpace && window.errorSpace.ErrorSend && window.errorSpace.ErrorSend(); </script> </html>