Js错误处理

前端异常处理-ttc

前端异常分类

JS 语法错误、代码异常
AJAX 请求异常
静态资源加载异常
Promise 异常
Iframe 异常
跨域 Script error
崩溃和卡顿

异常处理

Try-Catch

无法捕捉到语法错误及异步错误,只能捕捉运行时错误;
可以拿到出错的信息,堆栈,出错的文件、行号、列号;
只能捕获块里面的错误;

window.onerror

注意:onerror 最好写在所有 JS 脚本的前面,否则有可能捕获不到错误;
onerror 无法捕获语法错误;
在实际的使用过程中,onerror 主要是来捕获预料之外的错误(包括全局错误)
而 try-catch 则是用来在可预见情况下监控特定的错误,两者结合使用更加高效

window.addEventListener

Promise Catch

window.addEventListener("unhandledrejection", function(e){})

VUE errorHandler

iframe 异常

Script error

崩溃和卡顿

Service Worker 有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker 一般情况下不会崩溃;
Service Worker 生命周期一般要比网页还要长,可以用来监控网页的状态;网页可以通过 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 发送消息。

错误上报

通过 Ajax 发送数据 因为 Ajax 请求本身也有可能会发生异常,而且有可能会引发跨域问题,一般情况下更推荐使用动态创建 img 标签的形式进行上报。
动态创建 img 标签的形式

收集异常信息量太多,怎么办?实际中,我们不得不考虑这样一种情况:如果你的网站访问量很大,那么一个必然的错误发送的信息就有很多条,这时候,我们需要设置采集率,从而减缓服务器的压力:

总结

可疑区域增加 Try-Catch
全局监控 JS 异常 window.onerror
全局监控静态资源异常 window.addEventListener
捕获没有 Catch 的 Promise 异常:unhandledrejection
VUE errorHandler
监控网页崩溃:window 对象的 load 和 beforeunload
跨域 crossOrigin 解决

前端异常监控

$_PS: 主要说了window.onerror。现代异常及上报无

如果说前端的异常监控有个救星的话,我想那就是 window.onerror 这个全局错误监听事件了。它给了我们统一处理前端全局错误的机会,使得错误上报有了一线生机。

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

  • message: 错误信息,在 HTML 中的 onerror 属性中设置的回调可以传递事件
  • source: 出错文件的 url
  • lineno: 出错位置的行数
  • colno: 出错时的列数
  • error: 出错时的 Error 对象。

实践中发现最后个参数 Error 对象中的值因浏览器的实现各有差异,比如 Chrome 中包含 messagestack,而 Safari 中则包含了前面四个参数的所有值。这在下面的示例代码的结果中可以看得出来。

window.onerror

注意
window.onerror 需要在有服务端的情况下才能正常工作,本地直接打开页面测试获取不到任何有用的错误信息。可以在命令行启动一个简单的服务端来进行测试。
因为 Mac 自带 Python,一般需要用到服务端的时候,我喜欢用 Python 自带的 SimpleHTTPServer

1
2
3
python -m SimpleHTTPServer
or
python3 -m http.server

以下代码我们对全局错误进行监听,然后将错误打印到页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
window.onerror = function (msg, source, line, col, error) {
printError.apply(null, arguments);
};

function printError(msg, source, line, col, error) {
var detail =
'msg:' +
msg +
'\ncourse:' +
source +
'\nline:' +
line +
'\ncol:' +
col +
'\nerror:' +
JSON.stringify(error, Object.getOwnPropertyNames(error));
var div = document.createElement('pre');
div.innerHTML = detail
document.body.appendChild(div);
}

然后在页面放上按钮以触发错误。这里测试了两种错误,一种运行时 JS 的抛错,另一种手动在代码中抛出的错误。

1
2
<button onclick="excptionGenerate()">点我执行出错代码</button>
<button onclick="throwError()">点我手动抛出异常</button>

Chrome 中异常的捕获与打印

Safari 中异常的捕获与打印

浏览器兼容性

要知道,最初版本的全局错误监听事件是这样的:

