在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 <h1>Meet Some Errors.</h1>;
}
return this.props.children;
}
}複製程式碼
接下來可以像使用普通React元件一樣使用該元件:
<ErrorBoundary>
<App />
</ErrorBoundary>複製程式碼
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);
}
},複製程式碼
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) {
return <h1>Meet Some Errors.</h1>
}
return <div onClick={this.handleClick}>Click Me</div>
}
}複製程式碼
常見的開源異常捕獲,上報庫,如sentry,badjs等都是利用這些方式提供常見的JavaScript執行異常。