React,优雅的捕获异常

简介: EerrorBoundary是16版本出来的,有人问那我的15版本呢,我不听我不听,反正我用16,当然15有unstable_handleError。关于ErrorBoundary官网介绍比较详细,这个不是重点,重点是他能捕捉哪些异常。

1.JPG


前言


人无完人,所以代码总会出错,出错并不可怕,关键是怎么处理。


我就想问问大家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;
    }
}
复制代码


总结一下


  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;
}


相关文章
|
16天前
|
前端开发 JavaScript UED
处理 React 运行时中的错误和异常
【10月更文挑战第25天】在React运行时中,有效地处理错误和异常对于确保应用的稳定性和用户体验至关重要。
|
前端开发 JavaScript
在React项目是如何捕获错误的?
在React项目是如何捕获错误的?
126 0
|
前端开发 JavaScript
React,优雅的捕获异常进阶篇, 含Hooks方案
在React项目中,因为事件处理程序总是需要写 try/catch,不胜其烦。 虽然可以丢给window.onerror或者 window.addEventListener("error")去处理,但是对错误细节的捕获以及错误的补偿是极其不友好的。
927 0
React,优雅的捕获异常进阶篇, 含Hooks方案
|
1月前
|
前端开发 JavaScript 开发者
深入理解React Hooks:提升前端开发效率的关键
【10月更文挑战第5天】深入理解React Hooks:提升前端开发效率的关键
|
10天前
|
前端开发 JavaScript 开发者
颠覆传统:React框架如何引领前端开发的革命性变革
【10月更文挑战第32天】本文以问答形式探讨了React框架的特性和应用。React是一款由Facebook推出的JavaScript库,以其虚拟DOM机制和组件化设计,成为构建高性能单页面应用的理想选择。文章介绍了如何开始一个React项目、组件化思想的体现、性能优化方法、表单处理及路由实现等内容,帮助开发者更好地理解和使用React。
36 9
|
1月前
|
前端开发
深入解析React Hooks:构建高效且可维护的前端应用
本文将带你走进React Hooks的世界,探索这一革新特性如何改变我们构建React组件的方式。通过分析Hooks的核心概念、使用方法和最佳实践,文章旨在帮助你充分利用Hooks来提高开发效率,编写更简洁、更可维护的前端代码。我们将通过实际代码示例,深入了解useState、useEffect等常用Hooks的内部工作原理,并探讨如何自定义Hooks以复用逻辑。
|
1月前
|
前端开发 JavaScript API
探索React Hooks:前端开发的革命性工具
【10月更文挑战第5天】探索React Hooks:前端开发的革命性工具
|
1月前
|
前端开发 数据管理 编译器
引领前端未来:React 19的重大更新与实战指南🚀
React 19 即将发布,带来一系列革命性的新功能,旨在简化开发过程并显著提升性能。本文介绍了 React 19 的核心功能,如自动优化重新渲染的 React 编译器、加速初始加载的服务器组件、简化表单处理的 Actions、无缝集成的 Web 组件,以及文档元数据的直接管理。这些新功能通过自动化、优化和增强用户体验,帮助开发者构建更高效的 Web 应用程序。
90 1
引领前端未来:React 19的重大更新与实战指南🚀
|
15天前
|
前端开发 JavaScript Android开发
前端框架趋势:React Native在跨平台开发中的优势与挑战
【10月更文挑战第27天】React Native 是跨平台开发领域的佼佼者,凭借其独特的跨平台能力和高效的开发体验,成为许多开发者的首选。本文探讨了 React Native 的优势与挑战,包括跨平台开发能力、原生组件渲染、性能优化及调试复杂性等问题,并通过代码示例展示了其实际应用。
43 2
|
17天前
|
前端开发 JavaScript 开发者
React与Vue:前端框架的巅峰对决与选择策略
【10月更文挑战第23天】React与Vue:前端框架的巅峰对决与选择策略