zoukankan      html  css  js  c++  java
  • 错误监控

    https://www.cnblogs.com/datiangou/p/10224846.html

    写在前面

    在前端项目中,由于JavaScript本身是一个弱类型语言,加上浏览器环境的复杂性,网络问题等等,很容易发生错误。做好网页错误监控,不断优化代码,提高代码健壮性是一项很重要的工作。本文将从Error开始,讲到如何捕获页面中的异常。文章较长,细节较多,请耐心观看。

    前端开发中的Error

    JavaScript中的Error

    JavaScript中,Error是一个构造函数,通过它创建一个错误对象。当运行时错误产生时,Error的实例对象会被抛出。构造一个Error的语法如下:

    
    // message: 错误描述
    // fileName: 可选。被创建的Error对象的fileName属性值。默认是调用Error构造器代码所在的文件的名字。
    // lineNumber: 可选。被创建的Error对象的lineNumber属性值。默认是调用Error构造器代码所在的文件的行号。
    
    new Error([message[, fileName[, lineNumber]]])
    ECMAScript标准:

    Error有两个标准属性:

    • Error.prototype.name :错误的名字
    • Error.prototype.message:错误的描述

    例如,在chrome控制台中输入以下代码:

    
    var a = new Error('错误测试');
    console.log(a); // Error: 错误测试
                    // at <anonymous>:1:9
    console.log(a.name); // Error
    console.log(a.message); // 错误测试

    Error只有一个标准方法:

    • Error.prototype.toString:返回表示一个表示错误的字符串。

    接上面的代码:

    
    a.toString();  // "Error: 错误测试"
    非标准的属性

    各个浏览器厂商对于Error都有自己的实现。比如下面这些属性:

    1. Error.prototype.fileName:产生错误的文件名。
    2. Error.prototype.lineNumber:产生错误的行号。
    3. Error.prototype.columnNumber:产生错误的列号。
    4. Error.prototype.stack:堆栈信息。这个比较常用。

    这些属性均不是标准属性,在生产环境中谨慎使用。不过现代浏览器差不多都支持了。

    Error的种类

    除了通用的Error构造函数外,JavaScript还有7个其他类型的错误构造函数。

    • InternalError: 创建一个代表Javascript引擎内部错误的异常抛出的实例。 如: "递归太多"。非ECMAScript标准。
    • RangeError: 数值变量或参数超出其有效范围。例子:var a = new Array(-1);
    • EvalError: 与eval()相关的错误。eval()本身没有正确执行。
    • ReferenceError: 引用错误。 例子:console.log(b);
    • SyntaxError: 语法错误。例子:var a = ;
    • TypeError: 变量或参数不属于有效范围。例子:[1,2].split('.')
    • URIError: 给 encodeURI或 decodeURl()传递的参数无效。例子:decodeURI('%2')

    当JavaScript运行过程中出错时,会抛出上8种(上述7种加上通用错误类型)错误中的其中一种错误。错误类型可以通过error.name拿到。

    你也可以基于Error构造自己的错误类型,这里就不展开了。

    其他错误

    上面介绍的都是JavaScript本身运行时会发生的错误。页面中还会有其他的异常,比如错误地操作了DOM。

    DOMException

    DOMException是W3C DOM核心对象,表示调用一个Web Api时发生的异常。什么是Web Api呢?最常见的就是DOM元素的一系列方法,其他还有XMLHttpRequest、Fetch等等等等,这里就不一一说明了。直接看下面一个操作DOM的例子:

    
    var node = document.querySelector('#app');
    var refnode = node.nextSibling;
    var newnode = document.createElement('div');
    node.insertBefore(newnode, refnode);
    
    // 报错:Uncaught DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.

    单从JS代码逻辑层面来看,没有问题。但是代码的操作不符合DOM的规则。

    DOMException构造函数的语法如下:

    
    // message: 可选,错误描述。
    // name: 可选,错误名称。常量,具体值可以在这里找到:https://developer.mozilla.org/zh-CN/docs/Web/API/DOMException
    
    new DOMException([message[, name]]);

    DOMException有以下三个属性:

    1. DOMException.code:错误编号。
    2. DOMException.message:错误描述。
    3. DOMException.name:错误名称。

    以上面那段错误代码为例,其抛出的DOMException各属性的值为:

    
    code: 8
    message: "Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node."
    name: "NotFoundError"
    Promise产生的异常

    Promise中,如果Promisereject了,就会抛出异常:PromiseRejectionEvent。注意,下面两种情况都会导致Promisereject

    1. 业务代码本身调用了Promise.reject
    2. Promise中的代码出错。

    PromiseRejectionEvent的构造函数目前在浏览器中大多都不兼容,这里就不说了。

    PromiseRejectionEvent的属性有两个:

    1. PromiseRejectionEvent.promise:被rejectPromise
    2. PromiseRejectionEvent.reasonPromisereject的原因。会传递给rejectPromsiecatch中的参数。
    加载资源出错

    由于网络,安全等原因,网页加载资源失败,请求接口出错等,也是一种常见的错误。

    关于错误的小结

    一个网页在运行过程中,可能发生四种错误:

    1. JavaScript在运行过程,语言自身抛出的异常。
    2. JavaScript在运行过程中,调用Web Api时发生异常。
    3. Promise中的拒绝。
    4. 网页加载资源,调用接口时发生异常。

    我认为,对于前两种错误,我们在平时的开发过程中,不用特别去区分,可以统一成:【代码出错】。

    捕获错误

    网页发生错误,开发者如何捕获这些错误呢 ? 常见的有以下方法。

    try...catch...

    try...catch…大家都不陌生了。一般用来在具体的代码逻辑中捕获错误。

    
    try {
      throw new Error("oops");
    }
    catch (ex) {
      console.log("error", ex.message); // error oops
    }

    try-block中的代码发生异常时,可以在catck-block中将异常接住,浏览器便不会抛出错误。但是,这种方式并不能捕获异步代码中的错误,如:

    
    try {
        setTimeout(function(){
            throw new Error('lala');
        },0);
    } catch(e) {
        console.log('error', e.message);
    }

    这个时候,浏览器依然会抛出错误:Uncaught Error: lala

    试想以下,如果我们将所有的代码合理的划分,然后都用try catch包起来,是不是就可以捕获到所有的错误了呢?可以通过编译工具来实现这个功能。不过,try catch是比较耗费性能的。

    window.onerror

    
    window.onerror = function(message, source, lineno, colno, error) { ... }

    函数参数:

    • message:错误信息(字符串)
    • source:发生错误的脚本URL(字符串)
    • lineno:发生错误的行号(数字)
    • colno:发生错误的列号(数字)
    • error:Error对象(对象)

    注意,如果这个函数返回true,那么将会阻止执行浏览器默认的错误处理函数。

    window.addEventListener('error')

    
    window.addEventListener('error', function(event) { ... })

    我们调用Object.prototype.toString.call(event),返回的是[object ErrorEvent]。可以看到eventErrorEvent对象的实例。ErrorEvent是事件对象在脚本发生错误时产生,从Event继承而来。由于是事件,自然可以拿到target属性。ErrorEvent还包括了错误发生时的信息。

    • ErrorEvent.prototype.message: 字符串,包含了所发生错误的描述信息。
    • ErrorEvent.prototype.filename: 字符串,包含了发生错误的脚本文件的文件名。
    • ErrorEvent.prototype.lineno: 数字,包含了错误发生时所在的行号。
    • ErrorEvent.prototype.colno: 数字,包含了错误发生时所在的列号。
    • ErrorEvent.prototype.error: 发生错误时所抛出的 Error 对象。

    注意,这里的ErrorEvent.prototype.error对应的Error对象,就是上文提到的ErrorInternalErrorRangeErrorEvalErrorReferenceErrorSyntaxErrorTypeErrorURIErrorDOMException中的一种。

    window.addEventListener('unhandledrejection')

    
    window.addEventListener('unhandledrejection', function (event) { ... });

    在使用Promise的时候,如果没有声明catch代码块,Promise的异常会被抛出。只能通过这个方法或者window.onunhandledrejection才能捕获到该异常。

    event就是上文提到的PromiseRejectionEvent。我们只需要关注其reason就行。

    window.onerror 和 window.addEventListener('error')的区别

    1. 首先是事件监听器事件处理器的区别。监听器只能声明一次,后续的声明会覆盖之前的声明。而事件处理器则可以绑定多个回调函数。
    2. 资源( <img> 或 <script> )加载失败时,加载资源的元素会触发一个Event接口的error事件,并执行该元素上的onerror()处理函数。但这些error事件不会向上冒泡到window。不过,这些error事件能被window.addEventListener('error')捕获。也就是说,面对资源加载失败的错误,只能用window.addEventListerner('error')window.onerror无效。

    关于错误捕获的小结

    我认为,在开发的过程中,对于容易出错的地方,可以使用try{}catch(){}来进行错误的捕获,做好兜底处理,避免页面挂掉。而对于全局的错误捕获,在现代浏览器中,我倾向于只使用使用window.addEventListener('error')window.addEventListener('unhandledrejection')就行了。如果需要考虑兼容性,需要加上window.onerror,三者同时使用,window.addEventListener('error')专门用来捕获资源加载错误。

    跨域脚本错误,Script Error

    在进行错误捕获的过程中,很多时候并不能拿到完整的错误信息,得到的仅仅是一个"Script Error"

    产生原因

    由于12年前这篇文章里提到的安全问题:https://blog.jeremiahgrossman...,浏览器们都对内核进行了升级:

    当加载自不同域的脚本中发生语法错误时,为避免信息泄露,语法错误的细节将不会报告,而是使用简单的"Script error."代替

    一般而言,页面的JS文件都是放在CDN的,和页面自身的URL产生了跨域问题,所以引起了"Script Error"

    解决办法

    服务端添加Access-Control-Allow-Origin,页面在script标签中配置 crossorigin="anonymous"。这样,便解决了因为跨域而带来的"Script Error"问题。

    能绕过Script Error

    上面介绍了"Script Error"的标准解决方案。但是,并不是所有的浏览器都支持crossorigin="anonymous",也不是所有的服务端都能及时配置Access-Control-Allow-Origin,这种情况下,还有什么方法能在全局捕获到所有的错误,并拿到详细信息呢?

    劫持原生方法

    看一个例子:

    
    const nativeAddEventListener = EventTarget.prototype.addEventListener; // 先将原生方法保存起来。
    EventTarget.prototype.addEventListener = function (type, func, options) { // 重写原生方法。
        const wrappedFunc = function (...args) { // 将回调函数包裹一层try catch
            try { 
                return func.apply(this, args);
            } catch (e) {
                const errorObj = {
                    ...
                    error_name: e.name || '',
                    error_msg: e.message || '',
                    error_stack: e.stack || (e.error &amp;&amp; e.error.stack),
                    error_native: e,
                    ...
                };
                // 接下来可以将errorObj统一进行处理。
            }
        }
        return nativeAddEventListener.call(this, type, wrappedFunc, options); // 调用原生的方法,保证addEventListener正确执行
    }

    我们劫持了原生的addEventListener代码,对addEventListener代码中的回调函数加了一层try{}catch(){},这样,回调函数中抛出的错误会被catch住,浏览器不会对try-catch 起来的异常进行跨域拦截,所以我们可以拿到详细的错误信息。通过上面的操作,我们可以拿到所有监听事件的回调函数中的错误啦。其他的场景怎么办呢?继续劫持原生方法。

    一个前端项目中,除了事件监听,接口请求也是一个频繁出现的场景。接着上面的代码,下面我们来劫持一下Ajax

    
    
        if (!XMLHttpRequest) {
            return;
        }
    
        const nativeAjaxSend = XMLHttpRequest.prototype.send; // 首先将原生的方法保存。
        const nativeAjaxOpen = XMLHttpRequest.prototype.open;
    
    
        XMLHttpRequest.prototype.open = function (mothod, url, ...args) { // 劫持open方法,是为了拿到请求的url
            const xhrInstance = this; 
            xhrInstance._url = url;
            return nativeAjaxOpen.apply(this, [mothod, url].concat(args));
        }
    
        XMLHttpRequest.prototype.send = function (...args) { // 对于ajax请求的监控,主要是在send方法里处理。
    
            const oldCb = this.onreadystatechange;
            const oldErrorCb = this.onerror;
            const xhrInstance = this;
    
            xhrInstance.addEventListener('error', function (e) { // 这里捕获到的error是一个ProgressEvent。e.target 的值为 XMLHttpRequest的实例。当网络错误(ajax并没有发出去)或者发生跨域的时候,会触发XMLHttpRequest的error, 此时,e.target.status 的值为:0,e.target.statusText 的值为:''
              
                const errorObj = {
                    ...
                    error_msg: 'ajax filed',
                    error_stack: JSON.stringify({
                        status: e.target.status,
                        statusText: e.target.statusText
                    }),
                    error_native: e,
                    ...
                }
              
                /*接下来可以对errorObj进行统一处理*/
              
            });
    
    
            xhrInstance.addEventListener('abort', function (e) { // 主动取消ajax的情况需要标注,否则可能会产生误报
                if (e.type === 'abort') { 
                    xhrInstance._isAbort = true;
                }
            });
    
    
            this.onreadystatechange = function (...innerArgs) {
                if (xhrInstance.readyState === 4) {
                    if (!xhrInstance._isAbort &amp;&amp; xhrInstance.status !== 200) { // 请求不成功时,拿到错误信息
                       const errorObj = {
                            error_msg: JSON.stringify({
                                code: xhrInstance.status,
                                msg: xhrInstance.statusText,
                                url: xhrInstance._url
                            }),
                            error_stack: '',
                            error_native: xhrInstance
                        };
                        
                        /*接下来可以对errorObj进行统一处理*/
                        
                    }
                    
                }
                oldCb &amp;&amp; oldCb.apply(this, innerArgs);
            }
            return nativeAjaxSend.apply(this, args);
        }
    }

    我们引用框架时,某些框架会用console.error的方法抛出错误。我们可以劫持console.error,来捕获错误。

    
            const nativeConsoleError = window.console.error;
            window.console.error = function (...args) {
                args.forEach(item =&gt; {
                    if (typeDetect.isError(item)) {
                       ...
                    } else {
                       ...
                    }
                });
                nativeConsoleError.apply(this, args);
            }

    原生的方法有很多,还比如fetchsetTimeout等。这里不一一列举了。但是使用劫持原生方法以覆盖所有的场景是十分困难的。

    前端框架是怎么捕获错误的

    我们主要来看一下ReactVue是怎么解决错误捕获问题的。

    React中的错误捕获

    Reactv16以前,可以使用unstable_handleError来处理捕获的错误。Reactv16以后,使用componentDidCatch来处理捕获的错误。若需全局捕获错误,可以在最外层包裹一层组件,在componentDidCatch中捕获错误信息。具体用法参考官方文档:https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html

    React中,错误会被throw出来。在写作本文的时候,我遇到一个问题,如果在加载react相关的代码前,按照上文的方法劫持addEventListener,那么React将不会正常工作了,但是没有任何报错。React有一套自己的事件系统,会不会和这个有关呢?之前没有研究过React源码,粗略调试了以下,没有发现问题所在。后续会仔细研究。

    Vue中的错误捕获

    Vue的源码中,在关键函数(比如钩子函数等)执行的时候,都加上try{}catch(){},在cacth中处理捕获到的错误。看下面的源码。

    
    ...
    // vue源码片段
    function callHook (vm, hook) {
      // #7573 disable dep collection when invoking lifecycle hooks
      pushTarget();
      var handlers = vm.$options[hook];
      if (handlers) {
        for (var i = 0, j = handlers.length; i &lt; j; i++) {
          try {
            handlers[i].call(vm);
          } catch (e) {
            handleError(e, vm, (hook + " hook"));
          }
        }
      }
      if (vm._hasHookEvent) {
        vm.$emit('hook:' + hook);
      }
      popTarget();
    }
    ...
    function globalHandleError (err, vm, info) {
      if (config.errorHandler) {
        try {
          return config.errorHandler.call(null, err, vm, info)
        } catch (e) {
          logError(e, null, 'config.errorHandler');
        }
      }
      logError(err, vm, info);
    }
    
    function logError (err, vm, info) {
      {
        warn(("Error in " + info + ": "" + (err.toString()) + """), vm);
      }
      /* istanbul ignore else */
      if ((inBrowser || inWeex) &amp;&amp; typeof console !== 'undefined') {
        console.error(err);
      } else {
        throw err
      }
    }

    Vue中提供了Vue.config.errorHandler`来处理捕获到的错误。

    
    // err: 捕获到的错误对象。
    // vm: 出错的VueComponent.
    // info: Vue 特定的错误信息,比如错误所在的生命周期钩子
    Vue.config.errorHandler = function (err, vm, info) {}

    如果开发者没有配置Vue.config.errorHandler,那么捕获到的错误会以console.error的方式输出。

    上报错误

    捕获到错误后,如何上报呢?最常见、最简单的方式就是通过<img>了。代码简单,且没有跨域烦恼。

    
    function logError(error){
        var img = new Image();
        img.onload = img.onerror = function(){
            img = null;
        }
        img.src = `${上报地址}?${processErrorParam(error)}`;
    }

    当上报数据比较多时,可以使用post的方式进行上报。

    错误的上报其实是一项复杂的工程,涉及到上报策略、上报分类等等。特别是在项目的业务比较复杂的时候,更应该关注上报的质量,避免影响到业务功能的正常运行。使用了打包工具处理的代码,往往还需要结合sourceMap进行代码定位。本文就不做介绍了。

    写在后面

    要建立一套完整、可用的前端错误监控体系是一项复杂、浩大的工程。但是,这项工程往往是必备的。本文主要介绍了你可能没关注过的Error的一些细节,以及如何捕获页面中的错误。关于劫持原生方法部分的代码,你可以在https://github.com/CoyPan/Fec找到。

    符合预期。

  • 相关阅读:
    7月15日考试 题解(链表+状压DP+思维题)
    暑假集训日记
    C# .NET 使用 NPOI 生成 .xlsx 格式 Excel
    JavaSE 基础 第42节 局部内部类
    JavaSE 基础 第41节 匿名内部类
    JavaSE 基础 第40节 内部类概述
    JavaSE 基础 第39节 接口的应用
    JavaSE 基础 第38节 接口的实现
    JavaSE 基础 第37节 接口概述
    JavaSE 基础 第36节 抽象类概述与使用
  • 原文地址:https://www.cnblogs.com/shangyueyue/p/11084235.html
Copyright © 2011-2022 走看看