JS 运行时错误(变量未声明,Can't read property 'xxx' of undefined 等)
Promise 抛出异常
外部脚本异常(如iframe,跨域脚本)
资源加载异常
常见异常示例
<script>
error
console.log('You wont see me.')
</script>
<script>
console.log('You will see me!!!')
</script>
输出
Uncaught ReferenceError: error is not defined
You will see me!!!
这里两个 script 标签属于两个不同的代码块,属于不同的宏任务,(JS 单线程的工作方式就是不停的去队列将任务取出来执行,具体细节参考 JS 的事件循环机制,本文不展开说明)因此可以看到第一个 log 被 JS 报错给阻断了,异常没有被捕获,但是这并不影响第二个任务块的执行
下面讨论下针对对以上异常的捕获方式
1. try-catch
最熟悉不过的就是 try-catch,还是用上面的例子
<script>
try{
error
}
catch(err) {
console.log('We Caught Error -> ', err)
}
console.log('Check if you can see me')
</script>
<script>
console.log('You will see me!!!')
</script>
输出
We Caught Error -> ReferenceError: error is not defined at error-script-demo.html:14
Check if you can see me
You will see me!!!
这时异常被 catch 掉了,所以不会阻断当前任务的执行,两个 log 都正常输出
另外 try-catch 无法捕获 JS 语法错误,举个🌰
<script>
try{
function()
}
catch(err) {
console.log('We Caught Error -> ', err)
}
console.log('Check if you can see me')
</script>
<script>
console.log('You will see me!!!')
</script>
随便写了个错误的写法,来看输出
Uncaught SyntaxError: Function statements require a function name
You will see me!!!
前言
为降低脚本执行的错误率,更快定位前端异常的情况,很多前端团队都有自己的前端监控系统,市面上也有一些颇受好评的第三方异常监控系统,如 sentry. 本文结合自己的一些见解和实践,围绕下面几个大的方向去讨论。
异常捕获/处理
前端的常见的异常主要分为两类,JS 脚本异常及网络相关异常
常见异常示例
输出
下面讨论下针对对以上异常的捕获方式
1. try-catch
最熟悉不过的就是 try-catch,还是用上面的例子
输出
另外 try-catch 无法捕获 JS 语法错误,举个🌰
随便写了个错误的写法,来看输出
可以看到第一个任务中的异常没有被捕获到,从而后面的代码被阻断了 这种语法错误类型的异常,只要在项目中像使用类似 ESlint 这种代码检测工具,一般都能提前发现并避免,否则就需要好好检查自己项目的开发流程及测试流程了
同时 try-catch 对异步的异常也无法进行捕获,如下
输出
try-catch 我们常用于 JS 运行时可预知可能会出现错误的地方捕获异常,通常是针对特定的业务场景,针对性比较强,且对代码的执行效率有所影响,因此在一个项目中,并不会大量使用。
window.onerror
window.onerror 是一个全局异常捕获事件,但能够捕获到未被捕获的脚本异常 注意,是未被捕获 window.onerror 可以捕获到语法错误(在 Chrome 浏览器测试,版本 80.0.3987.132(正式版本) 64 位,自己测试是可以的,但是在网上看到一些文章说不可以,具体自行测试吧),也可以捕获到异步产生的异常,同时在其回调函数上,有充足的异常信息,可以看出,它比 try-catch 功能要更强大一些
输出
另外 window.onerror 无法捕获到 http 异常,比如图片加载失败,示例
控制台只有一条异常输出,这是浏览器自己输出的,我们并没有捕获到
从上面可以看到,即使我们已经通过 window.onerror 方法捕获了异常,但是浏览器还是会输出自己的异常打印信息(浏览器默认的,并非我们手动打印的信息),此时我们可以通过 return true 来阻止其在控制台打印额外的信息,默认返回值是 false
更多见 MDN 上详细说明 GlobalEventHandlers.onerror
window.addEventListener
对于一些 http 请求相关的异常,我们可以通过 addEventListener 给 window 注册 error 事件
可以看到图片请求失败的异常被捕获到了 注意 addEventListener 的第三个参数,它的参数名为 useCapture,我把它设置为 true(默认为 false),因为 http 相关异常不会事件冒泡,因此必须在其捕获阶段将其捕获,但是 EventListener 捕获不了 http 的状态码,也就是 404, 500 这些都拿不到,还是得配合后端接口日志来进行排查
注意:
因为 window.onerror 有详细的调用栈信息,更适合去捕获一些脚本方面的异常,而 addEventListener error 事件,更适合捕获一些网络加载相关的异常,因此可以加以区分
Promise catch
输出
对于 Promise 抛出的异常,之前使用的 try-catch, window.onerror 和 addEventListener error 都无法捕获,只能够使用 Promise 自己的 catch 方法以及全局提供的 unhandledrejection 来捕获。
crossorigin
对于一些跨域的外部脚本,由于同源策略限制,如果其执行出现异常的话,会直接报 Script Error,没有其他任何信息。
解决思路无非两种
方式 1 简单粗暴解决了问题,但是也带来更大的问题,无法利用好文件缓存和 CDN 的优势
接下来主要说说方式2 先来看看具体问题,这里我引用了一个外部脚本,这个脚本对其他库有依赖,我没有引就会报错
可以看到 window.onerror 和 addEventListener error 事件都只能拿到一个 Script Error 错误信息
跨域资源共享,可以通过给 script 加 crossorigin 属性,增加 crossorigin 属性后,浏览器将自动在请求头中添加一个 Origin 字段,发起一个 跨来源资源共享 请求。Origin 向服务端表明了请求来源,服务端将根据来源判断是否正常响应,若为合法请求,后端在响应头上设置
Access-Control-Allow-Origin
,否则依然会报跨域异常这时再强制刷新一下(注意是强制,因为有可能会读缓存从而继续报错)
这时可以看到,已经有完整的报错信息了。
iframe 异常
我们来看下用之前的异常捕获能否捕获到 iframe 中的异常
可以看到,控制台有报错,但只有一条浏览器自己的报错信息,并没有触发我的事件声明
那如何监听到 iframe 里面的错误呢? 对于是同源的 iframe ,我们可以通过 window.frames 这个对象拿到页面上的 iframe 对象,它是一个类数组对象,我们可以这样:
成功捕获到异常
但是对于非同源的,这种方法是行不通的,需要使用到一些额外的通信手段了,因为对 iframe 不是很熟悉,所以就不详情展开了,具体可以参考文章 跨域,你需要知道的全在这里
另外还有一些框架相关的异常捕获 api,像 react 的 Error Boundaries,这里不一一展开说明,自行查看官方文档即可。
异常上报
前面介绍了这么多异常的类型和捕获的方法,那前端拿到了异常信息之后,要怎么上报呢?常见的有两种方案
第1种就是封装个接口,像平时发送请求一样发送数据,但是鉴于 ajax 本身也可能会产生异常,所以大家还是第2种方式居多
方式2 利用的是 img 标签的 src 属性,它可以发起一个单向 get 请求,因为上报日志我们并不需要获取响应数据,所以单向即可满足要求(也许会节省带网络带宽什么的,但我没有求证),并且 src 还具备跨域能力,非常方便,业界很多大厂像淘宝,京东都是使用 img src 来发送请求
参考: 前端监控体系怎么搭建? 脚本错误量极致优化-监控上报与Script error