前言
一名初级前端程序员,面对异常往往是佛当杀佛,神挡杀神,最后很累,脑子却始终没有一个清晰的概念,这样是不行的。
今天我整理一下,前端到底有多少种异常,以及它们的处理方式最佳实践。
异常分类
前端能避免的异常,就必须百分百避免:
-
JS语法错误、书写代码异常
-
Promise异常
-
其他崩溃和卡顿
前端无法绝对避免的异常:
前端无法绝对避免的异常要么来自于服务器,要么来自于网络连接。比如:
-
静态资源(js、css、图片、svg、字体、其他未列举的资源)加载异常
-
Iframe加载页面异常
-
跨域错误
-
AJAX异常
一、JS语法错误、运行时异常
先弄清2个概念,JS语法错误和运行时错误。
-
语法错误就是书写错误,比如少个括号,少个引号,在代码运行前,JS引擎进行代码分析的时候就会发现。即便异常出现在函数里,即便需要靠触发运行,也一样会被提前发现,更何况脚手架里大多自备语法分析工具,所以并不担心语法错误。
-
运行时错误就是JS引擎对语法的分析已经通过,JS引擎开始运行,运行到某处时报错,比如变量未定义(可能是多敲了一个字符)、视图访问undefined的属性,当然也会报异常。依靠触发的函数,里面有异常的话,直到它被触发,才会报错。前端程序员应当关心运行时错误。
console.log('log')
a.b.c = '1' // 这叫运行时错误,会说a未定义,如果单只有这句在,没有下面那句,可以打印log
a.b.c = '1 // 这叫语法错误,少个引号,有这句在,就不会打印log
这2种异常在开发阶段和测试阶段大多能发现,尤其是语法错误。发现之后修复就好了。但在一些极限场景下,依然可能报出一些异常,这是为什么?
-
后端接口的数据有错误。所以后端数据不可信,所以该做的判断一定不能少。
-
模块js文件加载失败。
-
打包过程中优化错误导致。
-
npm包升级之后带来新错误,没有及时发现。
怎么解决?
-
维护服务器和线路稳定。
-
不要在发包的前几天升级npm,起码应当有一个月来检验新的npm包是否有问题。
-
应当捕获错误,并简明扼要的提示。
大公司对于前端异常都有完善的捕获和上报系统,这个系统说起来太复杂,我也没见过,我只说说小项目的捕获和提示。
讨论try...catch...
它只能捕获同步代码的运行时异常,不能捕获异步的运行时异常。在如今的项目中,异步执行的代码比同步执行的代码要多得多,非要加try...catch...的话,会遍地都是try...catch...,会让开发者和维护者彻底疯掉。
所以try...catch...的使用场合:
-
尽量避免使用try...catch...。
-
只在最小范围使用try...catch...。
-
只在能预见的运行时错误场合使用try...catch...,比如我只关心
a.b.c.d
是不是等于3,如果不等于,哪怕a.b.c.d
有运行时错误,我也不关心,这种场景,可以用try...catch...。但必须注意,依然要尽量避免使用try...catch...,这个案例最好还是依次判断一下(a && a.b && a.b.c && a.b.c.d && a.b.c.d === 3
),更何况,ECMAScript 2020引入了可选链操作符(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/可选链)。
讨论window.onerror
window.onerror的捕获特点是:
-
只能捕获到运行时错误,原因上面说过,语法错误就根本通不过JS的代码分析,也不可能运行。
-
可以捕获同步代码和异步代码的异常。
-
主要是来捕获预料之外的错误。
-
捕获不到静态资源加载异常。
-
尽量写在项目所有代码的第一行。
讨论window.addEventListener('error', (error) => {})
它跟window.onerror的区别是:
-
window.onerror是标准错误捕获接口,window.addEventListener('error')不那么标准,各个浏览器的实现不统一,拿到的数据也不完整。
-
window.onError虽然标准但是不能捕获资源加载失败,window.addEventListener('error')就可以。
-
需要注意避免window.addEventListener重复监听。window.onerror不存在重复监听的现象。
使用场合:
-
重试静态资源加载。js如果发现静态资源加载失败,并不需要向用户弹提示,因为弹了提示也无法引导用户把资源加载成功。但是js至少可以重试一次加载,万一第二次加载成功了呢。
-
上报的作用更大一些,依靠这个可以知道静态资源是否偶尔失效。
讨论Promise的异常和window.addEventListener('unhandledrejection', (error) => {})
Promise的.catch回调、.then()的第二个回调,都能捕获2种错误:运行时错误和reject抛出的错误。
运行时错误未被捕获,会报Uncaught (in promise) xxxError
错误。
reject的错误未被捕获,会报Uncaught (in promise)
错误。
我们说过,能预见的运行时错误都应该当场最小范围利用try...catch...解决,而不是去捕获。不能预见的运行时错误,原则上我们希望0错误,即使有,也希望接近于0,对于不可预见的运行时错误,我们一定要使用统一的捕获:
window.addEventListener("unhandledrejection", function(e){
console.log(e);
});
unhandledrejection事件的使用场合:
-
推荐放在项目代码的第一行。
-
即便捕获到了,控制台依然会打印错误,如果不想让控制台打印错误,则在unhandledrejection事件函数末尾加上
event.preventDefault();
。 -
对于捕获的运行时错误,可以对用户弹提示,还可以上报。
-
对于reject错误,也分为2部分,一部分是可预见的错误,一部分是不可预见的错误。百分之99的Promise场合都是在Ajax,所以我们在下文Ajax部分继续讨论。
讨论Vue.config.errorHandler
官方手册:https://cn.vuejs.org/v2/api/#errorHandler
终于遇到一个封装好的错误处理机制了,它的使用就不在此多说了。
讨论iframe页面错误
iframe引用页面错误,又分为2种情况:
-
如果域名都是错的,收不到响应,这种情况下,Chrome控制台不会有任何报错,原因是iframe天生允许跨域,对方服务器有可能不归你管,对方没有任何响应不算错误。
-
如果服务器正确,且返回404,Chrome控制台会报错,但是无法捕获,原因是404是合法的状态码,Chrome控制台的报错应视为提示而不是错误。此时
window.frames[0].onload
会触发,onerror
不会触发。
变相捕获的办法只能是设window.frames[0].onload,然后看e.target.document.body里的内容是报错内容还是正常内容。
考虑到如今iframe的使用场合太少,出错的话影响大,也没有弹提示的意义,所以应将重心放在保证服务正常运行上,不考虑捕获和上报。
讨论跨域错误
就2020年的开发来讲,跨域错误百分之98是Ajax跨域错误,还有极低概率是iframe跨域错误和其他一些跨域错误。
跨域错误理论上在开发阶段就会被发现,非要说生产阶段出现跨域错误,无非有2种可能:
-
开发者不小心将正确的CORS跨域配置修改成了错误的跨域配置,还不自知。
-
前端发送了某种写错的请求,后端框架没有考虑到这种错误,也没有配置跨域,于是没有返回允许跨域的响应头。
跨域错误跟上面提到的Promise reject错误都放到最下面的Ajax错误里一并讨论。
讨论Ajax错误
Ajax错误是事实上前端最需要关注的错误。由于世上的所有Ajax解决方案都采用了Promise封装,所以开发者需要思考的问题已经很少了,Ajax错误事实上都被Ajax库封装为Promise错误,同时Ajax库已经解决了相当多的出错可能。
- 跨域
跨域被axios库归为Network Error
,axios的源码是:

