63

前言

做好错误监控,将用户使用时的错误日志上报,可以帮助我们更快的解决一些问题。目前开源的比较好的前端监控有

那前端监控是怎么实现的呢?要想了解这个,需要知道前端错误大概分为哪些以及如何捕获处理。

前端错误分为JS运行时错误、资源加载错误和接口错误三种。

一、JS运行时错误

JS运行时错误一般使用window.onerror捕获,但是有一种特殊情况就是promise被reject并且错误信息没有被处理的时候抛出的错误

1.1 一般情况的JS运行时错误

使用window.onerror和window.addEventListener('error')捕获。

window.onerror = function (msg, url, lineNo, columnNo, error) 
    { 
       // 处理error信息
    } 
 
    window.addEventListener('error', event =>  
    {  
       console.log('addEventListener error:' + event.target); 
    }, true); 
    // true代表在捕获阶段调用,false代表在冒泡阶段捕获。使用true或false都可以
例子:https://jsbin.com/lujahin/edit?html,console,output 点击button抛出错误,分别被window.onerror和window.addEventListener('error')捕获

1.2 Uncaught (in promise)

当promise被reject并且错误信息没有被处理的时候,会抛出一个unhandledrejection,并且这个错误不会被window.onerror以及window.addEventListener('error')捕获,需要用专门的window.addEventListener('unhandledrejection')捕获处理

window.addEventListener('unhandledrejection', event => 
    { 
       console.log('unhandledrejection:' + event.reason); // 捕获后自定义处理
    });
https://developer.mozilla.org...
例子:https://jsbin.com/jofomob/edit?html,console,output 点击button抛出unhandledrejection错误,并且该错误仅能被window.addEventListener('unhandledrejection')捕获

1.3 console.error

一些特殊情况下,还需要捕获处理console.error,捕获方式就是重写window.console.error

var consoleError = window.console.error; 
window.console.error = function () { 
    alert(JSON.stringify(arguments)); // 自定义处理
    consoleError && consoleError.apply(window, arguments); 
};
例子:https://jsbin.com/pemigew/edit?html,console,output

1.4 特别说明跨域日志

什么是跨域脚本error?

https://developer.mozilla.org...
当加载自不同域的脚本中发生语法错误时,为避免信息泄露(参见bug 363897),语法错误的细节将不会报告,而代之简单的"Script error."。在某些浏览器中,通过在<script>使用crossorigin属性并要求服务器发送适当的 CORS HTTP 响应头,该行为可被覆盖。一个变通方案是单独处理"Script error.",告知错误详情仅能通过浏览器控制台查看,无法通过JavaScript访问。

例子: http://sandbox.runjs.cn/show/... 请打开页面打开控制台。该页面分别加载了两个不同域的js脚本,配置了crossorigin的window.onerror可以报出详细的错误,没有配置crossorigin只能报出'script error',并且没有错误信息

1.5 特别说明sourceMap

在线上由于JS一般都是被压缩或者打包(webpack)过,打包后的文件只有一行,因此报错会出现第一行第5000列出现JS错误,给排查带来困难。sourceMap存储打包前的JS文件和打包后的JS文件之间一个映射关系,可以根据打包后的位置快速解析出对应源文件的位置。

但是出于安全性考虑,线上设置sourceMap会存在不安全的问题,因为网站使用者可以轻易的看到网站源码,此时可以设置.map文件只能通过公司内网访问降低隐患

sourceMap配置devtool: 'inline-source-map'
如果使用了uglifyjs-webpack-plugin 必须把 sourceMap设置为true
https://doc.webpack-china.org...

1.6 其它

1.6.1 sentry把所有的回调函数使用try catch封装一层
https://github.com/getsentry/raven-js/blob/master/src/raven.js

1.6.2 vue errorHandler
https://vuejs.org/v2/api/#errorHandler
其原理也是使用try catch封装了nextTick,$emit, watch,data等
https://github.com/vuejs/vue/blob/dev/dist/vue.runtime.js

二、资源加载错误

使用window.addEventListener('error')捕获,window.onerror捕获不到资源加载错误

https://jsbin.com/rigasek/edit?html,console 图片资源加载错误。此时只有window.addEventListener('error')可以捕获到

window.onerror和window.addEventListener('error')的异同:相同点是都可以捕获到window上的js运行时错误。区别是1.捕获到的错误参数不同 2.window.addEventListener('error')可以捕获资源加载错误,但是window.onerror不能捕获到资源加载错误

window.addEventListener('error')捕获到的错误,可以通过target?.src || target?.href区分是资源加载错误还是js运行时错误

三、接口错误

所有http请求都是基于xmlHttpRequest或者fetch封装的。所以要捕获全局的接口错误,方法就是封装xmlHttpRequest或者fetch

3.1 封装xmlHttpRequest

if(!window.XMLHttpRequest) return;
var xmlhttp = window.XMLHttpRequest;
var _oldSend = xmlhttp.prototype.send;
var _handleEvent = function (event) {
    if (event && event.currentTarget && event.currentTarget.status !== 200) {
          // 自定义错误上报 }
}
xmlhttp.prototype.send = function () {
    if (this['addEventListener']) {
        this['addEventListener']('error', _handleEvent);
        this['addEventListener']('load', _handleEvent);
        this['addEventListener']('abort', _handleEvent);
    } else {
        var _oldStateChange = this['onreadystatechange'];
        this['onreadystatechange'] = function (event) {
            if (this.readyState === 4) {
                _handleEvent(event);
            }
            _oldStateChange && _oldStateChange.apply(this, arguments);
        };
    }
    return _oldSend.apply(this, arguments);
}

3.2 封装fetch

if(!window.fetch) return;
    let _oldFetch = window.fetch;
    window.fetch = function () {
        return _oldFetch.apply(this, arguments)
        .then(res => {
            if (!res.ok) { // True if status is HTTP 2xx
                // 上报错误
            }
            return res;
        })
        .catch(error => {
            // 上报错误
            throw error;  
        })
}

结论

  1. 使用window.onerror捕获JS运行时错误
  2. 使用window.addEventListener('unhandledrejection')捕获未处理的promise reject错误
  3. 重写console.error捕获console.error错误
  4. 在跨域脚本上配置crossorigin="anonymous"捕获跨域脚本错误
  5. window.addEventListener('error')捕获资源加载错误。因为它也能捕获js运行时错误,为避免重复上报js运行时错误,此时只有event.srcElement inatanceof HTMLScriptElement或HTMLLinkElement或HTMLImageElement时才上报
  6. 重写window.XMLHttpRequest和window.fetch捕获请求错误

利用以上原理,简单写了一个JS监控,只处理了一些JS错误,暂时没有做和性能相关的监控
https://github.com/Lie8466/better-js

如果发现文章有错误,欢迎指正。


深蓝一人
1.6k 声望65 粉丝

暂时没有个人简介