调试的艺术

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

这是我 2015 年在阿姆斯特丹 Fronteers 首次发表的 调试艺术演讲的 配套文章。

TL;DR:学习每一种可用的工具,根据需要使用它们,享受解决错误的乐趣——敲击键盘并使用 6 个月的功能驱动器肯定会更有趣。

在我们开始之前......

如何跳到最后...

不。

写。

错误。

虽然...

假设您不是机器人,并且您以前可能写过一两个错误,那么事实是:没有灵丹妙药。

事实上,我刚才撒了谎,“不要编写错误”与学习调试恰恰相反。你需要经验。您必须遇到错误才能识别如何处理它们。

您无法获得用于调试的硬性技能(或者我相信)。当你第一次遇到某事时,它是随着时间的推移而获得的。花费数小时试图解决问题,但结果是:下次您不会再花费数小时。

在我 10 年前工作的公司,当新员工入职时,我们会让他们对这份工作和我们正在解决的各种问题感到兴奋,但在他们加入的那天,他们会被分配到 bug 3个月。这有点降低了他们的期望,但我们发现在这 3 个月之后,他们会要求继续处理错误。

他们有机会涉足业务的许多领域,而其他开发人员为同一个功能编写 3、6 甚至 12 个月的代码,即使那样,它也会上线,出现错误,我们的错误捕捉器会突然出现,修复错误,并获得一片荣耀。

我相信,获得 实践 经验是成为一名优秀开发人员的关键,但实际上,知道如何调试任何东西。 10 年前那家公司的设计师 Chris——他是 CSS 奇才。当服务器端开发人员陷入简单的事情时,他知道所有的答案。我经常发现自己问他为什么布局在我认为相对简单的设计中变得如此光秃秃的。他的回答通常很安静,是“将 zoom: 1 添加到该元素。”

他已经完成了头脑中的调试步骤,并提出了一个合理的建议,即这个特定的更改可以解决我的问题,几乎完全是因为他看到了如此多的视觉错误排列,以至于他可以用肉眼识别。

这就是我在遇到许多错误时会做的事情。我将充分了解特定的系统,让自己抢先一步找到解决方案。

但在我继续之前,我有 两个 免责声明......

免责声明 #1 - 框架

在任何人都说教之前,这不是在 web 上进行调试的最终方法。有 很多方法 。这恰好是我所知道的,以及我是如何做到的。如果这对你有帮助,超级。如果您以不同的方式做事,那也很酷。

个人 不使用框架和大型(固执己见的)库。 Ember、Polymer、React、Angular 等库。我不使用它们。我所做的任何事情都不需要它们,所以我没有学习的要求(请不要以此为邀请来教我!)。

这意味着我使用的特定工具 可能不适 用于您的工作流程。事实上,它完全有可能与您的工作流程不 兼容

该问题部分与您正在使用的应用程序的复杂性有关(“应用程序”是指用于提取您要构建的站点的支持代码)。例如,React 已经创建了自己的语言来为开发人员提供最大的 困惑 影响构建 应用程序 ,但正因为如此,它转换成的代码对人类完全无用,仅适用于计算机/浏览器。所以调试它 至少 需要 sourcemaps,但是因为(我只能假设)它也有自己的状态管理(和其他花哨的玩具)你被鼓励安装 devtools 扩展来调试你的 React 应用程序 (我相信 Ember 是这里也类似)。

这并不意味着这些信息对你没用,我会谈及调试时重要的想法,我只是说:我不使用框架,所以我也不用它们调试。

免责声明 #2 - 我很少跨浏览器测试

是的。我说了。但在你把我扔进狼群之前,请听我说完。我不进行跨浏览器测试,因为我的工作经常要求我编写 JavaScript。 Vanilla JavaScript,而不是与 DOM 交互的 JavaScript。

在我看来,有两种我感兴趣的 JavaScript:浏览器交互和其他一切。

其他所有内容都必须在 ES5 中工作(可能还带有一些 ES6),仅此而已。除非我支持 IE8(最近的项目我不支持),否则我所有的 JavaScript 都可以在所有浏览器上运行,因为它是这样的:


 function magicNumber(a, b) {
  return Math.pow(a, b) * b / Math.PI;
}

上面的代码在哪里运行并不重要,如果有错误,所有浏览器中都会有错误。如果没有错误,那就没有错误,就是这样。

