前端错误监控在前端领域越来越重要,很多常见的 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 引用的模块)*