前端错误监控在前端领域越来越重要,很多常见的bug,jquery is not define,Script error,为了给用户更好的体验,需要把前端可能出错的概率事件给消除掉,提高系统的稳定性
内置错误对象
在代码运行出错的时候浏览器会抛出异常,比如使用未定义的变量抛出ReferenceError,在对象里使用未定义的函数抛出TypeError。如果没有做错误处理,通常会导致脚本终止执行。javascript定义了7中内置的错误对象,有Error,RangeError,ReferenceError,SyntaxError,TypeError,URIError,EvalError
Error
通用的异常对象。我们通常使用Error来自定义异常,Error对象有name和message属性,可以通过message来得到具体的错误信息,比如
let error = new Error('接口报错');
let name = error.name; // 'Error'
let msg = error.message; // '接口报错'
RangeError
超出指定范围错误,比如声明一个负数的数组,使用toFixec超过了规定小数的位数(0-20)
new Array(-1)
(1.2).toFixed(21)
ReferenceError
访问未定义的变量,比如
function foo() {
bar++; // bar未定义
}
TypeError
类型错误,比如一个变量不是函数,却把它当做函数来调用
let a = 1;
a(); // 类型错误
SyntaxError
语法错误,一般出代码语句不完整,比如
let a = 1 > 0 ? // 正则不完整
if (a) { // 少了一个分号
URIError
在使用全局的URI函数,参数错误的时候抛出,比如
encodeURI('\uD800')
encodeURIComponent('\uD800')
decodeURI('%')
decodeURIComponent('%')
EvalError
调用eval()函数抛出错误,例如
eval('3a')
错误处理
我们可以通过try/catch语法来捕捉错误。最常用的是在函数里面捕捉错误,有错误就在catch处理
function foo () {
try {
bar();
} catch (e) {
// 错误处理,错误上报等等
console.log(e);
console.log(e.message)
}
}
try/catch
只能捕捉同步代码抛出的错误,不能捕捉异步代码抛出的错误
// 下面在定时器外和async函数是捕捉不到异步代码块抛出的错误
try {
setTimeout(() => {
throw new Error('async error')
}, 0)
} catch (e) {
console.log(e.message)
}
// async/await
async function foo () {
let a = 1;
let b = await a + 2;
console.log(b);
throw new Error('async error')
}
try {
foo();
} catch (e) {
console.log(e.message);
}
// 在异步代码块里面的同步代码就可以捕捉到
setTimeout(() => {
try {
throw new Error('async error')
} catch (e) {
console.log(e.message);
}
}, 0)
async function foo () {
try {
let a = 1;
let b = await a + 2;
console.log(b);
throw new Error('async error')
} catch (e) {
console.log(e.message);
}
}
foo();
这种错误处理有一个弊端就是对每一个函数都需要进行try/catch
捕捉再进行处理,需要写很多重复的代码,其实可以使用一个全局的error事件来捕获所有的error
window.onerror = function(message, source, lineno, colno, error) {
// 错误信息,源文件,行号
console.log(message + '\n' + source + '\n' + lineno);
// 禁止浏览器打印标准的错误信息
return true;
}
window.onerror可以捕捉上面的运行时错误和自定义抛出的错误和异步抛出的错误,但是不能捕捉Script error和网络异常,还有promise错误
网络异常捕捉
网络异常可以在事件捕获的阶段捕捉到,通过window.addEventListener来实现,代码必须放在文档载入之前
// ie11和主流浏览器
window.addEventListener('error', function(e) {
e.stopImmediatePropagation();
const srcElement = e.srcElement;
if (srcElement === window) {
// 全局错误
console.log(e.message)
} else {
// 元素错误,比如引用资源报错
console.log(srcElement.tagName)
console.log(srcElement.src);
}
}, true)
Promise错误捕捉
promise的异常可以通过下面两种捕捉方式
- 通过then函数的第二个参数捕捉
- 通过catch函数捕捉
let pro = new Promise((resolve, reject) => {
console.log(c); // 抛出 c is not defined
reject('some error happen');
})
谁先提前声明错误捕捉回调,谁就先捕捉,但是只要有一个错误捕捉到了,后面的错误捕捉函数就不会调用到
pro.catch(err => {
console.log(`通过catch捕捉错误: ${err}`);
}).then(res => {}, err => {
console.log(`在then第二个参数捕捉错误: ${err}`);
})
//通过catch捕捉错误: ReferenceError: c is not defined
pro.then(res => {}, err => {
console.log(`在then第二个参数捕捉错误: ${err}`);
}).catch(err => {
console.log(`通过catch捕捉错误: ${err}`);
})
// 在then第二个参数捕捉错误: ReferenceError: c is not defined
如果promise实例自身没有做错误捕捉,会抛出一个全局的错误unhandledrejection
window.addEventListener('unhandledrejection', function(e) {
e.preventDefault();
console.log(e.type) // unhandledrejection
})
Async/await错误捕捉
async/await基于Promise实现的,它不能用于普通的回调函数,可以在async通过try/catch
处理同步或者异步的错误
// 获取数据
function getData () {
return new Promise((resolve, reject) => {
throw new Error('error')
})
}
try {
getData();
} catch (err) {
// 这里是无法捕捉到错误的
console.log(err);
}
(async function f() {
try {
await getData();
} catch (err) {
// 这里可以捕捉到错误
console.log(err);
}
})();
Script error
如果引用外链不同源的js文件,外链不同源js文件报错,onerror只会提示Script error,无法精确到指定文件和行数,可以通过script标签的crossorigin="anonymous"
,设置了该属性的话,那么需要在服务器对响应的静态文件设置Access-Control-Allow-Origin:*
响应头
<script type="text/javascript" src="http://localhost:3000/test/script.js" crossorigin="anonymous"></script>
这样就可以捕捉到script.js文件的的错误信息,如下
压缩js的错误定位
通过控制script标签的crossorigin="anonymous"
可以捕捉到不同域的js错误信息,在线上的代码都是经过压缩的,可以捕捉到的错误为压缩后的行数和变量,可以通过node提供的source-map模块来定位上报错误信息对应源文件错误的行号
const path = require('path')
const sourceMap = require('source-map')
const fs = require('fs')
const readFile = function (url) {
return new Promise((resolve, reject) => {
fs.readFile(url, (err, res) => {
if (err) {
reject(err)
} else {
resolve(res);
}
})
})
}
// 客户端传过来生产环境下的js文件
const error = {"message":"Uncaught ReferenceError: a is not defined","source":"http://localhost:3000/dist/index.min.js","line":1,"column":588,"error":"ReferenceError: a is not defined"}
console.log(error);
// 根据source获取source map文件
async function getSourceMap(source) {
let basename = path.basename(source);
let sm = await readFile(path.join(__dirname, './dist/' + basename + '.map'));
let smObj = {};
try {
smObj = JSON.parse(sm);
} catch (err) {
console.log('找不到对应的source map文件')
}
return smObj;
}
async function analyze(errObj) {
let rawSourceMap = await getSourceMap(errObj.source);
try {
await sourceMap.SourceMapConsumer.with(rawSourceMap, null, consumer => {
let sourcePos = consumer.originalPositionFor({
line: errObj.line,
column: errObj.column
});
Object.assign(errObj, sourcePos);
return errObj;
});
} catch (err) {
console.log(err.message);
}
return errObj;
}
analyze(error).then(res => {
// 定位错误后的具体信息
console.log(res);
});
上报的错误信息是index.min.js文件的第1行,第588列
解析后的错误信息定位是在src/foo.js
文件的第2行,第11列(foo.js是index.js引用的模块)