使用 tried-and-true 调试策略解决一个奇怪的 Bug

Avatar of Adrian Bece
Adrian Bece

DigitalOcean 为您的旅程的每个阶段提供云产品。立即开始使用 $200 免费信用额度!

还记得上次你遇到一个让你挠头几个小时的 UI 相关 Bug 吗?也许问题是随机发生的,或者是在特定情况下发生的(设备、操作系统、浏览器、用户操作),或者只是隐藏在项目中众多前端技术之一中?

我最近再次意识到 UI Bug 可能会多么复杂。我最近修复了一个有趣的 Bug,它影响了 Safari 浏览器中的某些 SVG,没有明显的模式或原因。我搜索了任何类似的问题以获得一些线索,但我没有找到有用的结果。尽管有障碍,我还是设法修复了它。

我使用了一些有用的调试策略来分析问题,这些策略我也将在本文中介绍。提交修复后,我想起了 Chris 一段时间前发布的一条推文。

…我们现在就在这里。

问题所在

我在我一直在进行的一个项目上发现了以下错误。这是在实时网站上。

我通过任何绘制事件来重现此问题,例如调整屏幕大小。

我创建了一个 CodePen 示例 来演示此问题,以便您可以自己查看。如果我们在 Safari 中打开此示例,按钮在加载时可能会按预期显示。但如果我们点击前两个较大的按钮,问题就会出现。

每当浏览器绘制事件发生时,较大按钮中的 SVG 就会渲染不正确。它只是被截断了。它可能在加载时随机发生。它甚至可能在调整屏幕大小时发生。无论是什么情况,它都会发生!

哎呀,为什么 SVG 图标被截断了?

以下是我解决问题的方法。

首先,让我们考虑一下环境

始终是一个好主意,要回顾一下项目的详细信息,以了解环境和存在 Bug 的条件。

  • 这个特定的项目正在使用 React(但这对于阅读本文不是必需的)。
  • SVG 作为 React 组件导入,并通过 webpack 内联到 HTML 中。
  • SVG 已从设计工具导出,并且没有语法错误。
  • SVG 应用了一些来自样式表的 CSS。
  • 受影响的 SVG 位于 <button> HTML 元素内部。
  • 该问题仅在 Safari 中出现(并在版本 13 上被发现)。

掉进兔子洞

让我们看一下问题,看看我们是否可以对正在发生的事情做出一些假设。像这样的 Bug 会变得很复杂,我们不会立即知道发生了什么。我们不必在第一次尝试中 100% 正确,因为我们将一步一步地进行,并形成可以测试的假设,以缩小可能的成因。

形成假设

乍一看,这似乎是一个 CSS 问题。某些样式可能应用于悬停事件,从而破坏了布局或 SVG 图形的溢出属性。它看起来也像是随机发生的,无论何时 Safari 渲染页面(调整屏幕大小、悬停、点击等时的绘制事件)。

让我们从最简单、最明显的路线开始,并假设 CSS 是问题的原因。我们可以考虑 Safari 浏览器中存在一个 Bug 的可能性,该 Bug 导致 SVG 在某些特定样式应用于 SVG 元素时渲染不正确,例如 Flex 布局。

通过这样做,我们形成了一个假设。我们的下一步是设置一个测试,该测试将确认或反驳假设。每个测试结果都将产生关于 Bug 的新事实,并有助于形成进一步的假设。

问题简化

我们将使用一种称为问题简化的调试策略来尝试查明问题所在。康奈尔大学的 CS 讲座 将此策略描述为“一种逐渐消除与 Bug 无关的代码部分的方法”。

通过假设问题在于 CSS 中,我们可以最终查明问题所在,或者从等式中消除 CSS,从而减少了可能的成因和问题的复杂性。

让我们尝试确认我们的假设。如果我们暂时排除所有非浏览器样式表,问题就不应该出现。我在我的源代码中通过注释掉项目中的以下代码行来做到这一点。

import 'css/app.css';

我创建了一个方便的 CodePen 示例来演示这些元素,不包含 CSS。在 React 中,我们 将 SVG 图形作为组件导入,它们通过 webpack 内联到 HTML 中。