此外 ,这并不意味着我的代码没有在其他浏览器上进行过测试。如果可能和需要,我将在不同的浏览器环境中运行自动化测试(使用像 Karma 或 Zuul 这样的工具——但整个自动化跨浏览器测试的事情还没有真正修复,现在有点乱)。

同样,这完全是由于我的工作性质。稍后我将说明我将如何(甚至是否)进行跨浏览器测试。

调试的艺术

这是我在所有调试工作坊中打开的东西,看,甚至维基百科都说这是一门艺术, 所以这是一件事,好吧!

我分解(在我的脑海中)调试如下:

  1. 复制 翻译:查看错误
  2. 隔离 翻译:了解错误
  3. 消除 翻译:修复错误

复制

复制错误是整个工作中最难的部分。通常情况下,您会收到如下错误报告:


保存不起作用

...就是这样。

是的,所以我不仅要在你的回应中表现得圆滑,就像我想简单地回答“是的”[关闭],而且我还需要收集尽可能多的信息以便能够复制什么该用户正在查看。

如果他们谈论的站点是 jsbin,那么我知道保存确实有效,因为我刚刚使用它,这只意味着保存它对他们(可能还有其他人)有用。翻译: 储蓄可能对 有用

如果我访问用户正在谈论的 URL,它会立即中断。这是一个幸运的休息。这是 试金石 ,它总是值得做的。永远不要直接尝试 100% 复制——一步一步来。然而,也更有可能是在 bug 出现之前发生了一系列事件,我必须了解那些是什么,然后自己重​​复。

认真、细致、系统。这很重要,因为我不会只做一次,我必须能够一遍又一遍地做(或者至少:两次)。

有一些关键工具可以帮助我复制环境,或者至少有两个工具可以帮助确定我可以排除环境的哪些部分:

隐私浏览模式/隐身

Chrome 中的隐身模式(在其他浏览器中以其他名称而闻名)让我可以 在没有 (大部分)浏览器扩展运行的情况下启动网站。对于 cookie、离线存储和我的“正常”浏览会话附带的任何其他预烘焙配置,它也会从一个干净的状态开始。

我可以肯定地说,我每年 至少 收到一个 bug,这通常很奇怪,归结为用户浏览器上的一个 rouge 扩展,它干扰了网站代码。

通过以隐身模式运行,没有看到错误,然后要求用户重复相同的任务,我能够立即确认有外部实体在起作用(即通常是扩展)。

多个配置文件

在 Chrome 中,我有我的个人资料。一个让我访问我的电子邮件而不总是要求我登录的人(虽然......也许这很糟糕,但很快就会继续)。

我还有另外两个配置文件:

  • 匿名 - 这个用户是完全干净的,没有扩展,没有历史
  • 巨魔 - 这个用户会像匿名用户一样,但也会禁用 cookie,并且将安全设置设置为最大

我并不经常需要切换到这些配置文件(主要是因为我能够在测试的早期复制错误),但可以轻松跳转到这些配置文件。

troll 用户特别有用,因为(对我而言)很容易忘记某些用户具有更高的安全设置,结果是像 localStorage 这样的 API 会抛出异常——如果未被捕获,可能会导致混乱。

既然我能够始终如一地复制,是时候在我能够修复之前尽可能多地剥离以减少噪音和潜在的混乱。

隔离

隔离就是尽可能地减少 bug。如果一个扩展是错误的原因,让我们一次禁用一个扩展,直到找到错误的扩展。

如果它是一组相对复杂的需要大量用户交互的 JavaScript 中的错误,我会问自己 是否可以 重构 这个特定的代码区域,以便我可以单独测试它并注入预烘焙状态

我正是为这个问题建立了 jsbin.com 。要解决问题,请将其剥离,然后修复并与需要的人分享。

一旦它像我想要的那样被精简,我就开始修复这个错误。

排除

一旦解决了复制方面的问题,这实际上很容易。这些天(2015 年)我更有可能在我的项目中实际创建一个失败的测试,它将复制我正在处理的错误,然后我将修复失败的测试。好处应该是显而易见的。

在这种状态下真的很简单。就像编写代码一样简单(一旦您可以触摸打字)。困难的部分是解决问题,这不是通过按键盘上的键来解决的。

当你无法复制...

好吧……你真倒霉,你可以盲目编写解决方案,但这不是调试。您需要考虑您手上是否有 Heisenbug (是的,我喜欢这个词!)。这是一个在您尝试询问它时会从字面上改变形状和形式的错误。

