【React错误处理】超全指南来了
一、为何报错会导致渲染异常?
在React中,未捕获错误会导致DOM被卸载, 浏览器无法渲染。 为何React选择完全移除错误的DOM呢,我们可以看看官网中的这段话:
未捕获错误(Uncaught Errors)的新行为
这一改变具有重要意义,自 React 16 起,任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载。
我们对这一决定有过一些争论,但根据我们的经验,把一个错误的 UI 留在那比完全移除它要更糟糕。例如,在类似 Messenger 的产品中,把一个异常的 UI 展示给用户可能会导致用户将信息错发给别人。同样,对于支付类应用而言,显示错误的金额也比不呈现任何内容更糟糕。
从我的开发经验看来,出现bug的原因主要有以下两点:
① 后端返回数据异常,前端代码未兼容完全;
② 前端程序逻辑错误;
如果项目上线后,页面无法正常打开,无法执行其他操作甚至一片空白,用户的体验感是非常不好的。
因此,我们有必要采取一些措施来预防和处理异常/错误,避免整个页面崩溃。
二、解决方案:防bug+补救bug
(1) “防bug于未然”: 对后端数据进行预处理
正常情况下,前端小伙伴与后端提前沟通好状态码和数据结构,根据状态码做出不同响应即可。但是,当后端数据异常(如返回undefined, null)时,前端直接调用数组的某些方法或者对象的某些属性时就会报错。
- 前端小伙伴谨记, "不要完全相信后端的数据"。*
在使用后端数据前,最好先赋默认值。
举个🌰:
// ① 解构时赋默认值 (注意:arr为null时,无法赋值成功)
// ② 使用逻辑或
const {arr = []} = data || {};
// ③ 使用可选运算符
const names = _arr?.map((item = {})=>(item?.name))).filter(Boolean);
...
在复杂的场景下,你甚至可以做更多——例如,先将后台数据进行预处理(与业务逻辑无关的数据处理),转为自身需要的结构和类型,让业务组件/逻辑更加纯粹地处理业务的同时,减少bug出现的概率。
(当然,有很多bug是前端代码自身的问题,在此不赘述预防措施了,大家可以自行思考🤔。)
然而人无完人,bug总是防不胜防,那么如何减小bug的影响呢?
(2) “亡羊补牢”之 使用常规手段捕获异常;
对于javascript而言,执行的事件主要有以下五种:同步方法、异步方法、资源加载、Promise、async...await,事件执行失败意味着程序出现bug。
幸运的是,这些异常均可通过框架(react/vue/angular等)之外的常规手段捕获到。
方法汇总
异常类型 | 同步方法 | 异步方法 | 资源加载 | Promise | async...await |
---|---|---|---|---|---|
try...catch | √ | √ | |||
window.onerror | √ | √ | |||
addEventListener('error') | √ | √ | √ | ||
addEventListener(“unhandledrejection”) | √ | √ |
1. try...catch
try...catch
语句标记要尝试的语句块,并指定一个出现异常时抛出的响应。
举些🌰:
① 处理同步错误;
- MDN Web Docs中的例子:
try {
nonExistentFunction();
} catch (error) {
console.error(error);
// Expected output: ReferenceError: nonExistentFunction is not defined
// (Note: the exact output may be browser-dependent)
}
② 处理异步错误
通常,若 try 中的异步模块产生了错误,catch 是捕获不到的。但是我们可以把 try-catch 放到异步代码中。
- 将try-catch放到setTimeout内部
setTimeout(() => {
try {
throw new Error('error in setTimeout');
} catch (err) {
console.error('catch error', err);
}
}, 200);
- 将try-catch放到then内部
Promise.resolve().then(() => {
try {
throw new Error('error in Promise.then');
} catch (err) {
console.error('catch error', err);
}
});
// 正常情况下,使用Promse自带的catch捕获异常即可
Promise.resolve()
.then(() => {
throw new Error('error in Promise.then');
})
.catch((err) => {
console.error('Promise.catch error', err);
});
③ 处理 async-await 的异常
- 将try放在async之后
const request = async () => {
try {
const { code, data } = await somethingThatReturnsAPromise();
} catch (err) {
console.error('request error', err);
}
};
2. window.onerror
当JavaScript运行时错误(包括语法错误)发生时,会执行window.onerror方法。
function onError (msg, url, lineNo, columnNo, error) {
/ *
* message:错误信息(字符串)。可用于HTML onerror=""处理程序中的event。
* source:发生错误的脚本URL(字符串)
* lineno:发生错误的行号(数字)
* colno:发生错误的列号(数字)
* error:Error对象
*/
// 没有返回值或者返回值为false的时候,异常信息会通过 console.error 的方式在控制台打印
return false;
}
window.onerror = onError
3. addEventListener('error')
当资源加载失败或无法使用时,会在Window
对象触发error
事件。例如:script 执行时报错。
window.addEventListener('error', (event) => {
console.log('捕获到异常', event);
return false;
},true); // 捕获阶段
4. addEventListener('unhandledrejection')
当 Promise
被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection
事件。
// 捕获未处理的 promise 异常
window.addEventListener("unhandledrejection", event => {
console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});
小结
① try...catch最为灵活,通过使用一些小技巧,可以捕获绝大部分异常,捕获到错误后可以在catch中进行更多处理;
② addEventListener('error') 事件监听 js 运行时错误事件,会比 window.onerror 先触发,与onerror的功能大体类似,但可以全局捕获资源加载异常的错误;
③ addEventListener('error')结合addEventListener('unhandledrejection'),几乎可以捕获程序中的所有错误,但主要只是提供了错误堆栈信息;
④ 当使用以上四种常规手段捕获到错误后,我们可以做许多事情。例如,在开发环境中,可将错误信息打印在浏览器控制台上、可抛出异常通知下游, 方便开发调试;在生产环境中,可上报错误日志进行错误监控,而在修复bug的过程中,我们可以做更多——结合错误边界(Error Boundary)为用户渲染一些有用的内容。
(3) “亡羊补牢”之 使用Error Boundary;
上文提到,try...catch特别好用,但是它无法直接捕获到react组件中的所有可能发生的错误,如子组件中的错误。
此时,Error Boundary就必须得闪亮登场了~
1.Error Boundary概念
错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树。
错误边界最基本的实现:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) { // 更新 state 使下一次渲染能够显示降级后的 UI return { hasError: true }; }
componentDidCatch(error, errorInfo) { // 你同样可以将错误日志上报给服务器 logErrorToMyService(error, errorInfo); }
render() {
if (this.state.hasError) { // 你可以自定义降级后的 UI 并渲染 return <h1>Something went wrong.</h1>; }
return this.props.children;
}
}
使用
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
tips, 日常开发中,我们需要为不同粒度的组件运用错误边界组件,尽量减小bug的影响范围。
2.Error Boundary可用场景和不可用场景
① 错误边界起作用的场景:
- 发生在整个子组件树的渲染期间、生命周期方法以及构造函数中的错误。
② 错误边界不起作用的场景:
- 组件外的报错、异步代码的报错、事件函数中的报错、错误边界自身抛出的错误、错误边界的父组件报错、 函数组件被卸载,触发 useEffect 的销毁。
3、怎么让errorBoundary处理在生命周期之外的错误?
机智的小伙伴会发现,错误边界不能处理的许多错误,比如promise、异步代码、各种回调和事件处理程序中的错误,可以使用常规 try...catch来处理。
因此,我们先用try...catch捕获这些错误,然后在catch 语句内触发正常的 React 重新渲染,然后将这些错误重新抛出到重新渲染生命周期中。
- ① 定义异步错误抛出工具:
// 定义
import { useState } from 'react';
const useThrowAsyncError = () => {
const [, setState] = useState();
return (e: any) => {
setState(() => { throw e })
}
};
export default useThrowAsyncError;
// 使用示例
const Component = () => {
const throwAsyncError = useThrowAsyncError();
useEffect(() => {
fetch('/bla').then().catch((e) => {
// throw async error here!
throwAsyncError(e)
})
})
}
- ② 为回调函数做额外处理:
// 定义
import { useState } from 'react';
const useCallbackWithErrorHandler = (callback: (...args: any[]) => any, useErrorBoundary: boolean = false) => {
const [, setState] = useState();
return async (...args: any[]) => {
try {
await callback(...args);
} catch (e) {
useErrorBoundary && setState(() => { throw e });
}
}
};
export default useCallbackWithErrorHandler;
// 使用示例
const Component = () => {
const onClick = () => {
// do something dangerous here
}
const onClickWithErrorHandler = useCallbackWithErrorHandler(onClick);
return <button onClick={onClickWithErrorHandler}>click me!</button>
}
三、总结
希望看到这里的小伙伴,可以从容而优雅地处理程序中出现的bug。
本文就错误处理做了详细的解析,主要内容如下:
-
① 必要性:因为未被错误边界捕获的异常会导致整个react组件树被卸载,微不足道的错误都有可能导致整个页面受到破坏,并为用户渲染出一个白屏,所以预防并处理异常是必要的。
-
② 预防措施:尽量减少bug出现的概率,除了减少前端自身代码的问题,最好对后端数据进行预处理再使用;
-
③ 事后补救方案1:使用常规手段(如try...catch、window.onerror、addEventListener('error')、addEventListener(“unhandledrejection”))捕获异常并做进一步处理,如错误上报、更新状态渲染降级UI等;
-
④ 事后补救方案2:结合Error Boundary为用户渲染有用的信息,避免白屏,提升用户体验感。
四、参考与感谢
转载自:https://juejin.cn/post/7225076114561253413