壹 ❀ 引
某一天,CSM日常找我反馈客户紧急工单,说有一个私有部署客户升级版本后,发现一个功能使用不太正常。因为我们公司客户分为两种,一种是SaaS客户,客户侧使用的版本被动跟随主版本变动,而私有部署客户而是由我们交付版本给客户,客户有选择权决定是否升级,且用户数据都由客户侧服务器自行存储,对客户来说更安全。而我在排查这个问题的过程中,其实心里也产生了一些自认为的真理,结果排查下来也是啪啪打脸,因为问题解决的过程也挺有意思,所以本文做个记录。
贰 ❀ 场景还原
先来描述下问题,因为我们公司是做研发管理工具(ONES,欢迎体验),那么自然有工时登记相关的功能,对于一个工作项,假设当前用户有所有用户登记工时权限,那么他点击登记工时,就可以选择为某个用户登记工时(下拉框可以选择),假设他只有登记自己工时的权限,那么登记工时的用户下拉将会是禁用状态且用户默认选择就是自己。
而客户侧问题是,一个用户明明只有给自己登工时的权限,但现在登记工时,用户选择居然可以选其他人,而且选择其他人后登记接口报错提示无权限登记工时,且这个问题是私有部署升级后才出现,之前都很正常。
我在与产品经理确认过工时权限对应表现后,分别在自己的开发环境,测试环境,集成测试环境以及SaaS正式环境分别做了测试,发现都无法复现这个问题,但客户侧就是能稳定复现,同一份前端代码,不同环境表现不同,所以初步推测可能是后端问题,会不会是账号脏数据,或者接口返回数据异常。
目前已知信息:
1.客户侧控制台无报错,接口无报错,但需要确认客户侧权限接口返回是否正常
2.近期升级引发,那么很可能是近期改动,找到对应功能文件查看近期commit记录,便于搜集信息。
3.目前掌握信息过少,客户侧稳定复现,支持远程排查。
叁 ❀ 不稳定的可选链.?
既然客户能稳定复现,而且支持远程,那么最有效的方法就是远程客户搜集信息,大不了直接读客户侧源码,但远程机会有限,我也不好意思远程半天占用客户太久时间,所以远程前去梳理了下对应功能的逻辑,并查看了近期代码提交记录,果然发现 2 个月前对应文件有改动,遗憾的是与作者沟通也并未获得太多有用信息(毕竟如果他知道问题这个bug就不会逃逸了)。
前面说了只有管理所有成员工时权限时,下拉框才能展开,而控制的开关是一个名为hasUpdateAllManHourPermission
的变量。另外,因为交付给客户的私有部署包都是高度压缩的代码(很多单词被压缩成字母,文件名也变了),一个文件几十万行代码,为了避免找不到位置,我也提前保留了几个具有特征性的单词,运气好可以通过它们大致定位范围。
顺利连接客户电脑,在让客户演示后,问题确实与客户说的一样,但由于没有接口报错,咱也没办法直接定位代码文件。接管鼠标,打开控制台,选择 Search
,这样控制台底端会出现一个搜索栏,而这个搜索的范围是当前页面运行的所有资源,输入hasUpdateAllManHourPermission
回车,运气还不错,这个单词没被压缩掉。
匹配结果可能会有多个,因此需要一个个点击,去阅读单词所在的逻辑是不是我们想要的,通过这种办法可以快速缩小代码范围,简单查看后,终于成功定位到我想要的范围,直接断点hasUpdateAllManHourPermission
查找,刷新页面,鼠标移上去一看一个false
,说明确实返回的是没权限....甩锅后端失败 = =,妥妥前端问题。
莫慌,排查问题的一个思路是,如果问题点的排查法失效,那就从这个点扩散范围,形成一个圈,我们接着阅读它的上下文,一遍没看出问题,那就反反复复看不断加范围,皇天不负有心人,阅读中我终于成功定位问题。原来除了权限点获取外,还有段这样的逻辑,假设当前用户有所有用户登记工时权限,那么选择人员的下拉框会将用户默认成当前工作项的负责人,因此前端需要通过工作项ID获取到工作项对象,再从这个对象中知晓此工作项负责人是谁。
而获取工作项ID的逻辑,是这么写的,注意,这是未压缩前的代码:
// 工作项id默认从state中获取,这里用了可选链?.
getCurrentTask = (taskUUID = this.state?.taskUUID) => {
const { getTask } = this.props;
if (!taskUUID) return null;
const task = getTask(taskUUID);
return task;
}
taskYYUD
默认从state
中取,如果没找到工作项ID,那么就不查找工作项了,直接返回null
,而接下来其实还有段逻辑,如果找不到task
也会返回null
,这段我就不贴了
而在客户侧压缩后变成了这样:
之前可选链在代码压缩后变成了三元表达式,这里的 e
就是taskUUID
,默认是undefined
,导致下面的t(e || this.state.taskUUID)
没执行,直接返回了一个null
。
而我将断点移动到下面括号中的state
,可以看到state
中确实存在UUID
,只是压缩后因为三元的缘故它没有机会被赋值了。
成功破案,罪魁祸首就是在函数形参中使用了可选链 ?.
,可以设想一下,当前组件是一定存在state
的,由于state
是一个对象,即便taskUUID
不存在,直接这么复制其实也没问题,所以修复方法就是去除可选链。
taskUUID = this.state?.taskUUID
// 改为
taskUUID = this.state.taskUUID
那为什么这个问题只有私有部署环境出现,其它环境都没问题呢?在与构建以及运维部署那边的同学沟通后得知,私有部署因为要交付代码,提供的代码压缩规则其实与SaaS环境压缩规则不同,这就是为啥在其它环境怎么都复现不了....
但不管怎么说,我们不建议在形参使用可选链,或者说不建议在给一个参数设置的默认值是一个obj.xx.xx
的格式,因为此时函数还没执行,在未知情况下,可能这个obj
或者某个上下文都没准备好,代码报错还好,像上面的问题被压缩转义,错误都没有,真就很难排查了。
我们还是推荐形参默认值是一个确定的数值,如果一定是对象链式访问的值,还是推荐将其写入函数中,这样风险最小,比如:
const fn = (name) {
let nameCopy = name || this.obj.name;
}
肆 ❀ 总
最近因为荣耀这个客户签约,可以说公司是全员进入加班状态,上层投入了一大半研发加入了修bug队伍,每天也都有同事找我资讯一些痛苦bug的排查思路,可以说是一件略微开心的事(都知道我们组的痛苦了!!!)。
今天大佬也是找到我,让我把身上简单的bug都转出来给新人练手,对我说这种bug不符合我现在的段位,让我负责一些难度更高的问题....未来估计还会痛苦好一段时间,不过也会发现很多有趣的bug吧。
另外,关于可选链用法可以参考这篇文章JS 可选链操作符?. 空值合并运算符?? 详解,更精简的安全取值与默认值设置小技巧,那么到这里又分享了一个有趣的bug,本文结束。
最后还是要吐槽一句,原神小保底今天歪了个刻晴,死气我了!!!!