如何使秋天变得柔和?

我没有找到有关React应用程序中错误处理的全面指南,因此我决定分享本文中获得的经验。 本文适用于初学者,并且可以作为系统化应用程序中错误处理的起点。
问题和目标设定
星期一早上,您冷静地喝咖啡,并吹嘘自己已修正了比上周更多的错误,然后经理运行并挥舞着他的手-“我们跌倒了,一切都很难过,我们正在亏钱”。 您运行并打开Mac,转到SPA的生产版本,单击几次以播放bug,看到白屏,只有全能者知道那里发生了什么,爬到控制台中,开始挖掘,在组件内部有一个名称为b的组件,错误无法读取未定义的属性getId。 经过N个小时的研究,您急匆匆地哭诉着要发布此修补程序。 这样的袭击以一定的频率发生并且已经成为常态,但是如果我说一切都可以不同怎么办? 如何减少调试错误的时间并构建过程,以使客户端在开发过程中几乎不会注意到不可避免的错误计算?
让我们按顺序检查我们遇到的问题:- 即使该错误无关紧要或在模块内定位,在任何情况下,整个应用程序都无法运行
在React版本16之前,开发人员没有单一的标准错误捕获机制,并且在某些情况下,数据损坏导致仅在后续步骤中呈现下降或奇怪的应用程序行为。 每个开发人员都会处理错误,因为他已经习惯了它,并且带有try / catch的命令式模型通常与React的声明性原则不太匹配。 在版本16中,出现了“错误边界”工具,该工具试图解决这些问题,我们将考虑如何应用它。 - 该错误仅在生产环境中复制,或者在没有其他数据的情况下无法复制。
在理想的世界中,开发环境与生产环境相同,我们可以在本地重现任何错误,但我们生活在现实世界中。 战斗系统上没有调试工具。 很难发掘此类事件,而且生产效率不高,基本上,您必须处理混乱的代码和缺少有关错误的信息,而不是问题的本质。 我们不会考虑如何将开发环境的条件与生产条件相近似的问题,但是,我们将考虑允许您获取有关已发生事件的详细信息的工具。
所有这些降低了软件产品的开发速度和用户忠诚度,因此我为自己设定了三个最重要的目标:
- 出现错误时改善应用程序的用户体验;
- 减少从生产错误到发现错误的时间;
- 为开发人员加快在应用程序中查找和调试问题的过程。
需要解决什么任务?- 使用错误边界处理严重错误
为了改善应用程序的用户体验,我们必须拦截并处理关键的UI错误。 如果应用程序由独立的组件组成,则这种策略将允许用户使用系统的其余部分。 如果可能,我们还可以尝试采取措施在崩溃后还原应用程序。
- 保存扩展错误信息
如果发生错误,请将调试信息发送到监视服务器,监视服务器将过滤,存储和显示有关事件的信息。 这将帮助我们在部署后快速检测并轻松调试错误。
严重错误处理从版本16开始,React更改了标准错误处理行为。 现在,使用错误边界未捕获到的异常将导致卸载整个React树,并因此导致整个应用程序的不可操作性。 事实是,最好不要展示任何东西,而不是让用户有机会获得无法预测的结果,这一事实证明了这一决定。 您可以在
官方的React文档中阅读更多
内容 。

此外,许多人都被注释所迷惑,即错误边界不会捕获事件处理程序和异步代码中的错误,但是如果您考虑一下,则任何处理程序最终都可以更改状态,根据该状态将调用新的呈现周期,最终帐户可能会导致UI代码错误。 否则,这对于UI来说不是关键错误,可以在处理程序内部以特定方式进行处理。
从我们的角度来看,一个严重错误是UI代码内部发生的异常,如果不对其进行处理,则会卸载整个React树。 其余错误并不重要,可以根据应用程序逻辑(例如使用通知)进行处理。
在本文中,我们将重点放在处理关键错误上,尽管在最坏的情况下非关键错误也会导致接口无法操作。 它们的处理很难分为一个公共块,每种情况都需要根据应用程序逻辑做出决定。
通常,非严重错误可能非常严重(例如双关语),因此有关错误的信息的记录方式应与严重错误相同。
现在,我们正在为简单的应用程序设计错误边界,它由导航栏,标题和主工作区组成。 它非常简单,仅关注于错误处理,但是对于许多应用程序却具有典型的结构。