你可以在axios响应拦截器的第二个回调里捕获这个错误,然后修改成网络出错
并弹出,毕竟中国人用中文。
- 断网
断网也会被axios处理为Network Error
,你也弹出网络错误
就得了。
- 服务器非200的响应
非200的响应,最出名的就是:403、404、500这三个,还可以加其他的状态码,就看你的团队是怎么约定的。
道理上说,200视为Promise的resolve,其他一律视为reject。那么有个问题就是什么情况应当归为200响应。
我认为,应当把合乎预料的反馈视为200,出乎预料的视为非200。
什么叫合乎预料?
一个List,比如用户的日记,0篇很正常,10篇也很正常,都是业务允许的,那么,服务器返回
{
list: []
}
或者
{
list: null
}
都是允许的,业务代码都需要考虑到,所以状态码都应当是200。
除此之外的任何情形,都应当视为出乎预料的,应设为非200,比如403、404、500等等。比如根本没有这个用户,你却要请求它的日记,要么返回404,要么返回500,看团队内部怎么定。
现在有一个话题,我们讨论一个场景。
有些API,后端给的响应状态码是500,响应体是这样的:
{
"msg": “某某错误某某错误”
}
前端程序员觉得这个msg给的特别棒,可以直接弹个提示,于是他就写了:
// 伪代码
Notify({
content: err.msg
})
但是有时候,前端又觉得后端给的msg写的稀烂,不适合直接弹,于是前端自己写了一个:
// 伪代码
Notify({
content: '某某错误某错'
})
这时候前端就在想:content: err.msg
是千篇一律的,我总要写一遍这个,是不是很浪费时间浪费空间?于是他想了一个主意:
凡是打算弹
content: err.msg
,就不写它,而且干脆连错误捕捉都不写。是不是省了N多的代码?
那么,谁负责弹提示呢?就让window.addEventListener("unhandledrejection", function(e){console.log(e);});
负责就好啦!
下方代码中,Notify
由于是伪代码,我为了能跑通,所以注释掉了。new Promise
是为了模拟axios返回了reject,then我只写了成功回调,意思是省略失败回调。其结果:成功打印了失败原因。
window.addEventListener("unhandledrejection", function(e){
// Notify((
// content: e.reason.msg;
// ))
console.log(e.reason.msg);
event.preventDefault();
});
new Promise((resolve, reject) => {
reject({msg: '某某错误某某错误'})
}).then(() => {
});
也就是说:凡是能统一处理的失败回调,都可以直接省掉失败回调,让window.addEventListener("unhandledrejection", fn)
去兜底就可以了。
那么,是否能在拦截器里统一处理失败回调的弹出呢?当然不行了,拦截器又不知道哪些msg写的漂亮,哪些写的稀烂。
上报
上报通常不采用Ajax,因为Ajax本身也有概率出问题,而且有概率报跨域,而且会抢正常业务的线程,所以业界一般是用new Image()动态赋值带参数的src来向服务器发请求。大公司的整套方案具体就不多说了,我也没见过,可以看看https://mp.weixin.qq.com/s/kxBObdhfOOh19rlGQ3gHWA。
网友评论