如果我们在 Safari 中打开这个笔刷并点击按钮,我们仍然遇到问题。它仍然在页面加载时发生,但在 CodePen 中,我们必须通过点击按钮来强制它。我们可以得出结论,CSS 不是罪魁祸首,但我们也可以看到,只有五个按钮中的两个按钮在这种情况下会中断。让我们记住这一点,并继续下一个假设。

即使排除了样式表,相同的 SVG 元素仍然会中断(Safari 13)

隔离问题

我们的下一个假设指出,Safari 在 HTML <button> 元素内部渲染 SVG 时存在 Bug。由于问题发生在前两个按钮上,我们将隔离第一个按钮,看看会发生什么。

Sarah Drasner 解释了 隔离的重要性,我强烈建议你阅读她的文章,如果你想了解有关调试工具和其他方法的更多信息。

隔离可能是所有调试中最强大的核心原则。我们的代码库可能很庞大,包含不同的库、框架,并且可能包括许多贡献者,甚至包括不再参与项目的人。隔离问题可以帮助我们慢慢地剔除与问题无关的部分,以便我们可以专注地解决一个问题。

它也被称为“简化测试案例”

我将此按钮移动到一个单独的空白测试路由(空白页面)。我创建了以下 CodePen 来演示该状态。即使我们已经得出结论,CSS 不是问题的原因,但我们应该继续排除它,直到我们找到 Bug 的真正原因,以尽可能地简化问题。

如果我们在 Safari 中打开这个笔刷,我们可以看到我们无法再重现问题,并且SVG 图形在点击按钮后按预期显示。我们不应该将此更改视为可接受的 Bug 修复,但它为创建最小可重现示例提供了一个良好的起点。

最小可重现示例

前两个示例之间的主要区别是按钮组合。在尝试了所有可能的组合后,我们可以得出结论,此问题仅在同一页面上,较大的 SVG 图形旁边存在较小的 SVG 图形时,绘制事件发生在较大 SVG 图形上时才会发生。

我们创建了一个最小可重现示例,它允许我们重现 Bug,而无需任何不必要的元素。使用 最小可重现示例,我们可以更详细地研究问题,并准确地查明导致问题的代码部分。

我创建了以下 CodePen 来演示最小可重现示例。

如果我们在 Safari 中打开这个演示并点击按钮,我们可以看到问题正在发生,并形成一个假设,即这两个 SVG 以某种方式相互冲突。如果我们将第二个 SVG 图形叠加在第一个 SVG 图形上,我们可以看到第一个 SVG 图形上裁剪的圆圈的大小与较小 SVG 图形的大小完全匹配。

编辑后的图像将较小的 SVG 图形与包含错误的第一个 SVG 图形进行比较。

分而治之

我们已经将问题缩小到两个 SVG 图形的组合。现在我们将进一步缩小范围,找到导致问题的特定 SVG 代码。如果我们对 SVG 代码只有基本了解,并且想查明问题所在,我们可以使用一种**二叉树搜索**策略,并采用分而治之的方法。康奈尔大学的CS 课程介绍了这种方法。

例如,从一大段代码开始,在代码中间设置一个检查点。如果错误没有出现在该点,则意味着错误发生在后半部分;否则,错误发生在前半部分。

在 SVG 中,我们可以尝试从第一个 SVG 中删除<filter>(以及<defs>,因为它本来就为空)。首先,让我们检查一下<filter>的作用。这篇文章由 Sara Soueidan 撰写对此解释得最为清楚。

与 SVG 中的线性渐变、蒙版、图案和其他图形效果一样,滤镜也拥有一个方便命名的专用元素:<filter>元素。

<filter>元素从不会直接渲染;它只作为可以使用 SVG 中的filter属性或 CSS 中的url()函数引用的内容。

在我们的 SVG 中,<filter>在 SVG 图形的底部应用了一个轻微的内阴影。在从第一个 SVG 图形中删除它之后,我们预计内阴影会消失。如果问题仍然存在,我们可以得出结论,SVG 标记的其余部分存在问题。

