最近在写错误上报,记录一下,如果对你有所帮助,荣幸之至;第一次写,有点啰嗦,见谅!
大概分为三个部分:
- 错误收集
- 错误筛选
- 错误上报
- 注意事项
- 完整示例
一、错误收集
js的错误一般分为:运行时错误、资源加载错误、网络请求错误;
对于语法错误、资源加载错误,供我们选择的错误收集方式一般是:
window.addEventListener('error', e => {}, true);
window.onerror = function (msg, url, line, col, error) {}
**划重点:**
- 两者获得的参数不一样;
- window.addEventListener能监测到资源(css,img,script)加载失败;
- window.addEventListener能捕捉到window.onerror能捕捉到的错误;
- 二者都不能捕捉到console.error的错误信息;
- 二者都不能捕捉到:当promise被reject并且错误信息没有被处理时的错误信息;
因此我们可以这样收集错误:
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);
当是运行时的语法错误时,我们可以拿到报错的行号,列号,错误信息,错误堆栈,以及发生错误的脚本的路径及名字。
当是资源路径错误时,我们可以拿到错误资源的路径及名字。
至此,我们就拿到了想要的资源加载错误、运行时语法错误的信息,那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
方式没法收集到,但是我们可以通过其他方式收集到错误,下面是一个更特殊的例子;
**特例:**
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
,
保证不会影响到其他的逻辑。
二、错误筛选
也许你会疑惑?不是所有的错误都上报么,为什么要筛选呢?
大多数情况,我们收集到错误,然后上报即可,
但是,有时候,会有循环报错
、资源加载失败一直重试,一直失败
等种种特殊情况,如果按照正常的上报流程,那么可能会发生在短短几秒的时间内,收集到了上千、上万条数据,导致程序卡顿,甚至是崩溃。
因此,我们需要对错误进行筛选。
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;
}
存储错误的数组也需要阈值,实际运用中,我们可以控制每次上报的错误条数,但是,一定得记得已经上报的错误一定要从数组中移出。此外,上报的数据结构根据需求可以调整,一般包含错误信息、堆栈信息、加载失败资源的路径。
三、错误上报
难道不是一收集到错误就上报?
同时出现一个两个错误,当然可以立即上报,
但是如果千百个错误在短短的几秒钟出现,就会出现网络拥堵,甚至是程序崩溃。
因此,一般都会全局维护一个计时器,延迟上报;
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);
}
我们可以根据错误的数量,调整错误上报的频率。但是这个间隔一般不要太小,不然容易出问题。
四、注意事项
1.无论是window.addEventLister
还是console.error
,在我们定义这些方法之前报的所有错误,我们是收集不到的,
怎么处理呢,很简单,js顺序执行,我们可以将相关代码放在最前头,
但是,要注意,放在最前面的是处理错误的逻辑,上报的计时器不能立即开启,因为,此时jquery 还没加载,
计时器开启放在至少jquery加载完成之后。
2.一定要做好处理错误部分代码的容错处理,不然业务逻辑代码还没报错,处理错误的部分反而报错就不好了。
3.当你直接双击html,在浏览器打开时,错误收集机制可能不会正确工作,例如没有行号,列号,文件名,错误信息仅仅是Script Error
,这是因为onerror MDN
当加载自 不同域的脚本中发生语法错误时,为避免信息泄露(参见 bug 363897),语法错误的细节将不会报告,而代之简单的**"Script error."**
。在某些浏览器中,通过在使用
`[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"
。这样,便解决了因为跨域而带来的问题。
五、完整代码