我们有一个包含3个链接的导航面板,每个链接都导致彼此独立的组件,因此我们想要实现一种行为,即使其中一个组件不起作用,我们也可以继续使用其余的组件。
结果,对于每个可通过导航菜单访问的组件,我们将具有ErrorBoundary,并且在标题组件,导航面板或ErrorBoundary内部发生错误的情况下,该通用的ErrorBoundary会通知整个应用程序崩溃,但我们并未解决处理并进一步丢弃。
考虑列出包裹在ErrorBoundary中的整个应用程序
const AppWithBoundary = () => ( <ErrorBoundary errorMessage="Application has crashed"> <App/> </ErrorBoundary> )
function App() { return ( <Router> <Layout> <Sider width={200}> <SideNavigation /> </Sider> <Layout> <Header> <ActionPanel /> </Header> <Content> <Switch> <Route path="/link1"> <Page1 title="Link 1 content page" errorMessage="Page for link 1 crashed" /> </Route> <Route path="/link2"> <Page2 title="Link 2 content page" errorMessage="Page for link 2 crashed" /> </Route> <Route path="/link3"> <Page3 title="Link 3 content page" errorMessage="Page for link 3 crashed" /> </Route> <Route path="/"> <MainPage title="Main page" errorMessage="Only main page crashed" /> </Route> </Switch> </Content> </Layout> </Layout> </Router> ); }
ErrorBoundary中没有魔术,它只是在其中定义componentDidCatch方法的类组件,也就是说,如果在其中定义了此方法,则可以使任何组件成为ErrorBoundary。
class ErrorBoundary extends React.Component { state = { hasError: false, } componentDidCatch(error) {
这是Page组件的ErrorBoundary外观,它将呈现到Content块中:
const PageBody = ({ title }) => ( <Content title={title}> <Empty className="content-empty" /> </Content> ); const MainPage = ({ errorMessage, title }) => ( <ErrorBoundary errorMessage={errorMessage}> <PageBody title={title} /> </ErrorBoundary>
由于ErrorBoundary是常规的React组件,我们可以使用相同的ErrorBoundary组件将每个页面包装在其自己的处理程序中,只需将不同的参数传递给ErrorBoundary,因为这些是类的不同实例,因此它们的状态不会相互依赖。
重要说明:ErrorBoundary只能在树中位于其下方的组件中捕获错误。在下面的清单中,该错误不会被本地ErrorBoundary拦截,但是将被树上方的处理程序抛出和拦截:
const Page = ({ errorMessage }) => ( <ErrorBoundary errorMessage={errorMessage}> {null.toString()} </ErrorBoundary> );
这里的错误是由本地的ErrorBoundary捕获的:
const ErrorProneComponent = () => null.toString(); const Page = ({ errorMessage }) => ( <ErrorBoundary errorMessage={errorMessage}> <ErrorProneComponent /> </ErrorBoundary> );
将每个单独的组件包装在我们的ErrorBoundary中,我们实现了必要的行为,使用link3将故意错误的代码放入该组件中,然后看看会发生什么。 我们故意忘记传递步骤参数:
const PageBody = ({ title, steps }) => ( <Content title={title}> <Steps current={2} direction="vertical"> {steps.map(({ title, description }) => (<Step title={title} description={description} />))} </Steps> </Content> ); const Page = ({ errorMessage, title }) => ( <ErrorBoundary errorMessage={errorMessage}> <PageBody title={title} /> </ErrorBoundary> );

该应用程序将通知我们发生了一个错误,但是它不会完全消失,我们可以浏览导航菜单并与其他部分一起工作。

这种简单的配置使我们可以轻松实现目标,但实际上,很少有人会非常注意错误处理,仅计划应用程序的定期执行。
保存错误信息既然我们已经在应用程序中放置了足够的ErrorBoundary,则有必要保存有关错误的信息,以便尽快检测和更正错误。 最简单的方法是使用SaaS服务,例如Sentry或Rollbar。 它们具有非常相似的功能,因此您可以使用任何错误监视服务。
我将在Sentry上展示一个基本示例,因为在短短的一分钟内您将获得最少的功能。 同时,Sentry本身会捕获异常,甚至修改console.log以获取所有错误信息。 之后,将在应用程序中发生的所有错误发送并存储在服务器上。 Sentry具有过滤事件,混淆个人数据,链接到发布等的机制。 我们将仅考虑基本的集成方案。
要进行连接,您必须在其官方网站上注册并阅读快速入门指南,该指南将在注册后立即为您提供指导。
在我们的应用程序中,我们只添加了几行,一切就开始了。
import * as Sentry from '@sentry/browser'; Sentry.init({dsn: “https:
再次,单击应用程序中的link / link3并显示错误屏幕,然后转到哨兵界面,这显然是发生了事件并且内部失败。

错误会根据类型,发生的频率和时间自动分组;可以应用各种过滤器。 我们有一个事件-进入该事件,然后在下一个屏幕上看到一堆有用的信息,例如堆栈跟踪

以及错误之前的最后一个用户操作(面包屑)。

即使配置如此简单,我们也可以累积并分析错误信息,并将其用于进一步的调试。 在此示例中,在开发模式下从客户端发送了一个错误,因此我们可以观察到有关组件和错误的完整信息。 为了从生产模式中获取类似的信息,您还必须使用Sentry配置发布数据的同步,Sentry将在其自身中存储源映射,从而使您可以保存足够的信息而不会增加包的大小。 我们不会在本文的框架中考虑这种配置,但是在实现之后,我将在另一篇文章中尝试讨论这种解决方案的陷阱。
结果:使用ErrorBoundary进行错误处理可以使我们在应用程序部分崩溃的情况下得以解决,从而增加了系统的用户体验,并使用了专用的错误监视系统来减少检测和调试问题的时间。
仔细考虑用于处理和监视应用程序错误的策略,将来这将为您节省大量时间和精力。
一个经过深思熟虑的策略将首先改善处理事件的过程,然后才影响代码的结构。PS您可以尝试使用各种ErrorBoundary配置选项,也可以在feature_sentry分支中将Sentry自己连接到应用程序,用在网站上注册期间获得的密钥替换密钥。
Git-hub演示应用程序React的官方错误边界文档