我已经创建了以下 CodePen 来展示此测试。

我们可以看到,问题仍然存在。即使我们删除了代码,内底阴影也会显示出来。不仅如此,现在所有浏览器上都会出现这个错误。我们可以得出结论,问题存在于 SVG 代码的其余部分。如果我们删除<g filter="url(#filter0_ii)">中剩余的id,阴影就会完全消失。这是怎么回事?

让我们再次看看前面提到的<filter>属性的定义,并注意以下细节。

<filter>元素从不会直接渲染;它只作为**可以使用**filter属性在 SVG 中引用的内容。

(重点是我的)

因此,我们可以得出结论,**来自第二个 SVG 图形的滤镜定义正在应用于第一个 SVG 图形**,并导致错误。

修复问题

我们现在知道问题与<filter>属性有关。我们也知道两个 SVG 都具有滤镜属性,因为它们都使用它来对圆形形状添加内阴影。让我们比较两个 SVG 之间的代码,看看是否可以解释和修复问题。

我已经简化了两个 SVG 图形的代码,以便我们可以清楚地看到发生了什么。以下代码片段显示了第一个 SVG 的代码。

<svg width="46" height="46" viewBox="0 0 46 46">
  <g filter="url(#filter0_ii)">
    <!-- ... -->
  </g>
  <!-- ... -->
  <defs>
    <filter id="filter0_ii" x="0" y="0" width="46" height="46">
      <!-- ... -->
    </filter>
  </defs>
</svg>

以下代码片段显示了第二个 SVG 图形的代码。

<svg width="28" height="28" viewBox="0 0 28 28">
  <g filter="url(#filter0_ii)">
    <!-- ... -->
  </g>
  <!-- ... -->
  <defs>
    <filter id="filter0_ii" x="0" y="0" width="28" height="28">
      <!-- ... -->
    </filter>
  </defs>
</svg>

我们可以注意到,生成的 SVG 使用相同的id属性id=filter0_ii。Safari 应用了它最后读取的滤镜定义(在我们的例子中,是第二个 SVG 标记),导致第一个 SVG 被裁剪到第二个滤镜的大小(从 46px 到 28px)。id属性在 DOM 中应该具有唯一值。如果在一个页面上存在两个或多个id属性,浏览器无法理解要应用哪个引用,并且filter属性将在每次绘制事件中重新定义,具体取决于导致问题随机出现的竞态条件。

让我们尝试为每个 SVG 图形分配唯一的id属性值,看看是否可以解决问题。

如果我们在 Safari 中打开 CodePen 示例并单击按钮,我们可以看到**通过为每个 SVG 图形文件中的<filter>属性分配一个唯一的 ID,我们已经解决了问题**。如果我们考虑到像 id 这样的属性具有非唯一值,这意味着**此问题应该存在于所有浏览器中**。由于某种原因,其他浏览器(包括 Chrome 和 Firefox)似乎能够处理此边缘情况而没有任何错误,尽管这可能只是一个巧合。

总结

真是不平凡的旅程!我们从对一个看似随机发生的错误几乎一无所知,到最终理解并修复了它。如果问题的原因不清楚或很复杂,调试 UI 和理解视觉错误可能很困难。幸运的是,一些有用的调试策略可以帮助我们。

首先,**我们通过形成假设来简化问题**,这有助于我们消除与问题无关的组件(样式、标记、动态事件等)。然后,**我们隔离了标记**,找到了最小的可重现示例,这使我们能够专注于一小段代码。最后,**我们使用分而治之的策略查明了问题**,并修复了它。

感谢您抽出时间阅读这篇文章。在我结束之前,我想给您留下最后一种调试策略,它也在康奈尔大学的CS 课程中介绍。

记住在调试尝试之间**休息一下,放松身心,清空思绪**。

如果在某个错误上花费了太多时间,程序员就会感到疲倦,调试可能会变得适得其反。休息一下,清空思绪;休息片刻后,尝试从不同的角度思考问题。

参考资料