1
window.onerror = function(messageOrEvent, source, lineno) { ... }

后来才增加了 colnoerror。而后来加的这两个参数其实是非常有用的。

因为线上代码一般为压缩过的代码,所有内容都在一行,假如没有提供发生问题的列数,这样的错误日志要追查起来很不方便。
错误对象则直接提供了错误堆栈信息(通过 error.stack 访问),就像我们在浏览器控制台看到的一样,对于定位问题十分有帮助。

主流浏览器中, Chrome, Safari 已经完成了5个参数的支持。
Firefox 从 31 开始支持了完整的5个参数。
截止到目前, 微软的 Edge 浏览器还没有实现对新增两个参数的支持。其实现情况可以在这里查阅得到。

  • 小贴士 *

    过程中顺便发现了微软Edge这个API Catalog页面可以查到主流浏览器对名前端特性的实现情况,数据比 caniuse 全,譬如 window.onerror 在 caniuse 上则没有。

MS Edge 浏览器对 `window.onerror` 第五个参数的实现情况

从这里也可以看到,其他主流浏览器都已经有了完整的支持。

IE,(逃~)

try catch

对于不提供第5个参数的环境,我们是拿不到错误堆栈信息的。这种情况下对错误的追查帮助不大。
但是,手动在代码中捕获并抛出的错误,是带了堆栈信息的。这就有了补救的希望。我们可以将可能出错的地方,或者我们期望进行监控的地方,使用 try catch。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function tryCatchError() {
try {
a();
} catch (error) {
printError(error)
}
}

function printError(error) {
var detail = 'error:' +
JSON.stringify(error, Object.getOwnPropertyNames(error)) +
'\n\n';
}

var div = document.createElement('code');
div.innerHTML = detail
document.body.appendChild(div);
}

同时在页面中添加按钮来调用新的测试函数。

1
<button onclick="tryCatchError()">利用 try catch 捕获异常并打印错误堆栈</button>

try catch 中打印错误
我们看到,这种方式确实能得到详细的报错堆栈。

全局无法捕获的情况

除了考虑上面的浏览器兼容性问题外,还有其他一些情况,也是无法通过这个全局的 onerror 获取到详细报错信息的。

跨域情况的错误捕获(CDN)

window.onerror 有个限制,来自非同域的代码有报错,不会给出错误的详细信息,只能得到一个 Script error.。这是浏览器出于安全考虑,不向第三方泄露信息而做的一个措施。但往往线上代码大部分都部署在 CDN,所以这个限制的影响还挺常见。

不过还好,某些浏览器中可以通过配置来更改这一行为,让我们能正常拿到报错的详细信息。

还有一点,就是虽然在 window.onerror 中倒不到详细的报错信息,但在浏览器控制台是可以看到详细信息的。

如果是跨域脚本,则提示去控制台查看报错信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
window.onerror = function (msg, url, lineNo, columnNo, error) {
var string = msg.toLowerCase();
var substring = "script error";
if (string.indexOf(substring) > -1){
alert('Script Error: See Browser Console for Detail');
} else {
var message = [
'Message: ' + msg,
'URL: ' + url,
'Line: ' + lineNo,
'Column: ' + columnNo,
'Error object: ' + JSON.stringify(error)
].join(' - ');

alert(message);
}

return false;
};

控制台能看到对于线上的错误监控来说没多大用,还是得解决上报的问题。我们来看看如何设置跨域脚本让我们可以捕获时拿到错误堆栈信息。
下面看跨域脚本的配置。

  • CDN 上开启允许跨域

    1
    2
    3
    Access-Control-Allow-Origin:*
    或者
    Access-Control-Allow-Origin: domain of your site
  • 然后 script 标签上设置跨域标识为匿名

    1
    <script crossorigin="anonymous" src="//url/for/your/cdn/scripts"></script>

唯一需要注意的是,一旦在前端设置了 crossorigin,要确保服务端相应设置了允许跨域的响应头,否则整个脚本文件会加载失败,影响页面正常功能。
目前来看,除了 Opera外,各主流浏览器都有支持此属性。

