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