参考答案

ErrorBoundary

EerrorBoundary 是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,对开发者来说,只需要关心出现错误后的处理。

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('报错咯'); // 上报异常信息到后端,动态创建标签方式 new Image().src = `http://localhost:3000/log/report?info=${JSON.stringify(errors)}` }} > <Main /> </ErrorCatch>) } export default

利用error捕获的错误,其最主要的是提供了错误堆栈信息,对于分析错误相当不友好,尤其打包之后。

事件处理程序的异常捕获

示例

使用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版本

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