React ,優雅的捕獲異常

前言

人無完人,所以代碼總會出錯,出錯並不可怕,關鍵是怎麼處理。
我就想問問大家 react 的應用的錯誤怎麼捕捉呢?這個時候:

ErrorBoundary

EerrorBoundary 是 16 版本出來的,有人問那我的 15 版本呢,我不聽我不聽,反正我用 16,當然 15 有unstable_handleError

關於 ErrorBoundary 官網介紹比較詳細,這個不是重點,重點是他能捕捉哪些異常。

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] 這種優秀的庫。
你只需要關心出現錯誤後需要關心什麼,還以來個 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 並不會捕捉這些錯誤:

原文可見參見官網 introducing-error-boundaries[2]

本文要捕獲的就是 事件處理程序的錯誤。
官方其實也是有方案的 how-about-event-handlers[3], 就是 try catch.
但是,那麼多事件處理程序,我的天,得寫多少,。。。。。。。。。。。。。。。。。。。。

  handleClick() {
    try {
      // Do something that could throw
    } catch (error) {
      this.setState({ error });
    }
  }

Error Boundary 之外

我們先看看一張表格,羅列了我們能捕獲異常的手段和範圍。

xnBYna

try/catch

可以捕獲同步和 async/await 的異常。

window.onerror , error 事件

    window.addEventListener('error', this.onError, true);
    window.onerror = this.onError

window.addEventListener('error') 這種可以比 window.onerror 多捕獲資源記載異常. 請注意最後一個參數是 true, false的話可能就不如你期望。

當然你如果問題這第三個參數的含義,我就有點不想理你了。拜。

unhandledrejection

請注意最後一個參數是 true

window.removeEventListener('unhandledrejection', this.onReject, true)

其捕獲未被捕獲的 Promise 的異常。

XMLHttpRequest 與 fetch

XMLHttpRequest 很好處理,自己有 onerror 事件。當然你 99.99% 也不會自己基於XMLHttpRequest封裝一個庫, axios 真香,有這完畢的錯誤處理機制。

至於fetch, 自己帶着 catch 跑,不處理就是你自己的問題了。

這麼多,太難了。
還好,其實有一個庫 react-error-catch[4] 是基於 ErrorBoudary,error 與 unhandledrejection 封裝的一個組件。

其核心如下

   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('報錯咯');
        // 上報異常信息到後端,動態創建標籤方式
        new Image().src = `http://localhost:3000/log/report?info=${JSON.stringify(errors)}`
      }}
    >
      <Main />
    </ErrorCatch>)
}

export default

鼓掌,鼓掌。

其實不然:利用 error 捕獲的錯誤,其最主要的是提供了錯誤堆棧信息,對於分析錯誤相當不友好,尤其打包之後。

錯誤那麼多,我就先好好處理 React 裏面的事件處理程序。
至於其他,待續。

事件處理程序的異常捕獲

示例

我的思路原理很簡單,使用 decorator[5] 來重寫原來的方法。

先看一下使用:

   @methodCatch({ message: "創建訂單失敗", toast: true, report:true, log:true })
    async createOrder() {
        const data = {...};
        const res = await createOrder();
        if (!res || res.errCode !== 0) {
            return Toast.error("創建訂單失敗");
        }
        
        .......
        其他可能產生異常的代碼
        .......
        
       Toast.success("創建訂單成功");
    }

注意四個參數:

可能你說,這這,消息定死,不合理啊。我要是有其他消息呢。
此時我微微一笑別急, 再看一段代碼

  @methodCatch({ message: "創建訂單失敗", toast: true, report:true, log:true })
    async createOrder() {
        const data = {...};
        const res = await createOrder();
        if (!res || res.errCode !== 0) {
            return Toast.error("創建訂單失敗");
        }
       
        .......
        其他可能產生異常的代碼
        .......
        
       throw new CatchError("創建訂單失敗了,請聯繫管理員"{
           toast: true,
           report: true,
           log: false
       })
       
       Toast.success("創建訂單成功");

    }

是都,沒錯,你可以通過拋出 自定義的CatchError來覆蓋之前的默認選項。

這個methodCatch可以捕獲,同步和異步的錯誤,我們來一起看看全部的代碼。

