在React 15.x版本及之前版本中,组件内的UI异常将中断组件内部状态,导致下一次渲染时触发隐藏异常。React并未提供友好的异常捕获和处理方式,一旦发生异常,应用将不能很好的运行。而React 16版本有所改进。本文主旨就是探寻React异常捕获的现状,问题及解决方案。
欢迎访问我的个人博客
前言
我们期望的是在UI中发生的一些异常,即组件内异常(指React 组件内发生的异常,包括组件渲染异常,组件生命周期方法异常等),不会中断整个应用,可以以比较友好的方式处理异常,上报异常。在React 16版本以前是比较麻烦的,在React 16中提出了解决方案,将从异常边界(Error Boundaries)开始介绍。
异常边界
所谓异常边界,即是标记当前内部发生的异常能够被捕获的区域范围,在此边界内的Javascript异常可以被捕获到,不会中断应用,这是React 16中提供的一种处理组件内异常的思路。具体实现而言,React提供一种异常边界组件,以捕获并打印子组件树中的Javascript异常,同时显示一个异常替补UI。
组件内异常
组件内异常,也就是异常边界组件能够捕获的异常,主要包括:
- 渲染过程中异常;
- 生命周期方法中的异常;
- 子组件树中各组件的constructor构造函数中异常。
其他异常
当然,异常边界组件依然存在一些无法捕获的异常,主要是异步及服务端触发异常:
- 事件处理器中的异常;
- 异步任务异常,如setTiemout,ajax请求异常等;
- 服务端渲染异常;
- 异常边界组件自身内的异常;
异常边界组件
前面提到异常边界组件只能捕获其子组件树发生的异常,不能捕获自身抛出的异常,所以有必要注意两点:
- 不能将现有组件改造为边界组件,否则无法捕获现有组件异常;
- 不能在边界组件内涉及业务逻辑,否则这里的业务逻辑异常无法捕获;
很显然,最终的异常边界组件必然是不涉及业务逻辑的独立中间组件。
那么一个异常边界组件如何捕获其子组件树异常呢?很简单,首先它也是一个React组件,然后添加ComponentDidCatch
生命周期方法。
实例
创建一个React组件,然后添加ComponentDidCatch
生命周期方法:
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 Meet Some Errors.
;
}
return this.props.children;
}
}
接下来可以像使用普通React组件一样使用该组件:
ComponentDidCatch
这是一个新的生命周期方法,使用它可以捕获子组件异常,其原理类似于Javascript异常捕获器try, catch
。
ComponentDidCatch(error, info)
error:应用抛出的异常;
info:异常信息,包含
ComponentStack
属性对应异常过程中冒泡的组件栈;
判断组件是否添加componentDidCatch
生命周期方法,添加了,则调用包含异常处理的更新渲染组件方法:
if (inst.componentDidCatch) {
this._updateRenderedComponentWithErrorHandling(
transaction,
unmaskedContext,
);
} else {
this._updateRenderedComponent(transaction, unmaskedContext);
}
在_updateRenderedComponentWithErrorHandling
里面使用try, catch
捕获异常:
/**
* Call the component's `render` method and update the DOM accordingly.
*
* @param {ReactReconcileTransaction} transaction
* @internal
*/
_updateRenderedComponentWithErrorHandling: function(transaction, context) {
var checkpoint = transaction.checkpoint();
try {
this._updateRenderedComponent(transaction, context);
} catch (e) {
// Roll back to checkpoint, handle error (which may add items to the transaction),
// and take a new checkpoint
transaction.rollback(checkpoint);
this._instance.componentDidCatch(e);
// Try again - we've informed the component about the error, so they can render an error message this time.
// If this throws again, the error will bubble up (and can be caught by a higher error boundary).
this._updateRenderedComponent(transaction, context);
}
},
具体源码见Github
unstable_handleError
其实异常边界组件并不是突然出现在React中,在15.x版本中已经有测试React 15 ErrorBoundaries,源码见Github。可以看见在源码中已经存在异常边界组件概念,但是尚不稳定,不推荐使用,从生命周期方法名也可以看出来:unstable_handleError
,这也正是ComponentDidCatch
的前身。
业务项目中的异常边界
前面提到的都是异常边界组件技术上可以捕获内部子组件异常,对于业务实际项目而言,还有需要思考的地方:
- 异常边界组件的范围或粒度:是使用异常边界组件包裹应用根组件(粗粒度),还是只包裹独立模块入口组件(细粒度);
- 粗粒度使用异常边界组件是暴力处理异常,任何异常都将展示异常替补UI,完全中断了用户使用,但是确实能方便的捕获内部所有异常;
- 细粒度使用异常边界组件就以更友好的方式处理异常,局部异常只会中断该模块的使用,应用其他部分依然正常不受影响,但是通常应用中模块数量是很多的,而且具体模块划分到哪一程度也需要开发者考量,比较细致;
点此传送查看实例
组件外异常
React 16提供的异常边界组件并不能捕获应用中的所有异常,而且React 16以后,所有未被异常边界捕获的异常都将导致React卸载整个应用组件树,所以通常需要通过一些其他前端异常处理方式进行异常捕获,处理和上报等,最常见的有两种方式:
window.onerror
捕获全局Javascript异常;// 在应用入口组件内调用异常捕获
componentWillMount: function () {
this.startErrorLog();
}
startErrorLog:function() {
window.onerror = (message, file, line, column, errorObject) => {
column = column || (window.event && window.event.errorCharacter);
const stack = errorObject ? errorObject.stack : null;
// trying to get stack from IE
if (!stack) {
var stack = [];
var f = arguments.callee.caller;
while (f) {
stack.push(f.name);
f = f.caller;
}
errorObject['stack'] = stack;
}
const data = {
message:message,
file:file,
line:line,
column:column,
errorStack:stack,
};
// here I make a call to the server to log the error
reportError(data);
// the error can still be triggered as usual, we just wanted to know what's happening on the client side
// if return true, this error will not be console log out
return false;
}
}try, catch
手动定位包裹易出现异常的逻辑代码;class Home extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
}
handleClick = () => {
try {
// Do something that could throw
} catch (error) {
this.setState({ error });
}
}
render() {
if (this.state.error) {
returnMeet Some Errors.
}
returnClick Me
}
}
常见的开源异常捕获,上报库,如sentry,badjs等都是利用这些方式提供常见的Javascript执行异常。
参考
- Error Boundaries
- Try Catch in Component
- Handle React Errors in v15