乐趣区

关于javascript:断点调试之压缩引发的血案

前段时间组里的小伙伴让我帮忙排查一个线上问题,我感觉排查流程比拟有意思,想着记录一下看看是否能对其它同学有所帮忙,遂有此文。

事件的起因是前几天线上忽然收到一个报警,谬误内容是 TypeError: C.fn is not a function。相干同学尝试排查无果后又回滚了最近上线的变更也没有排查到问题。尽管最终确认了复现门路,然而在本地却无奈复现。

🔍 初步排查

在线上复现该谬误后,点击谬误堆栈的文件跳转,疾速定位到线上出错的代码。因为线上都是压缩过的代码,这里咱们能够点击左下角的 {} 进行代码丑化。

通过丑化后咱们能够看进去,应该就是 189624 行出了问题。咱们间接尝试在这一行上打断点,之后会发现代码会在这块疯狂打转。这是因为它处于一个 for 循环中。仔细观察不难看出代码其实上是 this.head 这个链的递归执行,每次执行完以后 C 都会被赋值成链的下一个值,并执行该值对应的 fn() 办法。也就是问题是这个链上的某个值没有 fn() 办法,最终导致了这个报错。

大略确认问题后,咱们须要看一下最终这个 C 的值是什么。因为处在循环当中,一次一次的点击下一步切实是麻烦。因为咱们有明确的指标,所以咱们能够尝试增加条件断点,让只有合乎咱们条件的断点才停下来,否则都疏忽失常执行。

在 189624 行右键点击 Add conditional breakpoint... 选项,并输出 typeof C.fn !== 'function' 作为条件表达式。这样咱们就实现了一个仅在 C.fn 不是一个办法的时候才会触发的条件断点。

条件断点触发后,咱们能够在控制台中基于断点时的上下文输入变量进行调试。能够从下左图咱们能够清晰的看到,此时的 C.fn 确实是不存在的。

因为方才咱们已知 this.head 应该是一条链,顺次执行链上的办法。所以实践上来说链上的每个元素都是一样的。于是乎我就尝试输入了 this.head 链上所有的元素想看一下这个链到底是什么样子的。模仿代码里的循环我也在控制台尝试写了下,发现输入的后果如下左图展现。在链的最初一个元素就是咱们有问题的元素。

而之前咱们已知的是在本地开发环境是无奈复现这个问题的,所以我照猫画虎在本地同样的地位也输入了一下 this.head 链,后果见上右图。发现和线上输入的,除了最初这个有问题的元素,其它的输入根本是一样的。

看来问题的起因就在于线上的代码执行在链上减少了这么一个玩意导致的,而本地因为没有这个多余的元素所以没有触发问题。

🐞 确认问题

找到起因后我就想着从代码层面捋一下是哪里给减少了这么个玩意。因为之前的代码中能够显著的看到 i.prototype.finish 的字样,初步猜想这应该是一个类的定义。于是乎就想看看这个类是在哪里实例化执行的。

通过刚报错时的压缩后的代码,咱们能够看到报错的模块是”protobuf.js“这个模块。于是乎我在我的项目和依赖中查找是哪个模块依赖了它,最终查到了是咱们外部应用的一个 IM 音讯模块有用到。

之后在具体的依赖模块中搜寻 .finish() 相干字样,查到了最终的调用在如下中央。serialize() 办法会调用 Request.encode() 办法,它返回一个 $Writer 基类的实例,而 $Writer 就是 protobuf.js 模块中的 Writer 基类。Request.encode() 办法实例化完 Writer 基类后会执行一系列的成员函数,执行结束后会返回 Writer 实例,并调用它的 finish() 办法。

理解执行流程之后,我就顺着 Request.encode(req).finish() 这一句开始向上对 Request.encode() 办法进行断点(下左图)。如下图先尝试在开端断点输入 o.heado 是压缩后指向 Writer 实例的变量),发现此时曾经存在异样链元素了(下右图)。

两头的代码略微打了下断点发现也仍旧如此。最终在头部断点处发现了端倪。尝试在结尾减少断电之后,发现在 120274 行执行结束之后 o.head 链上就曾经存在了异样数据了。

