React如何优雅的捕获异常

2022-04-15 0 374
目录
  • 前言
  • ErrorBoundary
  • Error Boundary 之外
    • try/catch
    • window.onerror , error事件
    • unhandledrejection
    • XMLHttpRequest 与 fetch
  • 事件处理程序的异常捕获
    • 示例
    • 类型定义
    • 自定义的CatchError
    • 装饰器
  • 总结一下
    • 下一步
      • 写在最后

        前言

        人无完人,所以代码总会出错,出错并不可怕,关键是怎么处理。
        我就想问问大家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 = { 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, 完美。

        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 之外

        我们先看看一张表格,罗列了我们能捕获异常的手段和范围。

        异常类型 同步方法 异步方法 资源加载 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;
            }
        }

        总结一下

        利用装饰器重写原方法,达到捕获错误的目的
        自定义错误类,抛出它,就能达到覆盖默认选项的目的。增加了灵活性。

          @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
               })
            }

        下一步

        啥下一步,走一步看一步啦。
        不,接下来的路,还很长。  这才是一个基础版本。

        扩大成果

        @XXXCatch
        classs AAA{
            @YYYCatch
            method = ()=> {
            }
        }
        

        抽象,再抽象,再抽象

        再见。

        写在最后

        error-boundaries
        React异常处理
        catching-react-errors
        react进阶之异常处理机制-error Boundaries
        decorator
        core-decorators
        autobind.js

        到此这篇关于React如何优雅的捕获异常的文章就介绍到这了,更多相关React 捕获异常内容请搜索NICE源码以前的文章或继续浏览下面的相关文章希望大家以后多多支持NICE源码!

        免责声明:
        1、本网站所有发布的源码、软件和资料均为收集各大资源网站整理而来;仅限用于学习和研究目的,您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。 不得使用于非法商业用途,不得违反国家法律。否则后果自负!

        2、本站信息来自网络,版权争议与本站无关。一切关于该资源商业行为与www.niceym.com无关。
        如果您喜欢该程序,请支持正版源码、软件,购买注册,得到更好的正版服务。
        如有侵犯你版权的,请邮件与我们联系处理(邮箱:skknet@qq.com),本站将立即改正。

        NICE源码网 JavaScript React如何优雅的捕获异常 https://www.niceym.com/28878.html