類型定義

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__";
    /**
     * 捕捉到的錯誤
     * @param message 消息
     * @options 其他參數
     */
    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 或者 不是字符串或者對象
        opt = DEFAULT_ERROR_CATCH_OPTIONS;
    } else if (typeof options === "string") {  // 字符串
        opt = {
            ...DEFAULT_ERROR_CATCH_OPTIONS,
            message: options || DEFAULT_ERROR_CATCH_OPTIONS.message,
        }
    } else { // 有效的對象
        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__"){
                            err = 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;
    }
}

總結一下

  1. 利用裝飾器重寫原方法,達到捕獲錯誤的目的
  2. 自定義錯誤類,拋出它,就能達到覆蓋默認選項的目的。增加了靈活性。
  @methodCatch({ message: "創建訂單失敗", toast: true, report:true, log:true })
    async createOrder() {
        const data = {...};
        const res = await createOrder();
        if (!res || res.errCode !== 0) {
            return Toast.error("創建訂單失敗");
        }
       Toast.success("創建訂單成功");
       
        .......
        其他可能產生異常的代碼
        .......
        
       throw new CatchError("創建訂單失敗了,請聯繫管理員"{
           toast: true,
           report: true,
           log: false
       })
    }

下一步

啥下一步,走一步看一步啦。

不,接下來的路,還很長。這纔是一個基礎版本。

  1. 擴大成果,支持更多類型,以及hooks版本。
@XXXCatch
classs AAA{
    @YYYCatch
    method = ()={
    }
}
  1. 抽象,再抽象,再抽象

玩笑開完了,嚴肅一下:

當前方案存在的問題:

  1. 功能侷限

  2. 抽象不夠
    獲取選項, 代理函數, 錯誤處理函數完全可以分離,變成通用方法。

  3. 同步方法經過轉換後會變爲異步方法。
    所以理論上,要區分同步和異步方案。

  4. 錯誤處理函數再異常怎麼辦

之後,我們會圍繞着這些問題,繼續展開。

Hooks 版本

有掘友說,這個年代了,誰還不用 Hooks。
是的,大佬們說得對,我們得與時俱進。
Hooks 的基礎版本已經有了,先分享使用,後續的文章跟上。

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}>點我</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 進階之異常處理機制 - error Boundaries[9]
decorator[10]
core-decorators[11]
autobind.js[12]

參考資料

[1] https://www.npmjs.com/package/react-error-boundary: https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Freact-error-boundary

[2] https://reactjs.org/docs/error-boundaries.html#introducing-error-boundaries: https://link.juejin.cn?target=https%3A%2F%2Freactjs.org%2Fdocs%2Ferror-boundaries.html%23introducing-error-boundaries

[3] https://reactjs.org/docs/error-boundaries.html#how-about-event-handlers: https://link.juejin.cn?target=https%3A%2F%2Freactjs.org%2Fdocs%2Ferror-boundaries.html%23how-about-event-handlers

[4] https://www.npmjs.com/package/react-error-catch: https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Freact-error-catch

[5] http://es6.ruanyifeng.com/#docs/decorator: https://link.juejin.cn?target=http%3A%2F%2Fes6.ruanyifeng.com%2F%23docs%2Fdecorator

[6] https://reactjs.org/docs/error-boundaries.html: https://link.juejin.cn?target=https%3A%2F%2Freactjs.org%2Fdocs%2Ferror-boundaries.html

[7] https://www.colabug.com/1867349.html: https://link.juejin.cn?target=https%3A%2F%2Fwww.colabug.com%2F1867349.html

[8] https://engineering.classdojo.com/blog/2016/12/10/catching-react-errors/: https://link.juejin.cn?target=https%3A%2F%2Fengineering.classdojo.com%2Fblog%2F2016%2F12%2F10%2Fcatching-react-errors%2F

[9] https://blog.csdn.net/a986597353/article/details/78469979: https://link.juejin.cn?target=https%3A%2F%2Fblog.csdn.net%2Fa986597353%2Farticle%2Fdetails%2F78469979

[10] http://es6.ruanyifeng.com/#docs/decorator: https://link.juejin.cn?target=http%3A%2F%2Fes6.ruanyifeng.com%2F%23docs%2Fdecorator

[11] https://github.com/jayphelps/core-decorators: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fjayphelps%2Fcore-decorators

[12] https://github.com/jayphelps/core-decorators/blob/master/src/autobind.js: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fjayphelps%2Fcore-decorators%2Fblob%2Fmaster%2Fsrc%2Fautobind.js

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/PT9w-vv4XlwsmO-9h_SNSg