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