Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

前端开发中js代码异常处理及监控 #290

Open
confidence68 opened this issue Jun 16, 2018 · 0 comments
Open

前端开发中js代码异常处理及监控 #290

confidence68 opened this issue Jun 16, 2018 · 0 comments

Comments

@confidence68
Copy link
Owner

前言

其实,我在前端工作中,对错误异常处理做的比较少,因为我们知道,JavaScript 有基本的异常处理能力,前端开发过程中,很多错误js会直接抛出,但是这仅仅是表象的,有时候会因为环境不同,例如线下是好的,线上有问题,或者API等原因,造成js报错。针对这些报错,我们要及时捕获,才能不影响线上体验,减少损失!

异常捕获的方式

常见的js异常捕获一般有2中方式:

1、try..catch

2、 window.onerror

try..catch

try..catch我们都很熟悉了,一般用到自己比较明确的,容易出错的地方。

例如:输入框中代码校验、某个API进入非法状态等等,都用 try..catch

下面我们来模拟localStorage 超出容量的报错:

首先我们生成一个12MB的字符串:

var repeat = function(str, x) { return new Array(x+1).join(str); };
var too_big = repeat("x", 12*1024*1024/2); //每个js字符串是 2 bytes

var exception;
localStorage.clear();
try {
  localStorage.setItem("test", too_big);
} catch (e) {
  exception = e;
}

会报如下错误:

enter image description here

小结try,catch特点如下:

1、无法捕捉到语法错误,只能捕捉运行时错误;

2、可以拿到出错的信息,堆栈,出错的文件、行号、列号;

4、需要借助工具把所有的function块以及文件块加入try,catch,可以在这个阶段打入更多的静态信息。

window.onerror

由于try..catch只能捕获块里面的错误,全局的一些错误,例如:

1、JS脚本里边存着语法错误;

2、JS脚本在运行时发生错误。

针对这样的错误,我们用window.onerror来捕获。

但是这个方法存在兼容性问题,在不同的浏览器上提供的数据不完全一致,

部分过时的浏览器只能提供部分数据。它的用法如下:

window.onerror = function (message, url, lineNo, columnNo, error)

1、message {String}
错误信息。直观的错误描述信息,不过有时候你确实无法从这里面看出端倪,特别是压缩后脚本的报错信息,可能让你更加疑惑。

2、url {String} 发生错误对应的脚本路径,比如是你的https://www.haorooms.com/ 报错了还是 http://resource.haorooms.com/ 报错了。

3、lineNo {Number} 错误发生的行号。

4、columnNo {Number} 错误发生的列号。

5、error {Object} 具体的 error 对象,包含更加详细的错误调用堆栈信息,这对于定位错误非常有帮助

不同浏览器默认可获取的参数值如下:

enter image description here

window.onerrorjs错误上报库