跨域脚本加载浏览器兼容性

iframe 中异常的捕获

iframe 中发生异常,外界的 onerror 是不会触发的。但如果 iframe 地址同域,那么我们就可以设置 iframe 的全局 onerror 进行监听。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
document.getElementById("myiframe").contentWindow.onerror=function() {
alert('error!!');
return false;
}
```
以上代码需要保证在 iframe 加载完成后进行。

非同域情况下,如果 ifame 内的内容不来自第三方,也就是你自己可以控制,那么可以通过与 iframe 内进行通信的方式,将异常信息抛出来。iframe 通信试有很多,譬如 `postMessage`。这里不展开了。

非同域且内容不受自己控制的情况下,除了在控制台查看错误详细信息,真的没其他办法可以捕获了。


### 代码压缩在错误捕获中的还原
线上代码一般是压缩过的,如何更友好地展示还原事件发生地,对于错误上报也是个挑战。因为在错误监听的回调里面提供了列数,所以对于压缩后的代码,定位起位置来也不是难事,再结合错误对象里的报错堆栈信息,能够很好地定位代码的位置及原因。

关于压缩后的代码,有 sourse map 可以映射到源码,如果我们在异常捕获时通过与 soruce map 文件结合起来,那么就有可能在还原错误时分析出其在未压缩源码中的位置。目前来看,已经有相关的服务实现了这一功能,[sentry 的文档](https://docs.sentry.io/clients/javascript/sourcemaps/)中有提到。

前面讨论了一下错误详细信息的获取,最终我们是想在客户端收集到这些信息,再加上 ua, 发生错误的时间,设备相关信息等上报到服务端。总之,能够获取到信息越全越好,方便我们后期定位处理问题。当然,这套体系,市面上是有现成库已经做了的,来自 GitHub的这个代码仓库[cheeaun/javascript-error-logging](https://github.com/cheeaun/javascript-error-logging)收集了前端异常监控相关的资源,从 [GitHub 这个专题页面](https://github.com/topics/error-monitoring)也能找到许多。上面的坑库里面也都会覆盖到。

## 相关资料
<!-- $_PS: 没必要暴露了
* [GlobalEventHandlers.onerror](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror)
* [How to catch JavaScript Errors with window.onerror (even on Chrome and Firefox)](https://danlimerick.wordpress.com/2014/01/18/how-to-catch-javascript-errors-with-window-onerror-even-on-chrome-and-firefox/)
* [Cryptic “Script Error.” reported in Javascript in Chrome and Firefox](https://stackoverflow.com/questions/5913978/cryptic-script-error-reported-in-javascript-in-chrome-and-firefox)
* [Capture and report JavaScript errors with window.onerror](https://blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror.html)
* [MS Edge API Catalog](https://developer.microsoft.com/en-us/microsoft-edge/platform/catalog/?page=1&q=queryselector)
* [Script Error: JavaScript Forensics](https://trackjs.com/blog/script-error-javascript-forensics/)
* [CORS settings attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes)
* [can I catch exception of Iframe in parent window of Iframe](https://stackoverflow.com/questions/6327128/can-i-catch-exception-of-iframe-in-parent-window-of-iframe)

-->

# Web前端错误与性能监控实现原理
<!-- \# [Web前端错误与性能监控实现原理]https://ttc.zhiyinlou.com/#/articleDetail?id=2096 -->
<!-- author: 旷涛。 $_PS: 和伊美丹 差不多 -->

全局错误 window.onerror

资源加载错误
window.onerror不会捕获资源加载异常的情况。当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个Event接口的error事件,并执行该元素上的onerro()处理函数。这些error事件不会向上冒泡到window ,但是可以通过 window.addEventListener 捕获。

Promise 异常

用户行为监听
监听用户的点击行为,可以通过重写document.onclick函数来实现。

AJAX 请求监听
通过重写XMLHttpRequest,监听所有AJAX请求。

PV/UV


$_PS: 性能监听是怎么乱入了
## 三. 性能监听
监听方式
· 合成监控。第三方工具如 lighthouse ,成本低,采集数据丰富,样本小,
· 真实用户性能数据采集。成本高,一定程序影响用户访问体验,样本大。

Navigation Timing API 的性能指标,目前有两个版本。

首字节:responseStart – fetchStart
白屏时间:domInteractive – fetchStart
首屏时间:domContentLoadEventEnd – fetchStart 或者 loadEventStart - fetchStart
```js
function pagePerformance(){
let times = {};
let t = window.performance.timing;
console.log("useragent:", navigator.userAgent);
console.log("network type:", navigator.connection.effectiveType);
// 优先使用 navigation v2 https://www.w3.org/TR/navigation-timing-2/
if (typeof window.PerformanceNavigationTiming === 'function') {
try {
var nt2Timing = performance.getEntriesByType('navigation')[0]
if (nt2Timing) {
t = nt2Timing
}
} catch (err) {
}
}
//重定向时间
times.redirectTime = t.redirectEnd - t.redirectStart;
//dns查询耗时
times.dnsTime = t.domainLookupEnd - t.domainLookupStart;
//TTFB 读取页面第一个字节的时间
times.ttfbTime = t.responseStart - t.navigationStart;
//DNS 缓存时间
times.appcacheTime = t.domainLookupStart - t.fetchStart;
//卸载页面的时间
times.unloadTime = t.unloadEventEnd - t.unloadEventStart;
//tcp连接耗时
times.tcpTime = t.connectEnd - t.connectStart;
//request请求耗时
times.reqTime = t.responseEnd - t.responseStart;
//解析dom树耗时
times.analysisTime = t.domComplete - t.domInteractive;
//白屏时间
times.blankTime = (t.domInteractive || t.domLoading) - t.fetchStart;
//domReadyTime
times.domReadyTime = t.domContentLoadedEventEnd - t.fetchStart;
console.log(`[performance] ${JSON.stringify(times)}`);
}