我自己也遇到过其中的一些。最糟糕的类型(对我而言)是这些错误仅出现在我的 CI 系统(如 Travis)中。我正在处理的错误已在我的本地环境中修复,并且我对代码的理解足够深,知道错误已修复,但我的测试无法通过。现在的任务不同了,任务是调试测试环境,当它是 CI 时,它是一个封闭的系统。

我遇到此类问题的另一个重要时间是在我使用 Firebug 时(大约在 2009-2010 年停止)。 Firebug 是/曾经是一种侵入式调试工具,它将内容注入 DOM 以实现调试。它也有错误(就像 devtools 和所有其他调试器一样 - 请参阅本文的开头!)。这意味着您可能会遇到某些边缘情况,这些情况会触发调试器中的错误,从而使调试变得格外……具有挑战性。

今天 也是如此 。使用 devtools 时间线进行调试的建议是 不要 打开所有录音复选框,并且最好关闭所有其他选项卡 任何其他可能使用 WebKit 的东西(比如 Spotify ...我假设有一些重叠的操作系统访问 WebKit 和 Blink 拥有的...)。这是因为 所有 这些都会影响演奏录音。

调试方法

可用的工具分为两类:

  • 反了
  • 在外面

我承认这些不是好名字。由 内而外 ,我的意思是错误的 来源 是已知的。通常可以添加一个特定的函数或代码行,以及 debugger 语句、断点或条件断点(当表达式为真时中断)。

从外到内 更有趣,因为您可以从视觉上识别出存在错误,也许某个元素的行为不符合您的预期。有越来越多的工具可以帮助您从视觉问题中解脱出来,深入 问题的代码源中, 而无需 特别了解源代码。

这些工具包括:

  • DOM 断点 - 在子树修改、属性修改或节点删除时中断
  • Ajax 断点 - 在执行 XHR 调用时中断
  • 重播 XHR - 允许您重新注入来自 XHR 调用的响应
  • 时间线截图——既针对网络(通常是开机时间),也针对运行时的时间线

我最喜欢/最常用的工具

最后,我想与大家分享一些我使用的工作流程和一些我经常使用的工具。


工作区和实时更新

打开 devtools 并选择源面板,只需将要为其创建工作区的本地目录拖到源面板 ,devtools 将要求访问,您需要确认。

但这并没有完成该步骤。为了让 devtools 知道一个特定的来源,比如 http://localhost:8000 正在从你的新工作区提供服务,你需要映射至少一个文件。右键单击源列表中的文件,然后选择“映射到文件系统资源”,然后选择它关联的 本地 文件。

现在,无论何时进行任何更改,您都可以保存,它会直接保存到磁盘。为什么这很重要?现在您可以调试并直接提交到磁盘而无需切换上下文,无需从编辑器切换到浏览器。


真正有趣和强大的是,如果 CSS 文件也被映射,元素面板中样式 的任何 更改,直接更新附加的 CSS 文件。这意味着我可以在元素面板(我习惯于在其中进行更改)中进行非常微小的视觉更改,并且它已经为我保存到磁盘中。

撤消

我举办了许多 调试研讨会 ,在展示工作区后出现的一个一致问题是:

如何还原我在元素面板中所做的更改

与直接编辑源代码相比,开发人员使用元素面板似乎始终更加快捷和宽松。

这仍然是一个公平的问题。对此我回复:

  1. 源头控制!
  2. 撤消

Chrome devtools 有 很好 的撤销支持。我可以进行一系列的 CSS 更改,然后转到 JavaScript,然后对 DOM 进行更改,我 仍然 可以返回并撤消我所做的所有 CSS 更改。

我注意到我确实必须专注于特定面板和源才能使撤消工作(我想撤消历史记录与面板相关联),但这真的很好。

显然,当您重新加载时,您会丢失历史记录。这与 Sublime Editor 相同,如果我卸载并重新加载 Sublime(即重新启动应用程序),我希望撤消历史记录会丢失。

控制台快捷方式

  • $ & $$ ​​- 类似于 jQuery 的 $ 函数来查询页面上的元素
  • $_ - 最后一个表达式的结果
  • $0 - 当前在元素面板中选择的 DOM 节点
  • copy(...) - 复制到剪贴板,并将 JSON.stringify 对象,但也获取 DOM 节点的外部 HTML, copy($0) 对我来说很常见


时间线截图

回到过去查看应用程序启动(或交互时间)中哪些内容更改了页面上的内容,这是一种非常好的方式。我最近用它来解决两个不同的问题。

