React ,優(yōu)雅的捕獲異常
前言
人無完人,所以代碼總會(huì)出錯(cuò),出錯(cuò)并不可怕,關(guān)鍵是怎么處理。
我就想問問大家react的應(yīng)用的錯(cuò)誤怎么捕捉呢?這個(gè)時(shí)候:
- 小白+++:怎么處理?
- 小白++:ErrorBoundary
- 小白+:ErrorBoundary, try catch
- 小黑#: ErrorBoundary, try catch, window.onerror
- 小黑##: 這個(gè)是個(gè)嚴(yán)肅的問題,我知道N種處理方式,你有什么更好的方案?
ErrorBoundary
EerrorBoundary是16版本出來的,有人問那我的15版本呢,我不聽我不聽,反正我用16,當(dāng)然15有unstable_handleError。
關(guān)于ErrorBoundary官網(wǎng)介紹比較詳細(xì),這個(gè)不是重點(diǎn),重點(diǎn)是他能捕捉哪些異常。
- 子組件的渲染
- 生命周期函數(shù)
- 構(gòu)造函數(shù)
- 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>Something went wrong.</h1>;
- }
- return this.props.children;
- }
- }
- <ErrorBoundary>
- <MyWidget />
- </ErrorBoundary>
開源世界就是好,早有大神封裝了react-error-boundary[1] 這種優(yōu)秀的庫。
你只需要關(guān)心出現(xiàn)錯(cuò)誤后需要關(guān)心什么,還以來個(gè) Reset, 完美。
- import {ErrorBoundary} from 'react-error-boundary'
- function ErrorFallback({error, resetErrorBoundary}) {
- return (
- <div role="alert">
- <p>Something went wrong:</p>
- <pre>{error.message}</pre>
- <button onClick={resetErrorBoundary}>Try again</button>
- </div>
- )
- }
- const ui = (
- <ErrorBoundary
- FallbackComponent={ErrorFallback}
- onReset={() => {
- // reset the state of your app so the error doesn't happen again
- }}
- >
- <ComponentThatMayError />
- </ErrorBoundary>
- )
遺憾的是,error boundaries并不會(huì)捕捉這些錯(cuò)誤:
- 事件處理程序
- 異步代碼 (e.g. setTimeout or requestAnimationFrame callbacks)
- 服務(wù)端的渲染代碼
- error boundaries自己拋出的錯(cuò)誤
原文可見參見官網(wǎng)introducing-error-boundaries[2]
本文要捕獲的就是 事件處理程序的錯(cuò)誤。
官方其實(shí)也是有方案的how-about-event-handlers[3], 就是 try catch.
但是,那么多事件處理程序,我的天,得寫多少,。。。。。。。。。。。。。。。。。。。。
- handleClick() {
- try {
- // Do something that could throw
- } catch (error) {
- this.setState({ error });
- }
- }
Error Boundary 之外
我們先看看一張表格,羅列了我們能捕獲異常的手段和范圍。
異常類型 同步方法 異步方法 資源加載 Promise async/await
異常類型 | 同步方法 | 異步方法 | 資源加載 | Promise | async/await |
---|---|---|---|---|---|
try/catch | √ | √ | |||
window.onerror | √ | √ | |||
error | √ | √ | √ | ||
unhandledrejection | √ | √ |
try/catch
可以捕獲同步和async/await的異常。
window.onerror , error事件
- window.addEventListener('error', this.onError, true);
- window.onerror = this.onError
window.addEventListener('error') 這種可以比 window.onerror 多捕獲資源記載異常. 請(qǐng)注意最后一個(gè)參數(shù)是 true, false的話可能就不如你期望。
當(dāng)然你如果問題這第三個(gè)參數(shù)的含義,我就有點(diǎn)不想理你了。拜。
unhandledrejection
請(qǐng)注意最后一個(gè)參數(shù)是 true。
- window.removeEventListener('unhandledrejection', this.onReject, true)
其捕獲未被捕獲的Promise的異常。
XMLHttpRequest 與 fetch
XMLHttpRequest 很好處理,自己有onerror事件。當(dāng)然你99.99%也不會(huì)自己基于XMLHttpRequest封裝一個(gè)庫, axios 真香,有這完畢的錯(cuò)誤處理機(jī)制。
至于fetch, 自己帶著catch跑,不處理就是你自己的問題了。
這么多,太難了。
還好,其實(shí)有一個(gè)庫 react-error-catch[4] 是基于ErrorBoudary,error與unhandledrejection封裝的一個(gè)組件。
其核心如下
- ErrorBoundary.prototype.componentDidMount = function () {
- // event catch
- window.addEventListener('error', this.catchError, true);
- // async code
- window.addEventListener('unhandledrejection', this.catchRejectEvent, true);
- };
使用:
- import ErrorCatch from 'react-error-catch'
- const App = () => {
- return (
- <ErrorCatch
- app="react-catch"
- user="cxyuns"
- delay={5000}
- max={1}
- filters={[]}
- onCatch={(errors) => {
- console.log('報(bào)錯(cuò)咯');
- // 上報(bào)異常信息到后端,動(dòng)態(tài)創(chuàng)建標(biāo)簽方式
- new Image().src = `http://localhost:3000/log/report?info=${JSON.stringify(errors)}`
- }}
- >
- <Main />
- </ErrorCatch>)
- }
- export default
鼓掌,鼓掌。
其實(shí)不然:利用error捕獲的錯(cuò)誤,其最主要的是提供了錯(cuò)誤堆棧信息,對(duì)于分析錯(cuò)誤相當(dāng)不友好,尤其打包之后。
錯(cuò)誤那么多,我就先好好處理React里面的事件處理程序。
至于其他,待續(xù)。
事件處理程序的異常捕獲
示例
我的思路原理很簡單,使用decorator[5]來重寫原來的方法。
先看一下使用:
- @methodCatch({ message: "創(chuàng)建訂單失敗", toast: true, report:true, log:true })
- async createOrder() {
- const data = {...};
- const res = await createOrder();
- if (!res || res.errCode !== 0) {
- return Toast.error("創(chuàng)建訂單失敗");
- }
- .......
- 其他可能產(chǎn)生異常的代碼
- .......
- Toast.success("創(chuàng)建訂單成功");
- }
注意四個(gè)參數(shù):
- message:出現(xiàn)錯(cuò)誤時(shí),打印的錯(cuò)誤
- toast:出現(xiàn)錯(cuò)誤,是否Toast
- report: 出現(xiàn)錯(cuò)誤,是否上報(bào)
- log: 使用使用console.error打印
可能你說,這這,消息定死,不合理啊。我要是有其他消息呢。
此時(shí)我微微一笑別急, 再看一段代碼
- @methodCatch({ message: "創(chuàng)建訂單失敗", toast: true, report:true, log:true })
- async createOrder() {
- const data = {...};
- const res = await createOrder();
- if (!res || res.errCode !== 0) {
- return Toast.error("創(chuàng)建訂單失敗");
- }
- .......
- 其他可能產(chǎn)生異常的代碼
- .......
- throw new CatchError("創(chuàng)建訂單失敗了,請(qǐng)聯(lián)系管理員", {
- toast: true,
- report: true,
- log: false
- })
- Toast.success("創(chuàng)建訂單成功");
- }
是都,沒錯(cuò),你可以通過拋出 自定義的CatchError來覆蓋之前的默認(rèn)選項(xiàng)。
這個(gè)methodCatch可以捕獲,同步和異步的錯(cuò)誤,我們來一起看看全部的代碼。
類型定義
- export interface CatchOptions {
- report?: boolean;
- message?: string;
- log?: boolean;
- toast?: boolean;
- }
- // 這里寫到 const.ts更合理
- export const DEFAULT_ERROR_CATCH_OPTIONS: CatchOptions = {
- report: true,
- message: "未知異常",
- log: true,
- toast: false
- }
自定義的CatchError
- import { CatchOptions, DEFAULT_ERROR_CATCH_OPTIONS } from "@typess/errorCatch";
- export class CatchError extends Error {
- public __type__ = "__CATCH_ERROR__";
- /**
- * 捕捉到的錯(cuò)誤
- * @param message 消息
- * @options 其他參數(shù)
- */
- constructor(message: string, public options: CatchOptions = DEFAULT_ERROR_CATCH_OPTIONS) {
- super(message);
- }
- }
裝飾器
- import Toast from "@components/Toast";
- import { CatchOptions, DEFAULT_ERROR_CATCH_OPTIONS } from "@typess/errorCatch";
- import { CatchError } from "@util/error/CatchError";
- const W_TYPES = ["string", "object"];
- export function methodCatch(options: string | CatchOptions = DEFAULT_ERROR_CATCH_OPTIONS) {
- const type = typeof options;
- let opt: CatchOptions;
- if (options == null || !W_TYPES.includes(type)) { // null 或者 不是字符串或者對(duì)象
- opt = DEFAULT_ERROR_CATCH_OPTIONS;
- } else if (typeof options === "string") { // 字符串
- opt = {
- ...DEFAULT_ERROR_CATCH_OPTIONS,
- message: options || DEFAULT_ERROR_CATCH_OPTIONS.message,
- }
- } else { // 有效的對(duì)象
- opt = { ...DEFAULT_ERROR_CATCH_OPTIONS, ...options }
- }
- return function (_target: any, _name: string, descriptor: PropertyDescriptor): any {
- const oldFn = descriptor.value;
- Object.defineProperty(descriptor, "value", {
- get() {
- async function proxy(...args: any[]) {
- try {
- const res = await oldFn.apply(this, args);
- return res;
- } catch (err) {
- // if (err instanceof CatchError) {
- if(err.__type__ == "__CATCH_ERROR__"){
- errerr = err as CatchError;
- const mOpt = { ...opt, ...(err.options || {}) };
- if (mOpt.log) {
- console.error("asyncMethodCatch:", mOpt.message || err.message , err);
- }
- if (mOpt.report) {
- // TODO::
- }
- if (mOpt.toast) {
- Toast.error(mOpt.message);
- }
- } else {
- const message = err.message || opt.message;
- console.error("asyncMethodCatch:", message, err);
- if (opt.toast) {
- Toast.error(message);
- }
- }
- }
- }
- proxy._bound = true;
- return proxy;
- }
- })
- return descriptor;
- }
- }
總結(jié)一下
- 利用裝飾器重寫原方法,達(dá)到捕獲錯(cuò)誤的目的
- 自定義錯(cuò)誤類,拋出它,就能達(dá)到覆蓋默認(rèn)選項(xiàng)的目的。增加了靈活性。
- @methodCatch({ message: "創(chuàng)建訂單失敗", toast: true, report:true, log:true })
- async createOrder() {
- const data = {...};
- const res = await createOrder();
- if (!res || res.errCode !== 0) {
- return Toast.error("創(chuàng)建訂單失敗");
- }
- Toast.success("創(chuàng)建訂單成功");
- .......
- 其他可能產(chǎn)生異常的代碼
- .......
- throw new CatchError("創(chuàng)建訂單失敗了,請(qǐng)聯(lián)系管理員", {
- toast: true,
- report: true,
- log: false
- })
- }
下一步
啥下一步,走一步看一步啦。
不,接下來的路,還很長。這才是一個(gè)基礎(chǔ)版本。
1. 擴(kuò)大成果,支持更多類型,以及hooks版本。
- @XXXCatch
- classs AAA{
- @YYYCatch
- method = ()=> {
- }
- }
2. 抽象,再抽象,再抽象
玩笑開完了,嚴(yán)肅一下:
當(dāng)前方案存在的問題:
- 功能局限
- 抽象不夠
- 獲取選項(xiàng),代理函數(shù), 錯(cuò)誤處理函數(shù)完全可以分離,變成通用方法。
- 同步方法經(jīng)過轉(zhuǎn)換后會(huì)變?yōu)楫惒椒椒ā?/li>
- 所以理論上,要區(qū)分同步和異步方案。
- 錯(cuò)誤處理函數(shù)再異常怎么辦
之后,我們會(huì)圍繞著這些問題,繼續(xù)展開。
Hooks版本
有掘友說,這個(gè)年代了,誰還不用Hooks。
是的,大佬們說得對(duì),我們得與時(shí)俱進(jìn)。
Hooks的基礎(chǔ)版本已經(jīng)有了,先分享使用,后續(xù)的文章跟上。
Hook的名字就叫useCatch
- const TestView: React.FC<Props> = function (props) {
- const [count, setCount] = useState(0);
- const doSomething = useCatch(async function(){
- console.log("doSomething: begin");
- throw new CatchError("doSomething error")
- console.log("doSomething: end");
- }, [], {
- toast: true
- })
- const onClick = useCatch(async (ev) => {
- console.log(ev.target);
- setCount(count + 1);
- doSomething();
- const d = delay(3000, () => {
- setCount(count => count + 1);
- console.log()
- });
- console.log("delay begin:", Date.now())
- await d.run();
- console.log("delay end:", Date.now())
- console.log("TestView", this)
- throw new CatchError("自定義的異常,你知道不")
- },
- [count],
- {
- message: "I am so sorry",
- toast: true
- });
- return <div>
- <div><button onClick={onClick}>點(diǎn)我</button></div>
- <div>{count}</div>
- </div>
- }
- export default React.memo(TestView);
至于思路,基于useMemo,可以先看一下代碼:
- export function useCatch<T extends (...args: any[]) => any>(callback: T, deps: DependencyList, options: CatchOptions =DEFAULT_ERRPR_CATCH_OPTIONS): T {
- const opt = useMemo( ()=> getOptions(options), [options]);
- const fn = useMemo((..._args: any[]) => {
- const proxy = observerHandler(callback, undefined, function (error: Error) {
- commonErrorHandler(error, opt)
- });
- return proxy;
- }, [callback, deps, opt]) as T;
- return fn;
- }
寫在最后
error-boundaries[6]
React異常處理[7]
catching-react-errors[8]
react進(jìn)階之異常處理機(jī)制-error Boundaries[9]
decorator[10]
core-decorators[11]
autobind.js[12]