1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        React,優(yōu)雅的捕獲異常

        共 22993字,需瀏覽 46分鐘

         ·

        2021-07-06 16:49

        點擊上方 前端Q,關注公眾號

        回復加群,加入前端Q技術交流群

        來源:云的世界

        https://juejin.cn/post/6974383324148006926


        前言

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

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

        ErrorBoundary

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

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

        • 子組件的渲染
        • 生命周期函數
        • 構造函數
        class ErrorBoundary extends React.Component {
          constructor(props) {
            super(props);
            this.state = { hasErrorfalse };
          }

          componentDidCatch(error, info) {
            // Display fallback UI
            this.setState({ hasErrortrue });
            // 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 這種優(yōu)秀的庫。
        你只需要關心出現錯誤后需要關心什么,還以來個 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并不會捕捉這些錯誤:

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

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

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

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

        Error Boundary 之外

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

        異常類型同步方法異步方法資源加載Promiseasync/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('報錯咯');
                // 上報異常信息到后端,動態(tài)創(chuàng)建標簽方式
                new Image().src = `http://localhost:3000/log/report?info=${JSON.stringify(errors)}`
              }}
            >
              <Main />
            </ErrorCatch>
        )
        }

        export default 
        復制代碼

        鼓掌,鼓掌。

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

        錯誤那么多,我就先好好處理React里面的事件處理程序。
        至于其他,待續(xù)。

        事件處理程序的異常捕獲

        示例

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

        先看一下使用:


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

        注意四個參數:

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

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

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

            }
        復制代碼

        是都,沒錯,你可以通過拋出 自定義的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: stringpublic 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"創(chuàng)建訂單失敗"toasttruereport:truelog:true })
            async createOrder() {
                const data = {...};
                const res = await createOrder();
                if (!res || res.errCode !== 0) {
                    return Toast.error("創(chuàng)建訂單失敗");
                }
               Toast.success("創(chuàng)建訂單成功");
               
                .......
                其他可能產生異常的代碼
                .......
                
               throw new CatchError("創(chuàng)建訂單失敗了,請聯系管理員", {
                   toasttrue,
                   reporttrue,
                   logfalse
               })
            }
        復制代碼

        下一步

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

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

        1. 擴大成果,支持更多類型,以及hooks版本。

        @XXXCatch
        classs AAA{
            @YYYCatch
            method = ()=> {
            }
        }
        復制代碼
        1. 抽象,再抽象,再抽象

        玩笑開完了,嚴肅一下:

        當前方案存在的問題:

        1. 功能局限
        2. 抽象不夠
          獲取選項,代理函數, 錯誤處理函數完全可以分離,變成通用方法。
        3. 同步方法經過轉換后會變?yōu)楫惒椒椒ā?br>所以理論上,要區(qū)分同步和異步方案。
        4. 錯誤處理函數再異常怎么辦

        之后,我們會圍繞著這些問題,繼續(xù)展開。

        Hooks版本

        有掘友說,這個年代了,誰還不用Hooks。
        是的,大佬們說得對,我們得與時俱進。
        Hooks的基礎版本已經有了,先分享使用,后續(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}>點我</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, undefinedfunction (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

        聲明:文章著作權歸作者所有,如有侵權,請聯系小編刪除。




        內推社群


        我組建了一個氛圍特別好的騰訊內推社群,如果你對加入騰訊感興趣的話(后續(xù)有計劃也可以),我們可以一起進行面試相關的答疑、聊聊面試的故事、并且在你準備好的時候隨時幫你內推。下方加 winty 好友回復「面試」即可。


        瀏覽 63
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            黄色视频网站免费在线观看 | 扒开共妻屁股调教 | 午夜成人欧美一区二区三区 | 精品综合 | 国产午夜精品免费一区二区三区视频 | 天天舔天天插天天干 | 粉嫩被两根大粗黑进出视频 | 亚洲欧洲AV | 高清久久久久 | 欧美日韩大屌 |