React,優雅的捕獲異常

這是我參與更文挑戰的第7天,活動詳情查看: 更文挑戰html

姊妹篇 React,優雅的捕獲異常進階篇, 含Hooks方案react

前言

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

  • 小白+++:怎麼處理?
  • 小白++: ErrorBoundary
  • 小白+: ErrorBoundary, try catch
  • 小黑#: ErrorBoundary, try catch, window.onerror
  • 小黑##: 這個是個嚴肅的問題,我知道N種處理方式,你有什麼更好的方案?

ErrorBoundary

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

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

  • 子組件的渲染
  • 生命週期函數
  • 構造函數
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 這種優秀的庫。
你只須要關心出現錯誤後須要關心什麼,還以來個 Reset, 完美。github

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並不會捕捉這些錯誤:typescript

  • 事件處理程序
  • 異步代碼 (e.g. setTimeout or requestAnimationFrame callbacks)
  • 服務端的渲染代碼
  • error boundaries本身拋出的錯誤

原文可見參見官網introducing-error-boundariesnpm

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

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

Error Boundary 以外

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

異常類型 同步方法 異步方法 資源加載 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 多捕獲資源記載異常. 請注意最後一個參數是 true, false的話可能就不如你指望。

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

unhandledrejection

請注意最後一個參數是 true

window.removeEventListener('unhandledrejection', this.onReject, true)
複製代碼

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

XMLHttpRequest 與 fetch

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

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

這麼多,太難了。
還好,其實有一個庫 react-error-catch 是基於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來重寫原來的方法。

先看一下使用:

@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("建立訂單成功");
    }
複製代碼

注意四個參數:

  • message: 出現錯誤時,打印的錯誤
  • toast: 出現錯誤,是否Toast
  • report: 出現錯誤,是否上報
  • log: 使用使用console.error打印

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

@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
React異常處理
catching-react-errors
react進階之異常處理機制-error Boundaries
decorator
core-decorators
autobind.js

相關文章
相關標籤/搜索