Skip to content

Commit 5c4c031

Browse files
committed
添加:React异常处理篇
1 parent fd48e05 commit 5c4c031

File tree

2 files changed

+228
-0
lines changed

2 files changed

+228
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Reactjs & React Native resources and demos.
99
6. [React DOM](https://github.com/codingplayboy/reactjs/blob/master/react_dom.md)
1010
7. [react-redux分析](https://github.com/codingplayboy/reactjs/blob/master/react-redux.md)
1111
8. [Immutable.js与React,Redux及reselect的实践](https://github.com/codingplayboy/reactjs/blob/master/immutable-redux-react.md)
12+
9. [React异常处理](https://github.com/codingplayboy/reactjs/blob/master/react-error-handle.md)
1213

1314

1415

react-error-handle.md

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# React异常处理
2+
3+
在React 0.15版本及之前版本中,组件内的UI异常将中断组件内部状态,导致下一次渲染时触发隐藏异常。React并未提供友好的异常捕获和处理方式,一旦发生异常,应用将不能很好的运行。而React 16版本有所改进。本文主旨就是探寻React异常捕获的现状,问题及解决方案。
4+
5+
## 前言
6+
7+
我们期望的是在UI中发生的一些异常,即**组件内异常**(指React 组件内发生的异常,包括组件渲染异常,组件生命周期方法异常等),不会中断整个应用,可以以比较友好的方式处理异常,上报异常。在React 16版本以前是比较麻烦的,在React 16中提出了解决方案,将从异常边界(Error Boundaries)开始介绍。
8+
9+
## 异常边界
10+
11+
所谓异常边界,即是标记当前内部发生的异常能够被捕获的区域范围,在此边界内的JavaScript异常可以被捕获到,不会中断应用,这是React 16中提供的一种处理组件内异常的思路。具体实现而言,React提供一种异常边界组件,以捕获并打印子组件树中的JavaScript异常,同时显示一个异常替补UI。
12+
13+
### 组件内异常
14+
15+
组件内异常,也就是异常边界组件能够捕获的异常,主要包括:
16+
17+
1. 渲染过程中异常;
18+
2. 生命周期方法中的异常;
19+
3. 子组件树中各组件的constructor构造函数中异常。
20+
21+
### 其他异常
22+
23+
当然,异常边界组件依然存在一些无法捕获的异常,主要是异步及服务端触发异常:
24+
25+
1. 事件处理器中的异常;
26+
2. 异步任务异常,如setTiemout,ajax请求异常等;
27+
3. 服务端渲染异常;
28+
4. 异常边界组件自身内的异常;
29+
30+
## 异常边界组件
31+
32+
前面提到异常边界组件只能捕获其子组件树发生的异常,不能捕获自身抛出的异常,所以有必要注意两点:
33+
34+
1. 不能将现有组件改造为边界组件,否则无法捕获现有组件异常;
35+
2. 不能在边界组件内涉及业务逻辑,否则这里的业务逻辑异常无法捕获;
36+
37+
很显然,最终的异常边界组件必然是不涉及业务逻辑的独立中间组件。
38+
39+
那么一个异常边界组件如何捕获其子组件树异常呢?很简单,首先它也是一个React组件,然后添加`ComponentDidCatch`生命周期方法。
40+
41+
### 实例
42+
43+
创建一个React组件,然后添加`ComponentDidCatch`生命周期方法:
44+
45+
```react
46+
class ErrorBoundary extends React.Component {
47+
constructor(props) {
48+
super(props);
49+
this.state = { hasError: false };
50+
}
51+
52+
componentDidCatch(error, info) {
53+
// Display fallback UI
54+
this.setState({ hasError: true });
55+
// You can also log the error to an error reporting service
56+
logErrorToMyService(error, info);
57+
}
58+
59+
render() {
60+
if (this.state.hasError) {
61+
// You can render any custom fallback UI
62+
return <h1>Meet Some Errors.</h1>;
63+
}
64+
return this.props.children;
65+
}
66+
}
67+
```
68+
69+
接下来可以像使用普通React组件一样使用该组件:
70+
71+
```react
72+
<ErrorBoundary>
73+
<App />
74+
</ErrorBoundary>
75+
```
76+
77+
### ComponentDidCatch
78+
79+
这是一个新的生命周期方法,使用它可以捕获子组件异常,其原理类似于JavaScript异常捕获器`try, catch`
80+
81+
```react
82+
ComponentDidCatch(error, info)
83+
```
84+
85+
1. error:应用抛出的异常;
86+
87+
![异常对象](http://blog.codingplayboy.com/wp-content/uploads/2017/11/react-error-boundaries-error.png)
88+
89+
2. info:异常信息,包含`ComponentStack`属性对应异常过程中冒泡的组件栈;
90+
91+
![异常信息组件栈](http://blog.codingplayboy.com/wp-content/uploads/2017/11/react-error-boundaries-component-stack.png)
92+
93+
判断组件是否添加`componentDidCatch`生命周期方法,添加了,则调用包含异常处理的更新渲染组件方法:
94+
95+
```react
96+
if (inst.componentDidCatch) {
97+
this._updateRenderedComponentWithErrorHandling(
98+
transaction,
99+
unmaskedContext,
100+
);
101+
} else {
102+
this._updateRenderedComponent(transaction, unmaskedContext);
103+
}
104+
```
105+
106+
`_updateRenderedComponentWithErrorHandling`里面使用`try, catch`捕获异常:
107+
108+
```react
109+
/**
110+
* Call the component's `render` method and update the DOM accordingly.
111+
*
112+
* @param {ReactReconcileTransaction} transaction
113+
* @internal
114+
*/
115+
_updateRenderedComponentWithErrorHandling: function(transaction, context) {
116+
var checkpoint = transaction.checkpoint();
117+
try {
118+
this._updateRenderedComponent(transaction, context);
119+
} catch (e) {
120+
// Roll back to checkpoint, handle error (which may add items to the transaction),
121+
// and take a new checkpoint
122+
transaction.rollback(checkpoint);
123+
this._instance.componentDidCatch(e);
124+
125+
// Try again - we've informed the component about the error, so they can render an error message this time.
126+
// If this throws again, the error will bubble up (and can be caught by a higher error boundary).
127+
this._updateRenderedComponent(transaction, context);
128+
}
129+
},
130+
```
131+
132+
[具体源码见Github](https://github.com/facebook/react/blob/v16.0.0/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js)
133+
134+
### unstable_handleError
135+
136+
其实异常边界组件并不是突然出现在React中,在0.15版本中已经有测试[React 15 ErrorBoundaries](https://github.com/facebook/react/blob/15-stable/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js)[源码见Github](https://github.com/facebook/react/blob/15-stable/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js)。可以看见在源码中已经存在异常边界组件概念,但是尚不稳定,不推荐使用,从生命周期方法名也可以看出来:`unstable_handleError`,这也正是`ComponentDidCatch`的前身。
137+
138+
## 业务项目中的异常边界
139+
140+
前面提到的都是异常边界组件技术上可以捕获内部子组件异常,对于业务实际项目而言,还有需要思考的地方:
141+
142+
1. 异常边界组件的范围或粒度:是使用异常边界组件包裹应用根组件(粗粒度),还是只包裹独立模块入口组件(细粒度);
143+
2. 粗粒度使用异常边界组件是暴力处理异常,任何异常都将展示异常替补UI,完全中断了用户使用,但是确实能方便的捕获内部所有异常;
144+
3. 细粒度使用异常边界组件就以更友好的方式处理异常,局部异常只会中断该模块的使用,应用其他部分依然正常不受影响,但是通常应用中模块数量是很多的,而且具体模块划分到哪一程度也需要开发者考量,比较细致;
145+
146+
[点此传送查看实例](https://codepen.io/anon/pen/gXmrmR)
147+
148+
## 组件外异常
149+
150+
React 16提供的异常边界组件并不能捕获应用中的所有异常,而且React 16以后,**所有未被异常边界捕获的异常都将导致React卸载整个应用组件树**,所以通常需要通过一些其他前端异常处理方式进行异常捕获,处理和上报等,最常见的有两种方式:
151+
152+
1. `window.onerror`捕获全局JavaScript异常;
153+
154+
```react
155+
// 在应用入口组件内调用异常捕获
156+
componentWillMount: function () {
157+
this.startErrorLog();
158+
}
159+
startErrorLog:function() {
160+
window.onerror = (message, file, line, column, errorObject) => {
161+
column = column || (window.event && window.event.errorCharacter);
162+
const stack = errorObject ? errorObject.stack : null;
163+
164+
// trying to get stack from IE
165+
if (!stack) {
166+
var stack = [];
167+
var f = arguments.callee.caller;
168+
while (f) {
169+
stack.push(f.name);
170+
f = f.caller;
171+
}
172+
errorObject['stack'] = stack;
173+
}
174+
175+
const data = {
176+
message:message,
177+
file:file,
178+
line:line,
179+
column:column,
180+
errorStack:stack,
181+
};
182+
183+
// here I make a call to the server to log the error
184+
reportError(data);
185+
186+
// the error can still be triggered as usual, we just wanted to know what's happening on the client side
187+
// if return true, this error will not be console log out
188+
return false;
189+
}
190+
}
191+
```
192+
193+
2. `try, catch`手动定位包裹易出现异常的逻辑代码;
194+
195+
```react
196+
class Home extends React.Component {
197+
constructor(props) {
198+
super(props);
199+
this.state = { error: null };
200+
}
201+
202+
handleClick = () => {
203+
try {
204+
// Do something that could throw
205+
} catch (error) {
206+
this.setState({ error });
207+
}
208+
}
209+
210+
render() {
211+
if (this.state.error) {
212+
return <h1>Meet Some Errors.</h1>
213+
}
214+
return <div onClick={this.handleClick}>Click Me</div>
215+
}
216+
}
217+
```
218+
219+
220+
221+
常见的开源异常捕获,上报库,如sentry,badjs等都是利用这些方式提供常见的JavaScript执行异常。
222+
223+
## 参考
224+
225+
1. [Error Boundaries](https://reactjs.org/docs/error-boundaries.html)
226+
2. [Try Catch in Component](https://stackoverflow.com/questions/31111771/can-you-catch-all-errors-of-a-react-js-app-with-a-try-catch-block)
227+
3. [Handle React Errors in v15](https://medium.com/@blairanderson/handle-react-errors-in-v15-4cedaae757d7)

0 commit comments

Comments
 (0)