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

新闻 前端
随着前端页面承载功能越来越多,用户本地浏览器环境也错综复杂,因此即使有完善的测试,我们也无法保证上线的代码不会出错。

随着前端页面承载功能越来越多,用户本地浏览器环境也错综复杂,因此即使有完善的测试,我们也无法保证上线的代码不会出错。在这种场景下,前端页面的监控就成了各个web项目必备的工具。

 

一般对页面的监控包含页面性能、页面错误以及用户行为路径获取上报等。

 

而本文将重点关注其中的错误部分,主要介绍一下常见的错误类型以及如何对它们进行捕获并上报。

常见错误的分类

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

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运行时错误进行捕获。

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

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

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

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

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

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

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

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

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

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

4、fetch与xhr错误的捕获

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

改写fetch方法:

  1. // fetch的处理 
  2. function _errorFetchInit () { 
  3.     if(!window.fetch) return
  4.     let _oldFetch = window.fetch; 
  5.     window.fetch = function () { 
  6.         return _oldFetch.apply(this, arguments) 
  7.         .then(res => { 
  8.             if (!res.ok) { // 当status不为2XX的时候,上报错误 
  9.             } 
  10.             return res; 
  11.         }) 
  12.         // 当fetch方法错误时上报 
  13.         .catch(error => { 
  14.             // error.message, 
  15.             // error.stack 
  16.             // 抛出错误并且上报 
  17.             throw error;  
  18.         }) 
  19.     } 

对于XMLHttpRequest的重写:

xhr改写

  1. // xhr的处理 
  2. function _errorAjaxInit () { 
  3.     let protocol = window.location.protocol; 
  4.     if (protocol === 'file:'return
  5.     // 处理XMLHttpRequest 
  6.     if (!window.XMLHttpRequest) { 
  7.         return;   
  8.     } 
  9.     let xmlhttp = window.XMLHttpRequest;     
  10.     // 保存原生send方法 
  11.     let _oldSend = xmlhttp.prototype.send; 
  12.     let _handleEvent = function (event) { 
  13.         try { 
  14.             if (event && event.currentTarget && event.currentTarget.status !== 200) { 
  15.                     // event.currentTarget 即为构建的xhr实例 
  16.                     // event.currentTarget.response 
  17.                     // event.currentTarget.responseURL || event.currentTarget.ajaxUrl 
  18.                     // event.currentTarget.status 
  19.                     // event.currentTarget.statusText 
  20.                 }); 
  21.             } 
  22.         } catch (e) {va 
  23.             console.log('Tool\'s error: ' + e); 
  24.         } 
  25.     } 
  26.     xmlhttp.prototype.send = function () { 
  27.         this.addEventListener('error', _handleEvent); // 失败 
  28.         this.addEventListener('load', _handleEvent);  // 完成 
  29.         this.addEventListener('abort', _handleEvent); // 取消 
  30.         return _oldSend.apply(this, arguments); 
  31.     } 

关于responseURL 的说明

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

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

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

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

其他问题

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

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

  1. 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" 属性。

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

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

添加跨域 HTTP 响应头:

  1. Access-Control-Allow-Origin: * 

或者

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

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

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

解决方案2

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

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

  1. <!doctype html> 
  2. <html> 
  3. <head> 
  4.     <title>Test page in http://test.com</title> 
  5. </head> 
  6. <body> 
  7.     <script src="http://another-domain.com/app.js"></script> 
  8.     // app.js里面有一个foo方法,调用了不存在的bar方法 
  9.     <script> 
  10.     window.onerror = function (message, url, line, column, error) { 
  11.         console.log(message, url, line, column, error); 
  12.     } 
  13.     try { 
  14.         foo(); 
  15.     } catch (e) { 
  16.         console.log(e); 
  17.  
  18.         throw e; 
  19.     } 
  20. </script> 
  21. </body> 
  22. </html> 
  23.  
  24. // 运行输出结果如下: 
  25.  
  26. => ReferenceError: bar is not defined 
  27. at foo (http://another-domain.com/app.js:2:3) 
  28. at http://test.com/:15:3 
  29. => "Script error."""00, undefined 

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

总结

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

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

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

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

责任编辑:张燕妮 来源: vivo互联网技术
相关推荐

2022-10-08 15:07:06

ChatOps运维

2017-09-05 08:52:37

Git程序员命令

2021-03-08 09:15:46

日志Filebeat运维

2020-03-31 08:37:31

递归单链表反转

2020-10-09 08:15:11

JsBridge

2021-05-11 10:01:54

avaScript错误处理

2018-01-09 05:39:02

2022-02-21 09:44:45

Git开源分布式

2021-04-09 08:40:51

网络保险网络安全网络风险

2023-05-12 08:19:12

Netty程序框架

2024-06-25 08:18:55

2019-04-17 15:16:00

Sparkshuffle算法

2021-06-30 00:20:12

Hangfire.NET平台

2021-09-05 17:22:08

Strview.js工具js

2022-02-18 00:13:53

JavaScript编程语言数组

2021-11-04 10:34:02

JavaScript继承编程

2023-04-13 08:21:38

DevOpsAPI管理平台

2019-11-14 15:44:32

系统缓存架构

2021-07-01 10:01:16

JavaLinkedList集合

2021-06-04 09:56:01

JavaScript 前端switch
点赞
收藏

51CTO技术栈公众号