4

React,优雅的捕获异常

 3 years ago
source link: https://juejin.cn/post/6974383324148006926
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

这是我参与更文挑战的第7天,活动详情查看: 更文挑战

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

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

异常类型同步方法异步方法资源加载Promiseasync/awaittry/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. 错误处理函数再异常怎么办

之后,我们会围绕着这些问题,继续展开。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK