文档章节

一篇文章教你如何捕获前端错误

vivo互联网技术
 vivo互联网技术
发布于 2019/07/08 18:08
字数 2318
阅读 4.1K
收藏 61

本文首发于 vivo互联网技术 微信公众号 https://mp.weixin.qq.com/s/E51lKQOojsvhHvACIyXwhw
作者:黄文佳

常见错误的分类

对于用户在访问页面时发生的错误,主要包括以下几个类型:

1、js运行时错误

JavaScript代码在用户浏览器中执行时,由于一些边界情况、本地环境的不可控等因素,可能会存在js运行时错误。

而依赖客户端的某些方法,由于兼容性或者网络等问题,也有概率会出现运行时错误。

e.g: 下图是当使用了未定义的变量"foo",导致产生js运行时错误时的上报数据:

 

2、资源加载错误

这里的静态资源包括js、css以及image等。现在的web项目,往往依赖了大量的静态资源,而且一般也会有cdn存在。

如果某个节点出现问题导致某个静态资源无法访问,就需要能够捕获这种异常并进行上报,方便第一时间解决问题。

e.g: 下图是图片资源不存在时的上报数据:

3、未处理的promise错误

未使用catch捕获的promise错误,往往都会存在比较大的风险。而编码时有可能覆盖的不够全面,因此有必要监控未处理的promise错误并进行上报。

e.g: 下图是promise请求接口发生错误后,未进行catch时的上报数据:

4、异步请求错误(fetch与xhr)

异步错误的捕获分为两个部分:一个是传统的XMLHttpRequest,另一个是使用fetch api。

像axios和jQuery等库就是在xhr上的封装,而有些情况也可能会使用原生的fetch,因此对这两种情况都要进行捕获。

e.g: 下图是xhr请求接口返回400时捕获后的上报数据:

 

各个类型错误的捕获方式

1、window.onerror与window.addEventListener('error')捕获js运行时错误

使用window.onerror和window.addEventListener('error')都能捕获,但是window.onerror含有详细的error堆栈信息,存在error.stack中,所以我们选择使用onerror的方式对js运行时错误进行捕获。

window.onerror = function (msg, url, lineNo, columnNo, error) {
    // 处理错误信息
}
// demo
msg: Uncaught TypeError: Uncaught ReferenceError: a is not defined
error.statck: TypeError: ReferenceError: a is not defined at http://xxxx.js:1:13
window.addEventListener('error', event => (){ 
  // 处理错误信息
}, false);
// true代表在捕获阶段调用,false代表在冒泡阶段捕获。使用true或false都可以,默认为false

2、资源加载错误使用addEventListener去监听error事件捕获

实现原理:当一项资源(如<img>或<script>)加载失败,加载资源的元素会触发一个Event接口的error事件,并执行该元素上的onerror()处理函数。

这些error事件不会向上冒泡到window,不过能被window.addEventListener在捕获阶段捕获。

但这里需要注意,由于上面提到了addEventListener也能够捕获js错误,因此需要过滤避免重复上报,判断为资源错误的时候才进行上报。

window.addEventListener('error', event => (){ 
  // 过滤js error
  let target = event.target || event.srcElement;
  let isElementTarget = target instanceof HTMLScriptElement 
  || target instanceof HTMLLinkElement 
  || target instanceof HTMLImageElement;
  if (!isElementTarget) return false;
  // 上报资源地址
  let url = target.src || target.href;
  console.log(url);
}, true);

3、未处理的promise错误处理方式

实现原理:当promise被reject并且错误信息没有被处理的时候,会抛出一个unhandledrejection。

这个错误不会被window.onerror以及window.addEventListener('error')捕获,但是有专门的window.addEventListener('unhandledrejection')方法进行捕获处理。

window.addEventListener('rejectionhandled', event => {
  // 错误的详细信息在reason字段
  // demo:settimeout error
  console.log(event.reason);
});

4、fetch与xhr错误的捕获

对于fetch和xhr,我们需要通过改写它们的原生方法,在触发错误时进行自动化的捕获和上报。

改写fetch方法:

// fetch的处理
function _errorFetchInit () {
    if(!window.fetch) return;
    let _oldFetch = window.fetch;
    window.fetch = function () {
        return _oldFetch.apply(this, arguments)
        .then(res => {
            if (!res.ok) { // 当status不为2XX的时候,上报错误
            }
            return res;
        })
        // 当fetch方法错误时上报
        .catch(error => {
            // error.message,
            // error.stack
            // 抛出错误并且上报
            throw error; 
        })
    }
}

对于XMLHttpRequest的重写:

xhr改写