那咱们尝试翻看下代码看一下 o.create() 办法具体干了什么。从下图左咱们能够看到 Writer.create() 实质其实就是 Writer 基类的实例化工厂办法。而下图中能够看到 Writer 的构造方法对一些成员属性赋了初值。其中要害的 this.head 的初值是一个 Op 基类的实例。下图右能够看到 Op 基类的构造方法中也是赋了一些初值。同时咱们能够看到 function noop() {} 实际上就是一个空办法。也就是说 this.head 默认指向了一个空办法实例化的 Op 对象。

乍一看整个流程其实非常简单,实质上构造函数内都是一些简略的赋值操作,不会有什么问题。于是乎还是依照链路顺次向上排查问题。因为上一趴咱们排查到执行完 Writer.create() 工厂办法后就有问题了,所以这里咱们须要对 Writer 的构造函数进行断点排查。

尝试如下图在构造方法开端断点后,输入 this.head 链,发现此时曾经有异样数据了。而这个时候不过只是做了初值的操作而已,这怎么就能出问题了呢?因为断点状况下我能在以后上下文中进行调试,所以此时我尝试本人执行一下 Op 基类的实例化操作(见下图)。这时候发现的确它的 next 属性不对,是咱们要找的问题元素!

此时此刻,我感觉咱们曾经越来越靠近假相了!

如下图左咱们在 f 变量上 hover 一会儿,会呈现它的定义处链接,点击后会间接跳转到它的定义处下图右(其实就离的不太远)。

大家可能也都留神到了,咱们方才看的代码中 this.next 明明是定义成 undefined 怎么这里给定义成 g 了?而这个 g 又对上了 189456 行 g = s.base64,所以咱们才看到 this.head.next 的值这么奇怪。而咱们尝试看一下援用的 protobuf.js 代码,发现代码里 this.next 尽管是等于 g 然而它并没有关联到 u.base64 上。

因为我之前有解决过一些压缩再压缩后代码异样的 Case,所以至此我基本上能够判定,因为 protobuf.js 在咱们的依赖中是引入的压缩后的代码,而压缩后的代码再走压缩导致了变量指向呈现错乱从而导致的问题。这也侧面印证了为什么只有线上能够,本地无奈复现的起因。因为本地是没有走压缩的。

🛠 如何解决

找到问题后有两种解决办法。一是正向的去查找压缩工具造成这个问题的起因;二是反向的去躲避该问题,咱们不引入压缩后的代码而是失常引入未压缩的代码,最终对立由我的项目进行压缩解决。

这两种办法都能解决问题。而第一种须要的工夫会比拟久,所以咱们先采纳了第二种办法长期解决一下。因为该依赖包不是咱们保护的,咱们只能应用 patch-package 给模块打补丁的形式进行修复。它的性能是在装置完依赖后会依据咱们的 diff 文件对依赖进行批改。

这里咱们的批改比较简单,找到咱们依赖模块引入 protobuf.min.js 的中央,将其批改成 protobuf.js 即可。

🗒 后记

undefined 在压缩后就变成了 g 这个初步猜测应该是本地想要定义一个没有定义的变量,这样就是 undefined 了。我尝试克隆了下 protobuf.js 仓库进行了尝试,发现应该是 UglifyJS 中配置了 marguel.eval 导致有这个个性。

以上就是压缩造成的血案残缺的排查通过,整个的过程总结一下有以下几个教训能够供大家参考:

  1. 除了单步断点,咱们还有条件断点、日志断点等多种断点形式帮忙咱们排查问题,正当应用会减速咱们排查问题的速度。
  2. 断点后以后 JS 环境会停留在过后的上下文中,咱们能够在控制台执行、输入咱们想要的过后环境的数据帮忙排查。
  3. 控制台中咱们也能够 hover 查看定义地位,进行定义间疾速跳转。
  4. 压缩后的代码不可怕,咱们能够通过源码比照,无奈压缩的关键字进行定位查找。
  5. 只有是能够复现的问题,那都不是问题!

最初祝大家动工大吉,新的一年没有 Bug!

退出移动版