监控脚本代码异常并上报的方法

    如何完成一个记录网站加载脚本时发生的错误,并把错误上报呢?这是网站上线时最经常的需求。

    一般当我们的脚本发生错误时,浏览器都会在console里体现出错误信息,并且会提示我们出错的文件,行号,堆栈信息,此时js停止往下执行:比如这样:

    错误提示

    到这里先问自己一个问题,前端异常具体是指什么呢?

    第一种情况:JS脚本里边存着语法错误,第二种情况:JS脚本在运行时发生错误

    一般有两种情况可以处理这两种错误:

    一种是try,catch方案,我们针对性的在可能出错的地方使用try,catch块。这个代码块如果出错了我们可以在catch块中收集并处理。

    一种是监听window的onerror事件,比如我们使用window.onerror=function(){}或者是window.addEventListener("error",function(event){}),这个onerror可以收集语法错误和运行时的错误,还可以知道出错的信息,文件,行号,列号等。


    利用try,catch的方法

    先来回忆复习下try,catch的基本用法。下面是我们最初学try,catch时会写的一个小demo。主要是throw的用法。throw 语句允许我们创建自定义错误。正确的技术术语是:创建或抛出异常(exception)。如果把 throwtrycatch 一起使用,就能够更加精准控制程序流,并生成自定义的错误消息。throw exception,这个异常可以是我们创建的字符串,数字,逻辑值,对象等。注意要小写。

    1
    2
    3
    <input type="text" placeholder="请填入一个5到10之间的数字" id="inputValue">
    <button onclick="test()">测试输入值是否合法</button>
    <p id="tip"></p>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function test(){
    try{
    var value = document.getElementById("inputValue").value;
    if(value == ""){
    throw "数字不能为空";
    }
    if(value>10){
    throw "数字不能大于10";
    }
    if(value<5){
    throw "数字不能小于5";
    }
    if(isNaN(value)){
    throw "必须为数字";
    }
    }catch(e){
    var tip = document.getElementById("tip");
    tip.innerHTML = e;
    //console.log(e);
    }
    }

    但是,try,catch有两个很严重的问题,第一个就是如果代码块有语法错误,此时JS解释器不会执行当前这块代码块,也更加不会执行catch里面的东西了。第二个问题就是try,catch可以理解成一个函数块,这有这个里面的运行错误才会被捕捉,也就是没有办法捕捉到全局错误事件

    第一个就是语法错误,不会捕捉,会停止运行,上代码:

    1
    2
    3
    4
    5
    6
    try{
    if =1;
    }catch(e){
    console.log("testerror");
    console.log(e);
    }

    错误提示

    如图所示,并不会打印出错误信息,只是在控制台直接输出了错误信息。如果我们把这个语法错误变为执行时的错误,就是可以捕捉到的。如下图所示:

    1
    2
    3
    4
    5
    6
    try{
    i<l;
    }catch(e){
    console.log("testerror");
    console.log(e);
    }

    错误提示

    第二种情况怎么理解呢?

    try,catch只能捕捉到当前执行流里面的运行错误,对于异步回调来说,是不能被try,catch来捕捉的。我们知道js是单线程的,回调里面的都被放到了任务队列里面。如下例子:

    1
    2
    3
    4
    5
    6
    7
    8
    try{
    var btn = document.getElementsByTagName("button")[0];
    btn.onclick = function(){
    i<l;
    }
    }catch(e){
    console.log(e);
    }

    这时候我们打开浏览器是不会有任何东西输出的。


    利用window.onerror方法

    先具体看下onerror在什么情况下会被触发:An event handler for the error event. Error events are fired at various targets for different kinds of errors:

    When a JavaScript runtime error (including syntax errors) occurs, an error event using interface ErrorEvent is fired at window and window.onerror() is invoked.

    When a resource (such as an <img> or <script>) fails to load, an error event using interface Event is fired at the element, that initiated the load, and the onerror() handler on the element is invoked. These error events do not bubble up to window, but (at least in Firefox) can be handled with a single capturing window.addEventListener.

    具体的参数和语法如下,可以去MDN上看.

    onerror语法

    比如我们就把onerror的arguments对象打印出来就可以得到下面的结果:

    或者可以获得到出错信息的文件名,行号,列号,并且通过设置返回值为true或者false来保证信息不输出到控制台上。比如下面的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    window.onerror = function(message, source, lineno, colno, error) {
    console.log("-----------");
    console.log(message);
    console.log(source);
    console.log(lineno);
    console.log(colno);
    console.log(error);
    return true;
    }
    //line1
    if = 1;

    会得到下面的结果:

    错误提示

    这里注意下,语法错误不能放在onerror代码块内部,如果放在里面,因为语法在这个onerror里面都没有被通过,就更不可能会被执行了,就会跟平常一样代码直接打印到控制台。所以我们要把onerror单独提取出来,并放在其他脚本前面执行。

    错误提示

    这里还要注意一点,就是如果我们的js文件不是本地文件,是跨域文件,当这个文件出现了错误时,我们是不能够获得详细的信息的。只会简单报错: Script error,这样是为了防止信息泄露,避免不安全。那如果我们确实要跨域访问,怎么办呢?

    我们需要在客户端和服务器端同时设置可跨域访问。客户端中的imagescript标签都有 crossorigin参数,这个属性是告诉浏览器我要加载的这个资源是可以信任的。并在服务器端再设置:Access-Control-Allow-Origin的响应头。比如header('Access-Control-Allow-Origin: *');。下面这段话仔细看下可能更加好理解。

    When a syntax(?) error occurs in a script, loaded from a different origin, the details of the syntax error are not reported to prevent leaking information (see bug 363897). Instead the error reported is simply “Script error.” This behavior can be overriden in some browsers using the crossorigin attribute on <script> and having the server send the appropriate CORS HTTP response headers.


    解决方案

    在解决具体问题前,我们来看下window.onerror里面的第五个参数,error,这个信息。它看起来并不是那么特别。因为它里面包含几个例如:message, fileName, and lineNumber。这些事已经直接在前面参数提供给了你的。但是这个里面有一个非标准的属性,Error.prototype.stack。这个是追踪错误信息的关键所在。而且虽然不是标准的,但是是在主流浏览器中都支持的。这个error对象是会发生变化的,比如如果是语法错误,就会显示的是下面的SyntaxError,如果是未定义错误,会是ReferenceError。打印下error.stack

    错误提示

    这个里面的stack属性,就是可以追踪的所在,我们这里的错误比较简单,所以是只有一层。

    错误提示

    如果是比较复杂的,打印出来可能是类似这样的。

    Error: foobar
    at bar (Unknown script code:2:5)
    at foo (Unknown script code:6:5)
    at Anonymous function (Unknown script code:11:5)
    at Anonymous function (Unknown script code:10:2)
    at Anonymous function (Unknown script code:1:73)
    

    这里具体的代码我在网上看到有人写了两种,我觉得两种都有自己的优缺点。学习了。

    第一个来自知乎的水歌,链接为:水歌的回答。学习了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    (function (BOM, $) {
    var Console_URL = $('head link[rel="console"]').attr('href');
    BOM.onerror = function (iMessage, iURL, iLine, iColumn, iError){
    BOM.setTimeout(function () {
    var iData = {
    message: iMessage,
    url: iURL,
    line: iLine,
    column: iColumn || (BOM.event && BOM.event.errorCharacter) || 0
    };
    if (iError && iError.stack)
    iData.stack = (iError.stack || iError.stacktrace).toString();
    if (Console_URL) {
    if (iData.stack)
    $.post(Console_URL, iData);
    else
    $.get(Console_URL, iData);
    }
    }, 0);
    return true;
    };
    })(self, self.jQuery);

    这段代码是只追踪了一层stack,并提交给了server。看到下面的post和get方法分别提交了吗?这个作者的回答是:

    如果有详细的调用堆栈信息,就用 POST 这种理论上无限容量的方法发送,否则就只把 错误简介、文件名、出错行号 等信息用 GET 发送,以提高网络传输、服务器端解析的性能~

    第二种写法来自rapheal的博客,rapheal的这种写法,是追踪了3层错误信息。学习了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    window.onerror = function(msg,url,line,col,error){
    //没有URL不上报!上报也不知道错误
    if (msg != "Script error." && !url){
    return true;
    }
    //采用异步的方式
    //我遇到过在window.onunload进行ajax的堵塞上报
    //由于客户端强制关闭webview导致这次堵塞上报有Network Error
    //我猜测这里window.onerror的执行流在关闭前是必然执行的
    //而离开文章之后的上报对于业务来说是可丢失的
    //所以我把这里的执行流放到异步事件去执行
    //脚本的异常数降低了10倍
    setTimeout(function(){
    var data = {};
    //不一定所有浏览器都支持col参数
    col = col || (window.event && window.event.errorCharacter) || 0;
    data.url = url;
    data.line = line;
    data.col = col;
    if (!!error && !!error.stack){
    //如果浏览器有堆栈信息
    //直接使用
    data.msg = error.stack.toString();
    }else if (!!arguments.callee){
    //尝试通过callee拿堆栈信息
    var ext = [];
    var f = arguments.callee.caller, c = 3;
    //这里只拿三层堆栈信息
    while (f && (--c>0)) {
    ext.push(f.toString());
    if (f === f.caller) {
    break;//如果有环
    }
    f = f.caller;
    }
    ext = ext.join(",");
    data.msg = ext;
    }
    //把data上报到后台!
    },0);
    return true;
    };

    涨知识

    顺便提到这里,最近被问到了一个问题,怎么测试网页前端性能。我以前有用过webpagetest这个网站在线测试过我的网站的性能。但是当时并没有认真的去分析到底里面有些什么使网站的加载变得慢,或者哪些是可以优化的地方。反思。最近我又看了看,这个真的是很好。仔细看的话,可以详细掌握网站加载过程中的瀑布流、性能得分、元素分布、视图分析等数据。其中比较直观的视图分析功能可以直接看到页面加载各个阶段的截屏。还可以看到:白屏时间和首屏时间,即用户多久能在页面中看到内容,以及多久首屏渲染完成(包含图片等元素加载完成)。这两个时间点直接决定了用户需要等待多久才能看到自己想看到的信息。谷歌优化建议中也提到减少非首屏使用的 css 及 JS,尽快让首屏呈现。回头我专门再研究下,再写一篇博客。


    总结

    主要是学习了两种追踪错误的写法,一种是try,catch。一种是onerror。主要学习自下面这几篇文章。推荐阅读: