摘要:
异常处理从入门到精通!
Fundebug经授权转载,版权归原作者所有。
前端一直是距离用户最近的一层,随着产品的日益完善,我们会更加注重用户体验,而前端异常却如鲠在喉,甚是烦人。
一、为什么要处理异常?
异常是不可控的,会影响最终的呈现结果,但是我们有充分的理由去做这样的事情。
增强用户体验;
远程定位问题;
未雨绸缪,及早发现问题;
无法复线问题,尤其是移动端,机型,系统都是问题;
完善的前端方案,前端监控系统;
对于
JS
而言,我们面对的仅仅只是异常,异常的出现不会直接导致
JS
引擎崩溃,最多只会使当前执行的任务终止。
二、需要处理哪些异常?
对于前端来说,我们可做的异常捕获还真不少。总结一下,大概如下:
JS
语法错误、代码异常
AJAX
请求异常
静态资源加载异常
Promise
异常
Iframe
异常
跨域 Script error
崩溃和卡顿
下面我会针对每种具体情况来说明如何处理这些异常。
三、Try-Catch 的误区
try-catch
只能捕获到同步的运行时错误,对语法和异步错误却无能为力,捕获不到。
1. 同步运行时错误:
try { let name = 'jartto' ; console .log(nam); } catch (e) { console .log('捕获到异常:' ,e); }
输出:
捕获到异常: ReferenceError : nam is not defined at :3 :15
2. 不能捕获到语法错误,我们修改一下代码,删掉一个单引号:
try
{ let name = 'jartto; console.log(nam); } catch(e) { console.log(' 捕获到异常:',e); }
输出:
Uncaught SyntaxError : Invalid or unexpected token
不过语法错误在我们开发阶段就可以看到,应该不会顺利上到线上环境。
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});} Jartto;
可以看到,我们捕获到了异常:
2. 再试试语法错误呢?
window .onerror = function (message, source, lineno, colno, error ) {console .log('捕获到异常:' ,{message, source, lineno, colno, error});} let name = 'Jartto
控制台打印出了这样的异常:
Uncaught SyntaxError : Invalid or unexpected token
什么,竟然没有捕获到语法错误?
3. 怀着忐忑的心,我们最后来试试异步运行时错误:
window
.onerror = function (message, source, lineno, colno, error ) { console .log('捕获到异常:' ,{message, source, lineno, colno, error}); } setTimeout(() => { Jartto; });
控制台输出了:
捕获到异常: {message : "Uncaught ReferenceError: Jartto is not defined" , source : "http://127.0.0.1:8001/" , lineno : 36 , colno : 5 , error : ReferenceError : Jartto is not defined at setTimeout (http:
4. 接着,我们试试网络请求异常的情况:
<script > window .onerror = function (message, source, lineno, colno, error ) { console .log('捕获到异常:' ,{message, source, lineno, colno, error}); return true ; } script ><img src ="./jartto.png" >
我们发现,不论是静态资源异常,或者接口异常,错误都无法捕获到。
补充一点:
window.onerror
函数只有在返回
true
的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示
Uncaught Error: xxxxx
window .onerror = function (message, source, lineno, colno, error ) { console .log('捕获到异常:' ,{message, source, lineno, colno, error}); return true
; } setTimeout(() => { Jartto; });
控制台就不会再有这样的错误了:
Uncaught ReferenceError : Jartto is not defined at setTimeout ((index):36 )
需要注意:
onerror
最好写在所有
JS
脚本的前面,否则有可能捕获不到错误;
onerror
无法捕获语法错误;
到这里基本就清晰了:在实际的使用过程中,
onerror
主要是来捕获预料之外的错误,而
try-catch
则是用来在可预见情况下监控特定的错误,两者结合使用更加高效。
问题又来了,捕获不到静态资源加载异常怎么办?
五、window.addEventListener
当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个
Event
接口的
error
事件,并执行该元素上的
onerror()
处理函数。这些
error
事件不会向上冒泡到
window
,不过(至少在
Firefox
中)能被单一的
window.addEventListener
捕获。
<scritp > window.addEventListener('error', (error) => { console.log('捕获到异常:', error); }, true) script ><img src ="./jartto.png" >
控制台输出:
由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断
HTTP
的状态是
404
还是其他比如
500
等等,所以还需要配合服务端日志才进行排查分析才可以。
需要注意:
六、Promise Catch
在
promise
中使用
catch
可以非常方便的捕获到异步
error
,这个很简单。
没有写
catch
的
Promise
中抛出的错误无法被
onerror
或
try-catch
捕获到,所以我们务必要在
Promise
中不要忘记写
catch
处理抛出的异常。
解决方案: 为了防止有漏掉的
Promise
异常,建议在全局增加一个对
unhandledrejection
的监听,用来全局监听
Uncaught Promise Error
。使用方式:
window .addEventListener("unhandledrejection" , function (e ) { console .log(e); });
我们继续来尝试一下:
window .addEventListener("unhandledrejection" , function (e ) { e.preventDefault() console .log('捕获到异常:' , e); return true ; }); Promise
.reject('promise error' );
可以看到如下输出:
那如果对
Promise
不进行
catch
呢?
window .addEventListener("unhandledrejection" , function (e ) { e.preventDefault() console .log('捕获到异常:' , e); return true ; }); new Promise ((resolve, reject ) => { reject('jartto: promise error' ); });
嗯,事实证明,也是会被正常捕获到的。
所以,正如我们上面所说,为了防止有漏掉的
Promise
异常,建议在全局增加一个对
unhandledrejection
的监听,用来全局监听
Uncaught Promise Error
。
补充一点:如果去掉控制台的异常显示,需要加上:
七、VUE errorHandler
Vue.config.errorHandler = (err, vm, info ) => { console .error('通过vue errorHandler捕获的错误' ); console .error(err); console .error(vm); console .error(info); }
八、React 异常捕获
React 16
提供了一个内置函数
componentDidCatch
,使用它可以非常简单的获取到
react
下的错误信息
componentDidCatch(error, info) { console .log(error, info); }
除此之外,我们可以了解一下:
error boundary
UI
的某部分引起的
JS
错误不应该破坏整个程序,为了帮
React
的使用者解决这个问题,
React 16
介绍了一种关于错误边界(
error boundary
)的新观念。
需要注意的是: error boundaries 并不会捕捉下面这些错误。
我们来举一个小例子,在下面这个
componentDIdCatch(error,info)
里的类会变成一个
error boundary
:
class ErrorBoundary extends React .Component { constructor (props) { super (props); this .state = { hasError : false }; } componentDidCatch(error, info) { this .setState({ hasError : true }); logErrorToMyService(error, info); } render() { if (this
.state.hasError) { return <h1 > Something went wrong.h1 > ; } return this .props.children; } }
然后我们像使用普通组件那样使用它:
<ErrorBoundary > <MyWidget /> ErrorBoundary >
componentDidCatch()
方法像
JS
的
catch{}
模块一样工作,但是对于组件,只有
class
类型的组件(
class component
)可以成为一个
error boundaries
。
实际上,大多数情况下我们可以在整个程序中定义一个
error boundary
组件,之后就可以一直使用它了!
九、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}); return true ; }; script >
十、Script error
一般情况,如果出现
Script error
这样的错误,基本上可以确定是出现了跨域问题。这时候,是不会有其他太多辅助信息的,但是解决思路无非如下:
跨源资源共享机制(
CORS
):我们为
script
标签添加
crossOrigin
属性。
<script src ="http://jartto.wang/main.js" crossorigin >script >
或者动态去添加
js
脚本:
const script = document .createElement('script' );script.crossOrigin = 'anonymous' ; script.src = url; document .body.appendChild(script);
特别注意,服务器端需要设置:Access-Control-Allow-Origin
此外,我们也可以试试这个-解决 Script Error 的另类思路:
const originAddEventListener = EventTarget.prototype.addEventListener;EventTarget.prototype.addEventListener = function (type, listener, options ) { const wrappedListener = function (...args ) { try { return listener.apply(this , args); } catch