前言
我們期望的是在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中,在0.15版本中已經有測試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執行異常。