找回密码
 会员注册
查看: 20|回复: 0

如何在react中处理报错

[复制链接]

5

主题

0

回帖

16

积分

新手上路

积分
16
发表于 2024-10-11 23:57:26 | 显示全部楼层 |阅读模式
本文为 360 奇舞团前端工程师翻译原文地址:https://www.developerway.com/posts/how-to-handle-errors-in-react我们都希望我们的应用能稳定、完美运行,并且能够考虑到每一个边缘情况。但是现实情况是,我们是人,是人就会犯错,并且也不存在没有bug的代码。无论我们多么小心或者编写了多少自动化测试,总会有出现严重错误的情况。重要的是,当错误影响到用户体验时,尽可能地定位它,并能以优雅的方式处理它,直到它真正被修复。所以今天,让我们来看看React中的错误处理:如果发生错误,我们可以做什么,不同的错误捕捉方法的注意事项是什么,以及如何减小错误的影响。为何要捕获react中的错误那么第一件事:为什么在React中拥有一些错误捕获解决方案是极其重要的?这个答案很简单:从16版开始,在React生命周期中抛出的错误,如果不停止的话,将导致整个应用自行卸载。在此之前,组件会被保留在屏幕上,即使是样式错误和交互错误的。现在,在UI的一些无关紧要的部分,甚至是一些你无法控制的外部库中,一个未被捕获的错误也可以使整个页面挂掉,给用户呈现白屏。在此之前,前端开发人员从来没有过这样的破坏力。还记得在js中是如何捕获错误信息的吗?在javascript里如何捕获错误?众所周知,我们可以使用try/catch语句:在try里做一些事情,在它们执行失败的时候catch这些错误来减少影响try{// if we're doing something wrong, this might throw an errordoSomething();}catch(e){//if error happened, catch it and do something with it without stopping the app//like sending this error to some logging service}相同的语法也适用于async函数try{awaitfetch('/bla-bla');}catch(e){// oh no, the fetch failed! We should do something about it!}如果我们正在使用旧的promises规范,它有专门的方法来捕获错误。我们可以基于promise的API来重写fetch例子,像下面这样:fetch('/bla-bla').then((result)=>{//if a promise is successful, the result will be here//we can do something useful with it}).catch((e)=>{//oh no, the fetch failed! We should do something about it!})以上两种是相同的概念,只是实现方式稍有不同,因此在接下来的文章中,我将只对try/catch错误使用语法。在React中的try/catch:如何操作和注意事项当一个错误被捕获时,我们需要对它做些什么?除了把它记录在某个地方之外,我们还能做什么?更准确地说:我们能为我们的用户做什么?仅仅让他们面对一个空屏幕或者一个不友好的界面。最明显和最直观的答案是在等待我们修复的时候渲染一些东西。幸运的是,我们可以在这个catch语句中做任何我们想做的事情,包括设置状态。所以我们可以做一些事情像这样:constSomeComponent=()=>{const[hasError,setHasError]=useState(false);useEffect(()=>{try{}catch(e){setHasError(true);}})if(hasError)returnreturn}我们试图发送一个获取请求,如果请求失败了--设置错误状态,如果错误状态为真,那么我们就渲染一个错误反馈的UI,为用户提供一些额外的信息,比如支持联系号码。这种方法非常简单,非常适合简单、可预测且范围狭窄的用例,例如捕获失败的fetch请求。但是如果你想捕捉一个组件中可能发生的所有错误,你将面临一些挑战和严格的限制。限制1:你会在使用useEffect钩子时遇到困难如果我们用try/catch包住useEffect,hook就失效了。try{useEffect(()=>{thrownewError('Hulksmash!');},[])}catch(e){//useEffectthrows,butthiswillneverbecalled}发生这种情况是因为useEffect是在渲染后被异步调用的,所以从try/catch的角度来看,一切都很顺利。这和任何Promise都是一样的:如果我们不等待结果,那么javascript就会继续它的工作,在承诺完成后返回,并且只执行useEffect(或Promise)中的内容。为了让在useEffect中的错误被捕获,try/catch应该被放在里面。useEffect(()=>{try{thrownewError('Hulksmash!');}catch(e){//thisonewillbecaught}},[])看一下这个例子就知道了:https://codesandbox.io/s/try-catch-and-useeffect-28h3uxfrom-embed这适用于所有使用useEffect的钩子或异步事情的场景。因此,你不能用一个try/catch包裹所有代码,而是将其拆分到每个hook中限制2:子组件。try/catch不能捕捉子组件内发生的任何事情。你不能像下面这样做:constComponent=()=>{letchild;try{child=}catch(e){//uselessforcatchingerrorsinsideChildcomponent,won'tbetriggered}returnchild;}甚至是这样constComponent=()=>{try{return}catch(e){//stilluselessforcatchingerrorsinsideChildcomponent,won'tbetriggered}}可以看一下这个例子https://codesandbox.io/s/try-catch-for-children-doesnt-work-5elto1from-embed发生这种情况是因为当我们写时,我们实际上并没有渲染这个组件。我们所做的是创建一个组件元素,这只是一个组件的定义。它只是一个包含必要信息的对象,比如组件类型和道具,以后会被React本身使用,它将实际触发这个组件的渲染。它将在try/catch块成功执行后发生,与promises和useEffect钩子的情况完全一样。如果你想更详细地了解元素和组件的工作原理,这里有一篇文章适合你:React元素、子代、父代和重排的奥秘(https://www.developerway.com/posts/react-elements-children-parents)限制3:在渲染过程中设置state是不可取的如果你想在useEffect和各种回调之外捕获错误(也就是说在组件的渲染过程中),那么正确处理它们就不再简单了,因为渲染过程中的状态更新是允许的。比如像这样简单的的代码,如果发生错误,就会导致重新渲染无限循环。constComponent=()=>{const[hasError,setHasError]=useState(false);try{doSomethingComplicated();}catch(e){setHasError(true);}}当然,我们可以在这里直接返回错误组件,而不是设置错误状态。constComponent=()=>{try{doSomethingComplicated();}catch(e){return}}但是,正如你想的,这有点麻烦,而且会迫使我们对同一组件的错误进行不同的处理:对useEffect和回调进行状态处理,而对其他的直接返回错误组件constSomeComponent=()=>{const[hasError,setHasError]=useState(false);useEffect(()=>{try{//dosomethinglikefetchingsomedata}catch(e){setHasError(true);}})try{//}catch(e){return;}if(hasError)returnreturn}总结一下本节的内容:如果在React中仅仅依靠try/catch,要么会错过大部分的错误,要么会把每个组件变成难以理解的混乱代码而造成错误幸运的是,还有其他方法。React ErrorBoundary component为了减轻上面的限制,React给我们提供了“错误边界”:一种特殊的API,它以某种方式将普通组件转换为 try/catch 语句,但是仅适用于 React 声明的代码。你可以在下面的示例中看到的经典用法,包括 React 文档。constComponent=()=>{return()}现在,如果这些组件或者他们的子组件在渲染中出现错误,这个错误会被捕获并处理。 但是React并没有提供原生组件,只是给我们提供了一个工具来实现它。最简单的实现是这样子的:classErrorBoundaryextendsReact.Component{constructor(props){super(props);//初始化error的状态this.state={hasError:false};}staticgetDerivedStateFromError(error){return{hasError:true};}render(){if(this.state.hasError){returnOhno!Epicfail!}returnthis.props.children;}}我们创建了一个普通的类组件,并实现了getDerivedStateFromError方法--这个方法可以让组件拥有错误边界。在处理错误时,另一个重要的事情是将错误信息传递到某个地方,让它能够触发所有监听者。为此,错误边界提供了componentDidCatch方法classErrorBoundaryextendsReact.Component{componentDidCatch(error,errorInfo){log(error,errorInfo);}}在错误边界设置后,我们可以像使用其他组件一样使用它。比如,我们可以将其优化得更便于重用,并将fallback做为props来传递render(){if(this.state.hasError){returnthis.props.fallback;}returnthis.props.children;}可以像下面这样使用:constComponent=()=>{return(Ohno!Dosomething!}>)}或者其他我们可能需要的东西,比如点击按钮时重置状态,区分错误类型,或者将错误传递到某个上下文中。查看完整的例子:https://4ldsun.csb.app/不过,这里有一个注意事项:它并不能捕获一切错误错误边界组件的限制错误边界只捕捉发生在React生命周期中的错误。在生命周期之外发生的事情,如resolved promise、带有setTimeout的异步代码、各种回调和事件监听函数,如果没有被不明确处理,将不能捕获。constComponent=()=>{useEffect(()=>{thrownewError('Destroyeverything!');},[])constonClick=()=>{thrownewError('Hulksmash!');}useEffect(()=>{fetch('/bla')},[])returnclickme}constComponentWithBoundary=()=>{return()}这里的建议是使用常规的try/catch来处理这类错误。而且至少在这里我们可以安全地使用state:事件监听函数的回调正是我们通常setState的地方。所以从技术上讲,我们可以把两种方法结合起来,做这样的事情。constComponent=()=>{const[hasError,setHasError]=useState(false);constonClick=()=>{try{thrownewError('Hulksmash!');}catch(e){setHasError(true);}}if(hasError)return'somethingwentwrong';returnclickme}constComponentWithBoundary=()=>{return()}但是。我们又回到了原点:每个组件都需要维持它的 "错误 "状态,更重要的是--决定如何处理它。当然,我们可以不在组件层面上处理这些错误,而只是通过props或Context将它们传递到拥有ErrorBoundary的父级。这样的话,我们只需要在一个地方设置一个 "fallback"组件。constComponent=({onError})=>{constonClick=()=>{try{thrownewError('Hulksmash!');}catch(e){onError();}}returnclickme}constComponentWithBoundary=()=>{const[hasError,setHasError]=useState();constfallback="Ohno!Somethingwentwrong";if(hasError)returnfallback;return(setHasError(true)}/>)}但这里有很多冗余代码,我们必须对渲染树的每一个子组件都这样做。更不用说我们现在还要维护两个错误状态:父组件,以及ErrorBoundary本身。而ErrorBoundary已经实现了一套捕获错误的机制,我们在这里做了重复的工作。那么,我们就不能用ErrorBoundary从异步代码和事件处理程序中捕捉这些错误吗?ErrorBoundary捕捉异步错误有趣的是--我们可以用ErrorBoundary把它们都捕获!大家最喜欢的Dan Abramov与我们分享了一个很酷的黑客技术。正是为了实现这一点:Throwing Error from hook not caught in error boundary · Issue #14981 · facebook/react. (https://github.com/facebook/react/issues/14981#issuecomment-468460187)这里的技巧是先用try/catch捕捉这些错误,然后在catch语句中触发正常的React重渲染,然后把这些错误重新抛回重渲染的生命周期。这样,ErrorBoundary就可以像捕获其他错误一样捕捉它们。由于state变化是触发重新渲染的方式,而setState实际上可以接受一个更新函数作为参数,这个解决方案是纯粹的黑魔法。constComponent=()=>{const[state,setState]=useState();constonClick=()=>{try{//somethingbadhappened}catch(e){setState(()=>{throwe;})}}}完整例子在这里:https://codesandbox.io/s/simple-async-error-in-error-boundary-r8l22gfrom-embed这里的最后一步将其抽象化,所以我们不必在每个组件中创建随机状态。我们可以在这里发挥创意,实现一个钩子用来将异步错误抛出。constuseThrowAsyncError=()=>{const[state,setState]=useState();return(error)=>{setState(()=>throwerror)}}像这样使用它:constComponent=()=>{constthrowAsyncError=useThrowAsyncError();useEffect(()=>{fetch('/bla').then().catch((e)=>{//throwasyncerrorhere!throwAsyncError(e)})})}或者,我们可以像下面这样为回调做一层封装:constuseCallbackWithErrorHandling=(callback)=>{const[state,setState]=useState();return(...args)=>{try{callback(...args);}catch(e){setState(()=>throwe);}}}像下面这样使用它:constComponent=()=>{constonClick=()=>{//dosomethingdangeroushere}constonClickWithErrorHandler=useCallbackWithErrorHandling(onClick);returnclickme!}完整的例子在这里:https://codesandbox.io/s/simple-async-errors-utils-for-error-boundary-fzg5zvfrom-embed可以用 react-error-boundary 来代替吗?对于那些讨厌重新造轮子的人,或者喜欢用库来解决已经解决的问题的人,有一个很好的库,它实现了一个灵活的ErrorBoundary组件,并且有一些类似于上述的有用的工具:https://github.com/bvaughn/react-error-boundary是否使用它,只是个人喜好、编码风格和组件特殊性的问题。今天就说到这里,希望从现在开始,当你的应用程序发生了报错,你都能够轻松而优雅地处理这些情况。请记住:try/catch块不会捕获像useEffect这样的钩子和任何子组件中的错误。ErrorBoundary可以捕捉它们,但它不会捕捉异步代码和事件处理回调中的错误。不过,你可以让ErrorBoundary捕捉这些错误,你只需要先用try/catch捕捉它们,然后再把它们重新传递到React生命周期中。-END-关于奇舞团奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-27 00:45 , Processed in 0.562729 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表