第一个是查看 jsbin.com 的启动屏幕截图,看到字体在最后加载,但占用了合理的时间(WRT 整个启动时间)。我可以 看到 这一点,因为字体会在准备就绪的文档末尾闪烁到位。然后,我能够使用字体加载技术通过本地存储加载字体,并缩短了感知到的启动时间。

第二次是使用我的产品 confwall.com 。问题在于加载标签系统时存在明显的延迟。如果您观看下面的动画(以 50% 的速度运行),您会看到选项卡呈现缓慢:

这也通过“相机”图标在渲染时间轴中捕获:

由此,我可以移动选项卡最终重新呈现到正确布局的 时间 点,并向后工作以找到正在运行和阻塞的内容。

节流

限制网络使我能够真正快速地了解模拟慢速或完全离线的连接,从而即时了解较慢网络的影响。

一个典型的例子是:我的带有自定义字体的网站在慢速连接下看起来像什么?是不是空白很久了?其他资产是否阻碍了字体渲染?有什么我可以做的吗?

网络详细信息和回复

网络请求的可视化很有用,但我也发现检查标头和复制原始响应非常有用。

我还发现,当我调试响应不正确的服务器端错误时(比如发回 HTML 而不是 JSON),我可以调试、修复并重新启动服务器,而 不是 刷新浏览器并吹走状态和当前堆栈——我可以简单地“重播 XHR”,我的代码将重新运行请求,并且 (IIRC) 回调将触发更新的服务器内容。

在 DOM 更改时中断

正如我之前提到的,“在 DOM 更改时中断”是我将从 外部 调试的一种方式。当我知道有视觉变化时,我已经使用了很多次,但我不确定这种变化的来源是什么。

我确实发现很难准确知道要使用哪个“break on...”。通常“属性修改中断”很简单——即如果 className 发生变化,代码就会中断。否则,我倾向于只选择所有内容,直到代码中断,然后我将单步执行或向后执行调用堆栈。

这里的一个 额外提示 是有时调用堆栈会由于异步调用而被 斩首 。 Devtools 在源代码面板上提供了一项功能(内存消耗很大,所以记得将其关闭)。选中“异步”框并重复错误。您现在将拥有跨异步调用的完整调用堆栈。

内存泄漏的表面扫描

最后,内存泄漏传统上(对我来说当然)是调试中最难的部分。事实上,我很少看记忆,除非我觉得有什么东西向我袭来。然而,devtools 在挖掘漏洞所需的便利性方面确实取得了进步。

我将采用两种方法,完全参考了几年前这段 出色的 Chrome 视频

  1. 看着楼梯的表面测试
  2. 使用分析工具捕获泄漏源的线索

阶梯效应是判断是否存在内存泄漏的第一个初始线索。对我来说,诀窍是可靠地重现泄漏效果。我将亲自开始时间线录制,并选择“内存”(仅此而已)。我会开始互动,在停止之前,点击强制垃圾收集的垃圾箱,然后我会 再次 重复这个过程,然后结束录制。

我在这里要做的是:建立基线内存使用(我开始交互之前的数据),运行交互。如果有大量内存 无法 被垃圾回收,那么我就有泄漏了。然后开始分析。

分析可以采用两种方法。第一个是捕获两个堆转储,一个在交互开始时,一个在交互结束时。我可能还会运行两次交互,但在开始第二次运行之前,我将强制执行垃圾回收。然后任务是比较增量。我将选择第二个堆转储,并将其从“摘要”更改为“比较”并按“增量”排序。现在我要找的是内存中的 红色 项目。这些是不能被垃圾收集的项目。

然后,这将(希望)提供有关泄漏内容的线索。通常是 DOM 节点,什么 JavaScript 引用仍然指向这些节点。令人沮丧的是,它通常位于 JavaScript 库中,因此了解库的工作原理会有很大帮助。

包起来

正如我在开始时所说,没有灵丹妙药。我怀疑这篇文章的许多读者会直接浏览到可操作的部分并复制和粘贴。这很酷,我也会这样做。

磨练你的调试技能是一场漫长的比赛,直接关系到编写导致错误的人工制品的代码。希望您也能抓住调试的机会!

请记住,从调试中休息一下也是值得的,很多 很多 错误都在不靠近计算机(长途步行、淋浴等)的情况下得到解决 - 因为计算机有时也会 有点压力 ......!