// 监控信息上报
function sendMonitorInfo(url, data){
let reportUrl = url;
reportUrl += `?t=${new Date().getTime()}`;
Object.keys(data).forEach(item =&gt; {
reportUrl += `&${item}=${encodeURIComponent(data[item])}`
})
let img = new Image();
img.onload = function () {
img = null;
}
img.src = reportUrl;
}

四. 数据上报

上报方式
用获取图片(gif)形式上报。
• 首先POST/GET会有跨域的问题,图片没有跨域问题
• IMG是比较老标签没有兼容问题
• 缺点就是一次上报的数据量有限一般不超过 2K,同时对页面载入性能可能有影响

上报时机
如果每次收集到数据就直接上报,请求数会比较大,对后台要求比较高。可以采用定时上报外加监听unload事件,当发生unload时将剩下的数据全部上报。

上报数据
数据可以结合业务需要进行过滤,截断,采样命中。然后进行压缩处理后再上报。

后记

后续可以开垦的点:

  • 本地模拟跨域
  • 自己实现异常上报的库

[10 种最常见的 Javascript 错误]

2018.4.15 星期日 11:30

JavaScript-错误-Top-10
JavaScript-错误-Top-10

JavaScript-错误-Top-10

为了便于阅读,我们将每个错误描述都缩短了。接下来,让我们深入到每一个错误,来确定什么会导致它,以及如何避免创建它。

1. Uncaught TypeError: Cannot read property

2. TypeError: ‘undefined’ is not an object

这是在 Safari 中读取属性或调用未定义对象上的方法时发生的错误。您可以在 Safari Developer Console 中轻松测试。这与 1 中提到的 Chrome 的错误基本相同,但 Safari 使用了不同的错误消息提示语。

3. TypeError: null is not an object

这是在 Safari 中读取属性或调用空对象上的方法时发生的错误。

4. (unknown): Script error