// xhr的处理
function _errorAjaxInit () {
    let protocol = window.location.protocol;
    if (protocol === 'file:') return;
    // 处理XMLHttpRequest
    if (!window.XMLHttpRequest) {
        return;  
    }
    let xmlhttp = window.XMLHttpRequest;    
    // 保存原生send方法
    let _oldSend = xmlhttp.prototype.send;
    let _handleEvent = function (event) {
        try {
            if (event && event.currentTarget && event.currentTarget.status !== 200) {
                    // event.currentTarget 即为构建的xhr实例
                    // event.currentTarget.response
                    // event.currentTarget.responseURL || event.currentTarget.ajaxUrl
                    // event.currentTarget.status
                    // event.currentTarget.statusText
                });
            }
        } catch (e) {va
            console.log('Tool\'s error: ' + e);
        }
    }
    xmlhttp.prototype.send = function () {
        this.addEventListener('error', _handleEvent); // 失败
        this.addEventListener('load', _handleEvent);  // 完成
        this.addEventListener('abort', _handleEvent); // 取消
        return _oldSend.apply(this, arguments);
    }
}

关于responseURL 的说明

需要特别注意的是,当请求完全无法执行的时候,XMLHttpRequest会收到status=0 和 statusText=null的返回,此时responseURL也为空string。

另外在安卓4.4及以下版本的webview中,xhr对象也不存在responseURL属性。

因此我们需要额外的改写xhr的open方法,将传入的url记录下来,方便上报时带上。

var _oldOpen = xmlhttp.prototype.open;
// 重写open方法,记录请求的url
xmlhttp.prototype.open = function (method, url) {
    _oldOpen.apply(this, arguments);
    this.ajaxUrl = url;
};

其他问题

1、其他框架,例如vue项目的错误捕获

vue内部发生的错误会被Vue拦截,因此vue提供方法给我们处理vue组件内部发生的错误。