;(function(){

    'use strict';

    if (window.badJsReport){ 

       return window.badJsReport 
    };

    /*
    *  默认上报的错误信息
    */ 
    var defaults = {
        msg:'',  //错误的具体信息
        url:'',  //错误所在的url
        line:'', //错误所在的行
        col:'',  //错误所在的列
        error:'', //具体的error对象
    };

    /*
    *ajax封装
    */
    function ajax(options) {
        options = options || {};
        options.type = (options.type || "GET").toUpperCase();
        options.dataType = options.dataType || "json";
        var params = formatParams(options.data);

        if (window.XMLHttpRequest) {
           var xhr = new XMLHttpRequest();
        } else { 
           var xhr = new ActiveXObject('Microsoft.XMLHTTP');
        }

        xhr.onreadystatechange = function () {
           if (xhr.readyState == 4) {
               var status = xhr.status;
               if (status >= 200 && status < 300) {
                   options.success && options.success(xhr.responseText, xhr.responseXML);
               } else {
                   options.fail && options.fail(status);
               }
           }
        }

        if (options.type == "GET") {
           xhr.open("GET", options.url + "?" + params, true);
           xhr.send(null);
        } else if (options.type == "POST") {
           xhr.open("POST", options.url, true);
           //设置表单提交时的内容类型
           xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
           xhr.send(params);
        }
    }

    /*
    *格式化参数
    */
    function formatParams(data) {
       var arr = [];
       for (var name in data) {
           arr.push(encodeURIComponent(name) + "=" + encodeURIComponent(data[name]));
       }
       arr.push(("v=" + Math.random()).replace(".",""));
       return arr.join("&");
    }


    /*
    * 合并对象,将配置的参数也一并上报
    */
    function cloneObj(oldObj) { //复制对象方法
      if (typeof(oldObj) != 'object') return oldObj;
      if (oldObj == null) return oldObj;
      var newObj = new Object();
      for (var prop in oldObj)
        newObj[prop] = oldObj[prop];
      return newObj;
    };

    function extendObj() { //扩展对象
      var args = arguments;
      if (args.length < 2) {return;}
      var temp = cloneObj(args[0]); //调用复制对象方法
      for (var n = 1,len=args.length; n <len; n++){
        for (var index in args[n]) {
          temp[index] = args[n][index];
        }
      }
      return temp;
    }

   /**
   * 核心代码区
   **/
   var badJsReport=function(params){

      if(!params.url){return}
      window.onerror = function(msg,url,line,col,error){

          //采用异步的方式,避免阻塞
          setTimeout(function(){

              //不一定所有浏览器都支持col参数,如果不支持就用window.event来兼容
              col = col || (window.event && window.event.errorCharacter) || 0;

              defaults.url = url;
              defaults.line = line;
              defaults.col =  col;

              if (error && error.stack){
                  //如果浏览器有堆栈信息,直接使用
                  defaults.msg = error.stack.toString();

              }else if (arguments.callee){
                  //尝试通过callee拿堆栈信息
                  var ext = [];
                  var fn = arguments.callee.caller;
                  var floor = 3;  //这里只拿三层堆栈信息
                  while (fn && (--floor>0)) {
                     ext.push(fn.toString());
                     if (fn  === fn.caller) {
                          break;//如果有环
                     }
                     fn = fn.caller;
                  }
                  defaults.msg = ext.join(",");
                }
                // 合并上报的数据,包括默认上报的数据和自定义上报的数据
                var reportData=extendObj(params.data || {},defaults);
                
                // 把错误信息发送给后台
                ajax({
                    url: params.url,      //请求地址
                    type: "POST",         //请求方式
                    data: reportData,     //请求参数
                    dataType: "json",
                    success: function (response, xml) {
                        // 此处放成功后执行的代码
                      params.successCallBack&&params.successCallBack(response, xml);
                    },
                    fail: function (status) {
                        // 此处放失败后执行的代码
                      params.failCallBack&&params.failCallBack(status);
                    }
                 });

          },0);

          return true;   //错误不会console浏览器上,如需要,可将这样注释
      };

  }
    
  window.badJsReport=badJsReport;

})();

/*===========================
badJsReport AMD Export
===========================*/
if (typeof(module) !== 'undefined'){
    module.exports = window.badJsReport;
}
else if (typeof define === 'function' && define.amd) {
    define([], function () {
        'use strict';
        return window.badJsReport;
    });
}

使用方法:

badJsReport({
  url:'http://www.haorooms.com',  //发送到后台的url  *必须
})

如果需要新增上报参数,或者要知道发送给后台的回调。可以用下面的方法

badJsReport({
  url:'http://www.haorooms.com', //发送到后台的url  *必须
  data:{},   //自定义添加上报参数,比如app版本,浏览器版本  -可省略
  successCallBack:function(response, xml){
      // 发送给后台成功的回调,-可省略
  },
  failCallBack:function(error){
      // 发送给后台失败的回调,-可省略
  }
})

注意点:

1、对于跨域的JS资源,window.onerror拿不到详细的信息,需要往资源的请求添加额外的头部。

静态资源请求需要加多一个Access-Control-Allow-Origin头部,也就是需要后台加一个Access-Control-Allow-Origin,同时script引入外链的标签需要加多一个crossorigin的属性。这样就可以获取准确的出错信息。

2、因为代码的最后return
true,所以如果有错误信息,浏览器不会console出来,如果需要浏览器console,可以注释掉最后的return true

缺点:

对于压缩之后的代码,我们得到错误的信息,但是我们却无法定位到错误的行数,比如jquery的源码压缩,总共才3行。这样就很难定位到具体的地方了,因为一行有很多很多的代码。关于这个问题,可以看看阮一峰的这篇文章:

JavaScript Source Map 详解

我通过uglifyJs模拟webpack压缩的配置将上文中的index.js压缩,得到source-map,通过mozilla/source-map的SourceMapConsumer接口,可以通过将转换后的行号列号传入Consumer得到原始错误位置信息。相应的node代码如下

var fs = require('fs')
var sourceMap = require('source-map')

// map文件
var rawSourceMapJsonData = fs.readFileSync('./dist/index.min.js.map', 'utf-8')
rawSourceMapJsonData = JSON.parse(rawSourceMapJsonData)

var consumer = new sourceMap.SourceMapConsumer(rawSourceMapJsonData);

// 打印出真实错误位置
console.log(consumer.originalPositionFor({line: 1, column: 220}))

通过上面方式得到错误行号。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant