ErrorBoundary
EerrorBoundary是16版本出来的,有人问那我的15版本呢,我不听我不听,反正我用16,当然15有unstable_handleError
。
关于ErrorBoundary官网介绍比较详细,这个不是重点,重点是他能捕捉哪些异常。
- 子组件的渲染
- 生命周期函数
- 构造函数
1class ErrorBoundary extends React.Component {
2 constructor(props) {
3 super(props);
4 this.state = { hasError: false };
5 }
6
7 componentDidCatch(error, info) {
8 // Display fallback UI
9 this.setState({ hasError: true });
10 // You can also log the error to an error reporting service
11 logErrorToMyService(error, info);
12 }
13
14 render() {
15 if (this.state.hasError) {
16 // You can render any custom fallback UI
17 return <h1>Something went wrong.</h1>;
18 }
19 return this.props.children;
20 }
21}
22
23
24<ErrorBoundary>
25 <MyWidget />
26</ErrorBoundary>
27复制代码
开源世界就是好,早有大神封装了react-error-boundary 这种优秀的库。 你只需要关心出现错误后需要关心什么,还以来个 Reset
, 完美。
1import {ErrorBoundary} from 'react-error-boundary'
2
3function ErrorFallback({error, resetErrorBoundary}) {
4 return (
5 <div role="alert">
6 <p>Something went wrong:</p>
7 <pre>{error.message}</pre>
8 <button onClick={resetErrorBoundary}>Try again</button>
9 </div>
10 )
11}
12
13const ui = (
14 <ErrorBoundary
15 FallbackComponent={ErrorFallback}
16 onReset={() => {
17 // reset the state of your app so the error doesn't happen again
18 }}
19 >
20 <ComponentThatMayError />
21 </ErrorBoundary>
22)
23复制代码
遗憾的是,error boundaries并不会捕捉这些错误:
- 事件处理程序
- 异步代码 (e.g. setTimeout or requestAnimationFrame callbacks)
- 服务端的渲染代码
- error boundaries自己抛出的错误
原文可见参见官网introducing-error-boundaries
本文要捕获的就是 事件处理程序的错误。 官方其实也是有方案的how-about-event-handlers, 就是 try catch. 但是,那么多事件处理程序,我的天,得写多少,。。。。。。。。。。。。。。。。。。。。
1 handleClick() {
2 try {
3 // Do something that could throw
4 } catch (error) {
5 this.setState({ error });
6 }
7 }
8复制代码
Error Boundary 之外
我们先看看一张表格,罗列了我们能捕获异常的手段和范围。
异常类型 | 同步方法 | 异步方法 | 资源加载 | Promise | async/await |
---|---|---|---|---|---|
try/catch | √ | √ | |||
window.onerror | √ | √ | |||
error | √ | √ | √ | ||
unhandledrejection | √ | √ |
try/catch
可以捕获同步和async/await的异常。
window.onerror , error事件
1 window.addEventListener('error', this.onError, true);
2 window.onerror = this.onError
3复制代码
window.addEventListener('error')
这种可以比 window.onerror
多捕获资源记载异常. 请注意最后一个参数是 true
, false
的话可能就不如你期望。
当然你如果问题这第三个参数的含义,我就有点不想理你了。拜。
unhandledrejection
请注意最后一个参数是 true
。
1window.addEventListener('unhandledrejection', this.onReject, true)
2复制代码
其捕获未被捕获的Promise的异常。
XMLHttpRequest 与 fetch
XMLHttpRequest
很好处理,自己有onerror事件。 当然你99.99%也不会自己基于XMLHttpRequest
封装一个库, axios
真香,有这完毕的错误处理机制。
至于fetch
, 自己带着catch跑,不处理就是你自己的问题了。
这么多,太难了。 还好,其实有一个库 react-error-catch 是基于ErrorBoudary,error与unhandledrejection封装的一个组件。
其核心如下
1 ErrorBoundary.prototype.componentDidMount = function () {
2 // event catch
3 window.addEventListener('error', this.catchError, true);
4 // async code
5 window.addEventListener('unhandledrejection', this.catchRejectEvent, true);
6 };
7复制代码
使用:
1import ErrorCatch from 'react-error-catch'
2
3const App = () => {
4 return (
5 <ErrorCatch
6 app="react-catch"
7 user="cxyuns"
8 delay={5000}
9 max={1}
10 filters={[]}
11 onCatch={(errors) => {
12 console.log('报错咯');
13 // 上报异常信息到后端,动态创建标签方式
14 new Image().src = `http://localhost:3000/log/report?info=${JSON.stringify(errors)}`
15 }}
16 >
17 <Main />
18 </ErrorCatch>)
19}
20
21export default
22复制代码
鼓掌,鼓掌。
其实不然: 利用error捕获的错误,其最主要的是提供了错误堆栈信息,对于分析错误相当不友好,尤其打包之后。
错误那么多,我就先好好处理React里面的事件处理程序。 至于其他,待续。
事件处理程序的异常捕获
示例
我的思路原理很简单,使用decorator来重写原来的方法。
先看一下使用:
1 @methodCatch({ message: "创建订单失败", toast: true, report:true, log:true })
2 async createOrder() {
3 const data = {...};
4 const res = await createOrder();
5 if (!res || res.errCode !== 0) {
6 return Toast.error("创建订单失败");
7 }
8
9 .......
10 其他可能产生异常的代码
11 .......
12
13 Toast.success("创建订单成功");
14 }
15复制代码
注意四个参数:
- message: 出现错误时,打印的错误
- toast: 出现错误,是否Toast
- report: 出现错误,是否上报
- log: 使用使用console.error打印
可能你说,这这,消息定死,不合理啊。我要是有其他消息呢。 此时我微微一笑别急, 再看一段代码
1 @methodCatch({ message: "创建订单失败", toast: true, report:true, log:true })
2 async createOrder() {
3 const data = {...};
4 const res = await createOrder();
5 if (!res || res.errCode !== 0) {
6 return Toast.error("创建订单失败");
7 }
8
9 .......
10 其他可能产生异常的代码
11 .......
12
13 throw new CatchError("创建订单失败了,请联系管理员", {
14 toast: true,
15 report: true,
16 log: false
17 })
18
19 Toast.success("创建订单成功");
20
21 }
22复制代码
是都,没错,你可以通过抛出 自定义的CatchError
来覆盖之前的默认选项。
这个methodCatch
可以捕获,同步和异步的错误,我们来一起看看全部的代码。
类型定义
1export interface CatchOptions {
2 report?: boolean;
3 message?: string;
4 log?: boolean;
5 toast?: boolean;
6}
7
8// 这里写到 const.ts更合理
9export const DEFAULT_ERROR_CATCH_OPTIONS: CatchOptions = {
10 report: true,
11 message: "未知异常",
12 log: true,
13 toast: false
14}
15复制代码
自定义的CatchError
1import { CatchOptions, DEFAULT_ERROR_CATCH_OPTIONS } from "@typess/errorCatch";
2
3export class CatchError extends Error {
4
5 public __type__ = "__CATCH_ERROR__";
6 /**
7 * 捕捉到的错误
8 * @param message 消息
9 * @options 其他参数
10 */
11 constructor(message: string, public options: CatchOptions = DEFAULT_ERROR_CATCH_OPTIONS) {
12 super(message);
13 }
14}
15
16复制代码
装饰器
1import Toast from "@components/Toast";
2import { CatchOptions, DEFAULT_ERROR_CATCH_OPTIONS } from "@typess/errorCatch";
3import { CatchError } from "@util/error/CatchError";
4
5const W_TYPES = ["string", "object"];
6export function methodCatch(options: string | CatchOptions = DEFAULT_ERROR_CATCH_OPTIONS) {
7
8 const type = typeof options;
9
10 let opt: CatchOptions;
11
12
13 if (options == null || !W_TYPES.includes(type)) { // null 或者 不是字符串或者对象
14 opt = DEFAULT_ERROR_CATCH_OPTIONS;
15 } else if (typeof options === "string") { // 字符串
16 opt = {
17 ...DEFAULT_ERROR_CATCH_OPTIONS,
18 message: options || DEFAULT_ERROR_CATCH_OPTIONS.message,
19 }
20 } else { // 有效的对象
21 opt = { ...DEFAULT_ERROR_CATCH_OPTIONS, ...options }
22 }
23
24 return function (_target: any, _name: string, descriptor: PropertyDescriptor): any {
25
26 const oldFn = descriptor.value;
27
28 Object.defineProperty(descriptor, "value", {
29 get() {
30 async function proxy(...args: any[]) {
31 try {
32 const res = await oldFn.apply(this, args);
33 return res;
34 } catch (err) {
35 // if (err instanceof CatchError) {
36 if(err.__type__ == "__CATCH_ERROR__"){
37 err = err as CatchError;
38 const mOpt = { ...opt, ...(err.options || {}) };
39
40 if (mOpt.log) {
41 console.error("asyncMethodCatch:", mOpt.message || err.message , err);
42 }
43
44 if (mOpt.report) {
45 // TODO::
46 }
47
48 if (mOpt.toast) {
49 Toast.error(mOpt.message);
50 }
51
52 } else {
53
54 const message = err.message || opt.message;
55 console.error("asyncMethodCatch:", message, err);
56
57 if (opt.toast) {
58 Toast.error(message);
59 }
60 }
61 }
62 }
63 proxy._bound = true;
64 return proxy;
65 }
66 })
67 return descriptor;
68 }
69}
70复制代码
总结一下
- 利用装饰器重写原方法,达到捕获错误的目的
- 自定义错误类,抛出它,就能达到覆盖默认选项的目的。增加了灵活性。
1 @methodCatch({ message: "创建订单失败", toast: true, report:true, log:true })
2 async createOrder() {
3 const data = {...};
4 const res = await createOrder();
5 if (!res || res.errCode !== 0) {
6 return Toast.error("创建订单失败");
7 }
8 Toast.success("创建订单成功");
9
10 .......
11 其他可能产生异常的代码
12 .......
13
14 throw new CatchError("创建订单失败了,请联系管理员", {
15 toast: true,
16 report: true,
17 log: false
18 })
19 }
20复制代码
下一步
啥下一步,走一步看一步啦。
不,接下来的路,还很长。 这才是一个基础版本。
- 扩大成果,支持更多类型,以及hooks版本。
1 @XXXCatch
2classs AAA{
3 @YYYCatch
4 method = ()=> {
5 }
6}
7复制代码
- 抽象,再抽象,再抽象
玩笑开完了,严肃一下:
当前方案存在的问题:
- 功能局限
- 抽象不够 获取选项,代理函数, 错误处理函数完全可以分离,变成通用方法。
- 同步方法经过转换后会变为异步方法。 所以理论上,要区分同步和异步方案。
- 错误处理函数再异常怎么办
之后,我们会围绕着这些问题,继续展开。
Hooks版本
有掘友说,这个年代了,谁还不用Hooks。 是的,大佬们说得对,我们得与时俱进。 Hooks的基础版本已经有了,先分享使用,后续的文章跟上。
Hook的名字就叫useCatch
1 const TestView: React.FC<Props> = function (props) {
2
3 const [count, setCount] = useState(0);
4
5
6 const doSomething = useCatch(async function(){
7 console.log("doSomething: begin");
8 throw new CatchError("doSomething error")
9 console.log("doSomething: end");
10 }, [], {
11 toast: true
12 })
13
14 const onClick = useCatch(async (ev) => {
15 console.log(ev.target);
16 setCount(count + 1);
17
18 doSomething();
19
20 const d = delay(3000, () => {
21 setCount(count => count + 1);
22 console.log()
23 });
24 console.log("delay begin:", Date.now())
25
26 await d.run();
27
28 console.log("delay end:", Date.now())
29 console.log("TestView", this)
30 throw new CatchError("自定义的异常,你知道不")
31 },
32 [count],
33 {
34 message: "I am so sorry",
35 toast: true
36 });
37
38 return <div>
39 <div><button onClick={onClick}>点我</button></div>
40 <div>{count}</div>
41 </div>
42}
43
44export default React.memo(TestView);
45复制代码
至于思路,基于useMemo
,可以先看一下代码:
1export function useCatch<T extends (...args: any[]) => any>(callback: T, deps: DependencyList, options: CatchOptions =DEFAULT_ERRPR_CATCH_OPTIONS): T {
2
3 const opt = useMemo( ()=> getOptions(options), [options]);
4
5 const fn = useMemo((..._args: any[]) => {
6 const proxy = observerHandler(callback, undefined, function (error: Error) {
7 commonErrorHandler(error, opt)
8 });
9 return proxy;
10
11 }, [callback, deps, opt]) as T;
12
13 return fn;
14}