Vue.config.errorHandler = function (err, vm, info) {  // handle error  // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子  // 只在 2.2.0+ 可用}

2、script error的解决方式

"script error.”有时也被称为跨域错误。当网站请求并执行一个托管在第三方域名下的脚本时,就可能遇到该错误。最常见的情形是使用 CDN 托管 JS 资源。

其实这并不是一个 JavaScript Bug。出于安全考虑,浏览器会刻意隐藏其他域的 JS 文件抛出的具体错误信息,这样做可以有效避免敏感信息无意中被不受控制的第三方脚本捕获。

因此,浏览器只允许同域下的脚本捕获具体错误信息,而其他脚本只知道发生了一个错误,但无法获知错误的具体内容。

解决方案1:(推荐)

添加 crossorigin="anonymous" 属性。

<script src="http://another-domain.com/app.js" 
        crossorigin="anonymous">
</script>

此步骤的作用是告知浏览器以匿名方式获取目标脚本。这意味着请求脚本时不会向服务端发送潜在的用户身份信息(例如 Cookies、HTTP 证书等)。

添加跨域 HTTP 响应头:

Access-Control-Allow-Origin: *

或者

 Access-Control-Allow-Origin: http://test.com

注意:大部分主流 CDN 默认添加了 Access-Control-Allow-Origin 属性。

完成上述两步之后,即可通过 window.onerror 捕获跨域脚本的报错信息。

解决方案2

难以在 HTTP 请求响应头中添加跨域属性时,还可以考虑 try catch 这个备选方案。

在如下示例 HTML 页面中加入 try catch:

<!doctype html>
<html>
<head>
    <title>Test page in http://test.com</title>
</head>
<body>
    <script src="http://another-domain.com/app.js"></script>
    // app.js里面有一个foo方法,调用了不存在的bar方法
    <script>
    window.onerror = function (message, url, line, column, error) {
        console.log(message, url, line, column, error);
    }
    try {
        foo();
    } catch (e) {
        console.log(e);

        throw e;
    }
</script>
</body>
</html>

// 运行输出结果如下:

=> ReferenceError: bar is not defined
at foo (http://another-domain.com/app.js:2:3)
at http://test.com/:15:3
=> "Script error.", "", 0, 0, undefined

可见 try catch 中的 Console 语句输出了完整的信息,但 window.onerror 中只能捕获“Script error”。根据这个特点,可以在 catch 语句中手动上报捕获的异常。

总结

上述的错误捕获基本覆盖了前端监控所需的错误场景,但是第三部分指出的两个其他问题,目前解决的方式都不太完美。

对于有使用框架的项目:一是需要有额外的处理流程,比如示例中就需要单独为vue项目进行初始化;二是对于其他框架,都需要单独处理,例如react项目的话,则需要使用官方提供的componentDidCatch方法来做错误捕获。

而对于跨域js捕获的问题:我们并不能保证所有的跨域静态资源都添加跨域 HTTP 响应头;而通过第二种包裹try-catch的方式进行上报,则需要考虑的场景繁多并且无法保证没有遗漏。

虽然存在这两点不足,但前端错误捕获这部分还是和项目的使用场景密切相关的。我们可以在了解这些方式以后,选择最适合自己项目的方案,为自己的监控工具服务。

—— —— 参考文档 —— ——

1.Using XMLHttpRequest: 

https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest

2.script error 产生的原因和解决办法: 

https://www.alibabacloud.com/help/zh/faq-detail/88579.htm

3.JavaScript执行错误: 

https://docs.fundebug.com/notifier/javascript/type/javascript.html

4.betterjs的script error: 

https://github.com/BetterJS/badjs-report/issues/3

5.Vuejs的errorHandler: 

https://cn.vuejs.org/v2/api/index.html#errorHandler

6.React的componentDidCatch: 

https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html

vivo 互联网web前端开发工程师火热招聘中,发送简历到 2020Labs@vivo.com,获取内推机会哦。

更多内容敬请关注 vivo 互联网技术 微信公众号

注:转载文章请先与微信号:labs2020 联系。

© 著作权归作者所有

vivo互联网技术
粉丝 46
博文 35
码字总数 128461
作品 0
深圳
私信 提问
加载中

评论(3)

南漂一卒
南漂一卒
fetch() 是基于 Promise 的,XHR 等传统异步 API 也可以封装成 Promise,就可以统一用 unhandledrejection 来处理
vivo互联网技术
vivo互联网技术 博主
ajax错误和unhandledrejection错误属于不同的类别,分开去捕获并上报有益于错误统计页面的分类和展示
南漂一卒
南漂一卒
看异常调用栈就知道了呀
从零开始搭建前端监控系统(二)——实现圈选(无埋点)

前言 本系列文章旨在讲解如何从零开始搭建前端监控系统。 项目已经开源 项目地址: github.com/bombayjs/bo… (web sdk) github.com/bombayjs/bo… (服务端,用于提供api)(未完) github.com/...

aoping
2019/09/30
0
0
从零开始搭建前端监控系统(一)——web探针sdk

前言 本系列文章旨在讲解如何从零开始搭建前端监控系统。 项目已经开源 项目地址: github.com/bombayjs/bo… (web sdk) github.com/bombayjs/bo… (服务端,用于提供api)(未完) github.com/...

aoping
2019/09/26
0
0
个人分享--web前端学习资源分享

1.前言 时间过得真快,转眼间现在是2017年最后一个星期,而今天也是圣诞节,过几天也是元旦了。每到年底,大家都习惯总结和分享,我也不例外。但是经历,我之前已经发过了,那么我今天就分享...

2017/12/25
0
0
【前端3分钟】Script Error产生的原因和解法

本文首发于知乎【前端3分钟】Script Error产生的原因和解法,搬运转载请注明出处,否则追究版权责任。 Script Error对于前端开发者相信都不陌生,由于没有具体错误堆栈和代码行列号,成为可能...

杂货铺老板
2019/10/14
0
0
驳《慎用 try catch》

今天在掘金看到了一篇文章,慎用 try catch,发布者的昵称是“前端妹子”。根据我的经验,这种昵称一般都不是妹子,大概率是营销号(PS:如果能换个美女头像就更走心了)。 看完之后我评论道...

justjavac
2018/12/19
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Kettle自定义jar包供javascript使用

我们都知道 Kettle 是用 Java 语言开发,并且可以在 JavaScript 里面直接调用 java 类方法。所以有些时候,我们可以自定义一些方法,来供 JavaScript 使用。 本篇文章有参考自:https://www...

CREATE_17
昨天
102
0
处理CSV文件中的逗号

我正在寻找有关如何处理正在创建的csv文件的建议,然后由我们的客户上传,并且该值可能带有逗号(例如公司名称)。 我们正在研究的一些想法是:带引号的标识符(值“,”值“,”等)或使用|...

javail
昨天
79
0
如何克隆一个Date对象?

将Date变量分配给另一个变量会将引用复制到同一实例。 这意味着更改一个将更改另一个。 如何实际克隆或复制Date实例? #1楼 简化版: Date.prototype.clone = function () { return new ...

技术盛宴
昨天
73
0
计算一个数的数位之和

计算一个数的数位之和 例如:128 :1+2+8 = 11 public int numSum(int num) { int sum = 0; do { sum += num % 10; } while ((num = num / 10) > 0); return sum;......

SongAlone
昨天
124
0
为什么图片反复压缩后普遍会变绿,而不是其他颜色?

作者:Lion Yang 链接:https://www.zhihu.com/question/29355920/answer/119088684 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 业余版概要:安卓的...

shzwork
昨天
81
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部