上周,团队计划的使用 esbuild 压缩代码经过一周的灰度测试后,顺利上线,表现稳定。就在感觉十拿九稳,今年绩效又将迎来丰收的喜悦的的时候,一个 bug 又把我拍醒了……
2021.04.16
10:00
风和日丽,因为疫情推迟的年会也要召开了,抽奖的小手蠢蠢欲动,还跟同事约了去会场旁边吃烤鸭。
11:44
tl 分给了我一个 bug:编辑奖励金额,减少到 0,前端提示减少额度不能超过余额。
内心暗想:这肯定是前人判断错了!看我吃饭前就给你修完!
12:00
测试环境尝试复现问题失败,没问题嘛。准备原样扔回去给接收 bug 的同事,然后顺手再理论一番,表现出开发高高在上的姿态。
12:01
线上的确有问题。
12:05
虽有不甘,但还是跟同事打车去了饭店。
12:30
趁等位的时候打开同事的电脑,开始继续 debug。
这次 debug 流程更让人伤心,不止没有找到问题,反而发现加了 debugger 之后,每次看完东西继续向下走的时候,我的弹窗消失了,💔 也更痛了 。
加了 debugger 的页面大概长这样:
就是那个蓝色的小按钮,每次点完我的弹窗都消失了(; ̄O ̄)。
12:40
上菜了,吃完再说。
不过吃完就去年会了,暂时也没什么好的思路,所以回家再查。
2:00 - 21:00
年会 ing……
- 看大哥 💃
- 没中奖
- 没中奖
- 没中奖
- 看产品被围起来 🍻……
22:00
到家继续查。
既然线上 debug 不太行,那就改完直接部署,于是一边看着《火线》一边等部署成功。
日志打到测试环境,果然有问题,代码里的一个判断:
Decimal.sub(oldMoney, newMoney).isNegative();
当 oldMoney
和 newMoney
相等时,求值结果在开发环境表现为 false
,但是测试环境为 true
。
去 Decimal.js 官网 打开控制台,发现 Decimal.sub(1, 1)
是很正常的,没什么问题,那就只能怀疑是我们的打包流程有问题。联想到项目最近接上了 esbuild
,可能就是你了!
23:00
把线上的代码的 Decimal.sub
搞下来,然后和本地 node_modules 里的代码进行比较:
左边是 Decimal.js
的源代码,右边是编译之后格式化的代码。
不想听剧透的👨🎓👩🎓可以先找一下不同。
剧透分割线 1 ==============================================
剧透
左边的 new Ctor(rm === 3 ? -0 : 0)
,在压缩之后,变成了右边的 new pn(-0)
。
但是怎么确定就是 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 了。我:
解决问题
因为涉及到库的升级,所以不能立马修掉,只能从项目这边想办法,不过也很简单: 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 修复
直接上代码吧:
comment 里也注明了,是针对 -0 和 0 的情况加了一个特殊的 case。
bonus
前面提到,线上调试会发现在点掉断点让代码继续向下执行的时候,弹窗会直接消失,这是为什么呢?
让我们用一个录屏解释一切 🤦♂️: