上周,团队计划的使用 esbuild 压缩代码经过一周的灰度测试后,顺利上线,表现稳定。就在感觉十拿九稳,今年绩效又将迎来丰收的喜悦的的时候,一个 bug 又把我拍醒了……

2021.04.16

10:00

风和日丽,因为疫情推迟的年会也要召开了,抽奖的小手蠢蠢欲动,还跟同事约了去会场旁边吃烤鸭。

11:44

tl 分给了我一个 bug:编辑奖励金额,减少到 0,前端提示减少额度不能超过余额

内心暗想:这肯定是前人判断错了!看我吃饭前就给你修完!

12:00

测试环境尝试复现问题失败,没问题嘛。准备原样扔回去给接收 bug 的同事,然后顺手再理论一番,表现出开发高高在上的姿态。

12:01

https://i.loli.net/2021/04/23/LuYhgTU3IHSvlcK.jpg

线上的确有问题。

12:05

虽有不甘,但还是跟同事打车去了饭店。

12:30

趁等位的时候打开同事的电脑,开始继续 debug。

这次 debug 流程更让人伤心,不止没有找到问题,反而发现加了 debugger 之后,每次看完东西继续向下走的时候,我的弹窗消失了,💔 也更痛了 。

加了 debugger 的页面大概长这样:

https://i.loli.net/2021/04/22/pSIqhfFZAsv2yd1.png

就是那个蓝色的小按钮,每次点完我的弹窗都消失了(; ̄O ̄)。

12:40

上菜了,吃完再说。

不过吃完就去年会了,暂时也没什么好的思路,所以回家再查。

2:00 - 21:00

年会 ing……

  • 看大哥 💃
  • 没中奖
  • 没中奖
  • 没中奖
  • 看产品被围起来 🍻……

22:00

到家继续查。

既然线上 debug 不太行,那就改完直接部署,于是一边看着《火线》一边等部署成功。

日志打到测试环境,果然有问题,代码里的一个判断:

Decimal.sub(oldMoney, newMoney).isNegative();

oldMoneynewMoney 相等时,求值结果在开发环境表现为 false,但是测试环境为 true

Decimal.js 官网 打开控制台,发现 Decimal.sub(1, 1) 是很正常的,没什么问题,那就只能怀疑是我们的打包流程有问题。联想到项目最近接上了 esbuild,可能就是你了!

23:00

把线上的代码的 Decimal.sub 搞下来,然后和本地 node_modules 里的代码进行比较:

https://i.loli.net/2021/04/25/RojWNkp2sLqdAPl.png

左边是 Decimal.js 的源代码,右边是编译之后格式化的代码。

不想听剧透的👨‍🎓👩‍🎓可以先找一下不同。

剧透分割线 1 ==============================================

剧透

https://i.loli.net/2021/04/25/46J8rTjtZqKbEv1.png

左边的 new Ctor(rm === 3 ? -0 : 0),在压缩之后,变成了右边的 new pn(-0)

https://i.loli.net/2021/04/25/QUVEjhvKfoBL7xg.jpg

但是怎么确定就是 esbuild 的问题呢?

23:30 - 与 bug 的斗争进入白热化阶段

前人说过,理解一个 bug 的最好办法就是写一个最小的可复现案例。照着这个思路,我们尝试一下:

var a = window.p ? -0 : 0;

然后使用 esbuild 传入不同的参数进行编译:

// esbuild --bundle index.js --outfile=./dist.js
(() => {
  var a = window.p ? -0 : 0;
})();

// 加入 minify 参数
// esbuild --bundle index.js --minify --outfile=./dist.js
(()=>{var a=(window.p,-0);})();

Confirmed!果然就是 esbuild 导致的问题。项目里使用的 Decimal.isNegative() 是根据数值的符号来判断的,-0 的符号因为是负号所以会返回 true,因此也就有了上面的 bug。

2021.04.17

1:00

所以 esbuild 的这个处理有问题的,于是怒提一 issue 给官方,顺便贴上了处理这块儿的代码。

9:00

本以为要等几天才能修,结果起来就看到了 github 的消息,然后点进 issue 里,发现已经有一个哥们修完了,还加了测试用例,esbuild 作者也已经 review 完并合进 master 了。我:

https://i.loli.net/2021/04/25/tGX6SkgT8iADhIw.jpg

解决问题

因为涉及到库的升级,所以不能立马修掉,只能从项目这边想办法,不过也很简单: Decimal.js 提供了 lessThan 函数,-0 与 0 比较大小的时候是相同的,所以 Decimal(-0).lessThan(0) 会返回 false。算是一种治标不治本的解决方案吧。

esbuild 处理

这部分是 go 代码理解。esbuild 在开启--minify 参数时使用了一个 valuesLookTheSame 函数来比较两个传入的参数是否相等,比如 a() ? b : b,其实可以被压缩成 a(), b,更甚至 a ? b : b 可以被压缩成 b,连 a 也被省略了。

放到 demo 里:

var a = window.p ? -0 : 0;

esbuild 认为 -0 和 0 是相等的,因此就取了三目运算符的第一个值作为结果,如果下面这么写:

var a = window.p ? 0 : -0;

那么最终的压缩结果其实会是 (()=>{var a=(window.p,0);})();

bug 修复

直接上代码吧:

https://i.loli.net/2021/04/25/vyxkTgIG6RfuAwK.png

comment 里也注明了,是针对 -0 和 0 的情况加了一个特殊的 case。

bonus

前面提到,线上调试会发现在点掉断点让代码继续向下执行的时候,弹窗会直接消失,这是为什么呢?

让我们用一个录屏解释一切 🤦‍♂️:

https://i.loli.net/2021/04/25/d4iKAt8mBDGoUkH.gif

参考