谈谈前端异常捕获
作为一个前端开发人员,每次看到浏览器控制台信息里面红通通的报错信息是不是都很紧张......不要怕,下面我们就来讨论一下前端的异常捕获。
异常捕获,相对于其他知识点可能没那么被重视,特别是对于前端程序员。但不得不说,这又是一个不得不面对的知识点。
为什么要捕获异常
首先,我们为什么要进行异常捕获和上报呢?
正所谓百密一疏,用程序员的话来说就是:天下不存在没有bug的程序(不接受反驳 🤐 )。即使经过各种测试,还是会存在十分隐蔽的bug,这种不可预见的问题只有通过完善的监控机制才能有效的减少其带来的损失。因此,对于最接近用户的前端来说,为了能远程定位问题、增强用户体验,异常的捕获和上报至关重要。
目前市面上已经有一些非常完善的前端监控系统存在,如Fundebug、Bugsnag等,虽然这些已经能做到帮我们实时监控生产环境的异常,但是如果我们不了解异常是如何产生的,又怎么能得心应手的定位并处理问题呢?
对于JS而言,我们面对的仅仅只是异常,异常的出现不会直接导致JS引擎崩溃,最多只是终止当前代码的执行。下面来解释一下这句话:
<script>
error // 没定义过的变量,此处会报错
console.log('永远不会执行');
</script>
<script>
console.log('我继续执行')
</script>
异常捕获分类
这里我做了一个脑图归纳一些前端异常,不一定对,只是有个大概印象。如下:
下面就针对不同异常的捕获一一分析:
try...catch 的误区
try...catch
只能捕获到同步的运行时错误,对于语法和异步错误无能为力,捕获不到。
1.同步运行时错误
try {
let name = 'Jack';
console.log(nam);
} catch(e) {
console.log('捕获到异常:',e);
}
输出:
捕获到异常: ReferenceError: nam is not defined
at <anonymous>:3:15
2.不能捕获语法错误,我们修改一个代码,删掉一个单引号
try {
let name = 'Jack;
console.log(nam);
} catch(e) {
console.log('捕获到异常:',e);
}
输出:
Uncaught SyntaxError: Invalid or unexpected token
语法错误
SyntaxError
,不管是window.error
还是try...catch
都没法捕获异常。但是不用担心,在你写好代码按下保存那一刻,编译器会帮你检查是否有语法错误,如果有错误有会有个很明显的红红的波浪线,把鼠标移上去就能看到报错信息。因此,面对SyntaxError
语法错误,一定要小心小心再小心
3.异步错误
try {
setTimeout(() => {
undefined.map(v => v);
}, 1000)
} catch(e) {
console.log('捕获到异常:',e);
}
输出:
Uncaught TypeError: Cannot read property 'map' of undefined
at setTimeout (<anonymous>:3:11)
可以看到,并没有捕获到异常。
window.onerror 不是万能的
当JS运行时错误发生时,window
会触发一个 ErrorEvent
接口的 error
事件,并执行 window.onerror()
。
/**
* @param {String} message 错误信息
* @param {String} source 出错文件
* @param {Number} lineno 行号
* @param {Number} colno 列号
* @param {Object} error Error对象(对象)
*/
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
1.同步运行时错误
window.onerror = function(message, source, lineno, colno, error) {
// message:错误信息(字符串)。
// source:发生错误的脚本URL(字符串)
// lineno:发生错误的行号(数字)
// colno:发生错误的列号(数字)
// error:Error对象(对象)
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
UndefVar;
可以看到,我们捕获了异常:
2.语法错误
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
let name = 'Jack; // 少个单引号
控制台打印出了这样的异常:
Uncaught SyntaxError: Invalid or unexpected token
可以看出,并没有捕获到异常。
3.异步运行时错误
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
setTimeout(() => {
UndefVar;
});
同样看到,我们捕获了异常:
4.网络请求的异常
<script>
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
return true;
}
</script>
<img src="./xxx.png">
我们发现,不论是静态资源异常,或者接口异常,错误都无法捕获到。
注意:
window.onerror
函数只有在返回true
的时候,异常才不会向上抛出(浏览器接收后报红),否则即使是知道异常的发生控制台还是会显示Uncaught Error: xxxxx
window.onerror
最好写在所有JS脚本的前面,否则有可能捕获不到错误
window.onerror
无法捕获语法错误
那么问题来了,如何捕获静态资源加载错误呢?
window.addEventListener
当一项资源(如图片和脚本加载失败),加载资源的元素会触发一个Event
接口的error
事件,并执行该元素上的onerror
处理函数。这些error
事件不会向上冒泡到window
, 不过(至少在 Chrome
中)能被单一的window.addEventListener
捕获。
<script>
window.addEventListener('error', (error) => {
console.log('捕获到异常:', error);
}, true)
</script>
<img src="./xxxx.png">
可以捕获异常:
由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP
的状态是 404
还是其他比如 500
等等,所以还需要配合服务端日志才进行排查分析才可以。
注意:
- 不同浏览器下返回的
error
对象可能不同,需要注意兼容处理。- 需要注意避免
window.addEventListener
重复监听。
到此为止,我们学到了:在开发的过程中,对于容易出错的地方,可以使用try{}catch(){}
来进行错误的捕获,做好兜底处理,避免页面挂掉。而对于全局的错误捕获,在现代浏览器中,我倾向于只使用使用window.addEventListener('error')
,window.addEventListener('unhandledrejection')
就行了。如果需要考虑兼容性,需要加上window.onerror
,三者同时使用,window.addEventListener('error')
专门用来捕获资源加载错误。
Promise Catch
我们知道,在 promise
中使用 catch
可以非常方便的捕获到异步 error
。
没有写catch
的promise
中抛出的错误无法被onerror
或try...catch
捕获到,所以务必在promise
中写catch
做异常处理。
有没有一个全局捕获promise
的异常呢?答案是有的。 Uncaught Promise Error
就能做到全局监听,使用方式:
window.addEventListener("unhandledrejection", function(e){
// e.preventDefault(); // 阻止异常向上抛出
console.log('捕获到异常:', e);
});
Promise.reject('promise error');
同样可以捕获错误:
所以,正如我们上面所说,为了防止有漏掉的 promise
异常,建议在全局增加一个对 unhandledrejection
的监听,用来全局监听 Uncaught Promise Error
。
iframe 异常
对于 iframe
的异常捕获,我们还得借力 window.onerror
:
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
下面一个简单的例子:
<iframe src="./iframe.html" frameborder="0"></iframe>
<script>
window.frames[0].onerror = function (message, source, lineno, colno, error) {
console.log('捕获到 iframe 异常:', {message, source, lineno, colno, error});
};
</script>
Script error
在进行错误捕获的过程中,很多时候并不能拿到完整的错误信息,得到的仅仅是一个"Script Error"
。
产生原因
由于12年前这篇文章里提到的安全问题:blog.jeremiahgrossman.com/2006/12/i-k…
当加载自不同域的脚本中发生语法错误时,为避免信息泄露,语法错误的细节将不会报告,而是使用简单的"Script error."
代替。
一般而言,页面的JS文件都是放在CDN的,和页面自身的URL产生了跨域问题,所以引起了"Script Error"
。
解决办法
一般情况,如果出现 Script error
这样的错误,基本上可以确定是跨域问题。这时候,是不会有其他太多辅助信息的,但是解决思路无非如下:
跨源资源共享机制( CORS
):我们为 script
标签添加 crossOrigin
属性。
<script src="http://jartto.wang/main.js" crossorigin></script>
崩溃和卡顿
卡顿也就是网页暂时响应比较慢, JS可能无法及时执行。但崩溃就不一样了,网页都崩溃了,JS都不运行了,还有什么办法可以监控网页的崩溃,并将网页崩溃上报呢?
1.利用 window
对象的 load
和 beforeunload
事件实现了网页崩溃的监控。
不错的文章,推荐阅读:Logging Information on Browser Crashes。
window.addEventListener('load', function () {
sessionStorage.setItem('good_exit', 'pending');
setInterval(function () {
sessionStorage.setItem('time_before_crash', new Date().toString());
}, 1000);
});
window.addEventListener('beforeunload', function () {
sessionStorage.setItem('good_exit', 'true');
});
if(sessionStorage.getItem('good_exit') &&
sessionStorage.getItem('good_exit') !== 'true') {
/*
insert crash logging code here
*/
alert('Hey, welcome back from your crash, looks like you crashed on: ' + sessionStorage.getItem('time_before_crash'));
}
2.基于以下原因,我们可以使用 Service Worker
来实现网页崩溃的监控:
Service Worker
有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker
一般情况下不会崩溃Service Worker
生命周期一般要比网页还要长,可以用来监控网页的状态- 网页可以通过
navigator.serviceWorker.controller.postMessage API
向掌管自己的SW
发送消息
VUE errorHandler
在Vue中,异常可能被Vue自身给try...catch
了,不会传到window.onerror
事件触发。不过不用担心,Vue提供了特有的异常捕获,比如Vux2.x中我们可以这样用:
Vue.config.errorHandler = function (err, vm, info) {
let {
message, // 异常信息
name, // 异常名称
script, // 异常脚本url
line, // 异常行号
column, // 异常列号
stack // 异常堆栈信息
} = err;
// vm为抛出异常的 Vue 实例
// info为 Vue 特定的错误信息,比如错误所在的生命周期钩子
}
React 异常捕获
在React,可以使用ErrorBoundary
组件包括业务组件的方式进行异常捕获,配合React 16.0+
新出的componentDidCatch API
,可以实现统一的异常捕获和日志上报。
我们来举一个小例子,在下面这个 componentDIdCatch(error,info)
里的类会变成一个 error boundary
:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
然后我们像使用普通组件那样使用它:
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
componentDidCatch()
方法像JS的 catch{}
模块一样工作,但是对于组件,只有 class
类型的组件(class component
)可以成为一个 error boundaries
。
实际上,大多数情况下我们可以在整个程序中定义一个 error boundary
组件,之后就可以一直使用它了!
需要注意的是:
error boundaries
并不会捕捉下面这些错误:
- 事件处理器
- 异步代码
- 服务端的渲染代码
- 在
error boundaries
区域内的错误
总结
- 可疑区域增加
try...catch
- 全局监控JS异常:
window.onerror
- 全局监控静态资源异常:
window.addEventListener
- 全局捕获没有
catch
的promise
异常:unhandledrejection
- iframe 异常:
window.error
VUE errorHandler
和React componentDidCatch
- 监控网页崩溃:
window
对象的load
和beforeunload
Script Error
跨域crossOrigin
解决
参考: