记一次寻找bug的过程….
忙了一整天,bug都解决的差不多了。看看时间,已经是下午五点半了,寻思还有半个小时做点什么?
突然想起前两天接到的任务:要求优化前端报错异常捕获。
刚接到这个任务,大脑第一反应:so easy! window.onerror不就可以了?
当我把这个方案提出时,领导接着说,这样确实能捕获,但我们更多是需要对出错后的降级处理~(及时确认需求的重要性)
降级处理?
想想也不难,不就是显示出一个报错页面嘛,当window.onerror检测到错误时,直接重定向到一个错误UI页面不就ok!今天六点准时下班应该是稳了!想到这里,不禁开始飘飘然!
然而,领导再次提出,这样做对用户不太友好,我们只需要将报错的组件页面进行降级处理即可,不能影响其他tab栏或者页面的访问。
这……
经过google的一顿搜索,发现这种问题不少。给出的方案有如下几种:
- try catch
- Promise.catch
- window.onerror
- window.addEventListener(‘error’,cb)
- …
发现方案确实一大堆,但真正符合项目需求的基本上没有~~
首先项目中出错的地方很多,项目中写大量的try catch/Promise.catch显然不太友好,直接放弃。至于window.onerror/window.addEventListener(‘error’,cb) 这两个可以适当考虑一下。
通过进一步的调研,发现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});
}
复制代码
通过调研发现,onerror 无法捕获语法错误;以及是静态资源异常,或者接口异常,错误都无法捕获到。这也太坑爹了….
要知道在实际项目开发中,有大量的对对象链式操作,比如:
跟后端协商的数据类型:data:{a:{b:c:{d}}}
当我们要取数据 d 时是通过data.a.b.c.d
这样来获取的,只要其中一环出问题(后端接口返回数据不对或者接口被攻击失效等),在线上就会白屏,这种情况非常常见。
单凭无法捕获语法错误,直接pass掉。
接下来再去看看window.addEventListener(‘error’,cb),发现通过对error事件的监听,可以除了监听不到http请求的错误除外,其他资源加载失败以及语法错误都能监听到。
咦,既然这样,那这个问题不是就解决了?我们通过http拦截器监听http请求的错误,再通过window.addEventListener(‘error’,cb) 监听其他错误不就好了!想想就觉得兴奋,感觉差不多快下班了!
然而,当我开始动手时,想起了最开始的需求,要求对错误组件UI进行降级处理…,这怎么实现….又陷入了沉思。
降级处理?嗯?React官方不是有降级处理的说明!
灵感一现,赶紧去查看文档,果然,React nb,但是却发现
错误边界无法捕获以下场景中产生的错误:
- 事件处理(了解更多)
- 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
- 服务端渲染
- 它自身抛出来的错误(并非它的子组件)
这…..
事件处理和异步代码我们可以通过window.addEventListener(‘error’,cb) 来捕获 ,至于服务端渲染,目前没有用到,不考虑,它自身抛出的错误?直接复制你官方的组件,我就不信还能出错?叉会腰!
好了,现在方案已定,开始撸代码!
import React from 'react';
export interface IErrorBoundaryProps {
children: object;
}
export interface IErrorBoundaryState {
hasError: boolean;
}
export default class ErrorBoundary extends React.Component<
IErrorBoundaryProps,
IErrorBoundaryState
> {
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
console.log(error);
return { hasError: true };
}
constructor(props) {
super(props);
this.state = { hasError: false };
}
// componentDidCatch(error, errorInfo) {
// // 你同样可以将错误日志打印出来
// console.log(error);
// console.log(errorInfo);
// }
render() {
console.log(this.state.hasError, 'this.state.hasError');
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
复制代码
将ErrorBoundary套在路由组件中,不就可以对路由页面进行降级处理啦!
<Route
key={item.path}
path={item.path}
render={(props) => (
<ErrorBoundary>
<item.component {...props} routes={item.routes} />
</ErrorBoundary>
)}
/>;
复制代码
好了!测试一下准备下班!
随便在组件中写个错误代码,看看会不会触发。然而:
咦,这怎么回事?确实达到了预期的效果,不影响其他功能的使用,但是这UI,怎么和我想象的不一样?
苦思冥想,问题出在哪里?
莫非是webpack配置中 overlay: { warnings: false, errors: true }
影响了?
不对,如果是这两个属性触发的话,会导致全屏显示,根本不会是这样只在路由中显示。那问题出在哪里呢?
而且也没有触发window.addEventListener(‘error’,cb),说明并没有冒泡到window中,那到底是在哪一环被截取了呢?
通过仔细检查,我发现触发异常的这个组件,在上面的某一层中套了ErrorBoundary,最开始并没有引起我的警觉,后来仔细查看,发现
莫非是这玩意引起的?
去掉该组件,自定义的错误UI显示出来了!卧槽,大意了啊!
然而此时天已黑,完~~~
总结:通过ErrorBoundary捕获渲染过程中的异常,window.addEventListener(‘error’,cb) 捕获资源或者其他错误的异常,http拦截器捕获请求异常,让整个项目的异常可控。
后续有更好的方案和思路再更新~