当未捕获的 JavaScript 错误(通过window.onerror处理程序引发的错误,而不是捕获在try-catch中)被浏览器的跨域策略限制时,会产生这类的脚本错误。 例如,如果您将您的 JavaScript 代码托管在 CDN 上,则任何未被捕获的错误将被报告为“脚本错误” 而不是包含有用的堆栈信息。这是一种浏览器安全措施,旨在防止跨域传递数据,否则将不允许进行通信。

要获得真正的错误消息,请执行以下操作:
发送 ‘Access-Control-Allow-Origin’ 头部
将 Access-Control-Allow-Origin 标头设置为 * 表示可以从任何域正确访问资源。 如有必要,您可以将域替换为您的域:例如,Access-Control-Allow-Origin:www.example.com。 但是,处理多个域会变得棘手,如果你使用 CDN,可能由此产生更多的缓存问题会让你感觉到这种努力并不值得。 在这里看到更多。

这里有一些关于如何在各种环境中设置这个头文件的例子:

  1. Apache
    在 JavaScript 文件所在的文件夹中,使用以下内容创建一个 .htaccess 文件:

    Header add Access-Control-Allow-Origin"*"
    
  2. Nginx
    将 add_header 指令添加到提供 JavaScript 文件的位置块中:

    location ~^/assets/{
        add_header Access-Control-Allow-Origin*;
    }
    
  3. HAProxy
    将以下内容添加到您为 JavaScript 文件提供资源服务的后端:

    rspadd Access-Control-Allow-Origin:\ *
    

    <script> 中设置 crossorigin=”anonymous”在您的 HTML 代码中,对于您设置了Access-Control-Allow-Origin header 的每个脚本,在 script 标签上设置crossorigin =“anonymous”。在脚本标记中添加 crossorigin 属性之前,请确保验证上述 header 正确发送。 在 Firefox 中,如果存在crossorigin属性,但Access-Control-Allow-Origin头不存在,则脚本将不会执行。

5. TypeError: Object doesn’t support property

这是您在调用未定义的方法时发生在 IE 中的错误。
这相当于 Chrome 中的 “TypeError:”undefined“ is not a function” 错误。 是的,对于相同的逻辑错误,不同的浏览器可能具有不同的错误消息。

对于使用 JavaScript 命名空间的 Web 应用程序,这是一个 IE l浏览器的常见的问题。 在这种情况下,99.9% 的原因是 IE 无法将当前名称空间内的方法绑定到 this 关键字。 例如:如果你 JS 中有一个命名空间 Rollbar 以及方法 isAwesome 。

6. TypeError: ‘undefined’ is not a function

当您调用未定义的函数时,这是 Chrome 中产生的错误。

7. Uncaught RangeError: Maximum call stack

这是 Chrome 在一些情况下会发生的错误。 一个是当你调用一个不终止的递归函数。您可以在 Chrome 开发者控制台中进行测试。

此外,如果您将值传递给超出范围的函数,也可能会发生这种情况。 许多函数只接受其输入值的特定范围的数字。 例如:Number.toExponential(digits) 和 Number.toFixed(digits) 接受 0 到 20 的数字,Number.toPrecision(digits)接受 1 到 21 的数字。

8. TypeError: Cannot read property ‘length’

这是 Chrome 中发生的错误,因为读取未定义变量的长度属性。

9. Uncaught TypeError: Cannot set property

当我们尝试访问一个未定义的变量时,它总是返回 undefined,我们不能获取或设置任何未定义的属性。

10. ReferenceError: event is not defined

当您尝试访问未定义的变量或超出当前范围的变量时,会引发此错误。

结论

我们希望你学到了新的东西,可以避免将来的错误,或者本指南帮助你解决了头痛的问题。
尽管如此,即使有最佳实践,生产中也会出现意想不到的错误。能够查看影响用户的错误,并拥有快速解决问题的好工具,这一点非常重要。推荐 Rollbar。

其它:
最后,为你推荐
<!– 【第870期】JavaScript错误处理和堆栈追踪
【第318期】如何修复那些奇怪的 JavaScript 错误

–>

knowledge is no pay,reward is kindness
0%