一次查错
问题描述
从某天起,在大群里有 KOL 用户反馈:在微信客户端里打开商品详情页会出现页面尺寸放大的现象:
出现的频次不确定,不能稳定复现,且在北京、长沙几个地域均出现了该情况。
技术前提
出现这种现象的前提,是因为前端配置了页面的 viewport meta 标签,并使用 JS 动态根据设备尺寸和像素比来缩放页面,以适配不同屏幕的手机设备。在下文中,该段技术前提代码统称为『配置』。
排查步骤
排查的难度在于:无法稳定复现,所以只能不断排查触发条件。
-
第一种假设:配置失败
- 首先想到的是,这段配置并不在所有业务逻辑的最开始,是否是由于这之前的业务逻辑报错导致配置失效?于是将这段配置提升到业务逻辑最前面。失败。
- 再次观察问题描述,发现出现放大的情况,页面并没有标题,是否是由于设置标题出了问题(因为在微信里设置页面标题必须用 iframe 的 trick 的方式)?于是去掉了在微信里设置标题的业务代码。失败。
- 经历了两次业务逻辑排查失败,考虑从更高层面分析,是否是业务代码造成的影响?由于页面中大多数核心逻辑的错误都在 Promise 的流程中被 catch 住,而且页面级的 sentry 日志收集方案因为外网不支持,并没有保留上报的错误日志,最终能够分析的数据几乎为零。因此搭建了个最精简的页面,只包含配置代码和页面样式。依旧失败。
- 没有了业务代码,是否是配置本身有问题?于是在代码里加上了 try catch,发现并没有报错。失败。
这几次失败没有找到问题的原因,但至少验证了:不是业务代码的错误影响;不是本身配置语法错误的影响。那么,很有可能这个大的假设『配置失败』是错误的,至少配置的语法没有错误,因为均检测到页面 viewport 的值配置上了。于是开始另一种假设(实际上也许并没有接下来的假设,因为当时准备放弃选择另一种配置方案,然而巨大的迁移成本让我们望而怯步,幸好没有这么做)。
-
第二种假设:配置成功,但没有生效
- 没有生效的原因有很多,是否是语法兼容性的问题?尝试用
content
,setAttribute
,document.write
多种方法配置 viewport。失败。 - 是否是执行时序的问题?设置了页面加载完成后配置、延时配置。失败。
- 是否是微信内置的 webview 的兼容问题?由于问题出现时正好赶上微信客户端的一波升级,强烈怀疑下联系了微信 webview 的开发人员,反复尝试后无法复现。失败。但提出由于以后的微信 webview 使用与 safari 一样的 WK 内核,可以使用 safari 尝试复现一下。果然发现 safari 下也有类似的问题偶发,渐渐排除了对微信的怀疑。
- 是否是劫持导致的?在某次不经意发现 chrome 下也有该问题出现,并且发现有非公司域名的 js 脚本注入。于是尝试在测试页面加上外链的劫持脚本。由于安全策略,http 协议的脚本无法在 https 的域名下加载执行,因此看起来也没对配置有影响。失败。
- 没有生效的原因有很多,是否是语法兼容性的问题?尝试用
至此,看起来配置是设置成功了,但在某些条件下依旧『无法生效』。在所有条件的排除后线索断了,问题依旧无法稳定复现。不过进一步总结了问题的几个规律:页面被放大、页面无标题、fixed 定位的页面元素失效,都是页面级的影响。而且基本每次有新上线都会出现。
一次偶然的情况,再一次在 chrome 下碰到了劫持的情况,于是仔细分析了一下页面,不看不知道:
显然,这不是简单的往页面里注入 JS 脚本的劫持,而是整个页面级的劫持,将原有页面包在了 iframe 容器里,外层的页面则是劫持者的页面,而这个页面的 viewport 显然没有经过配置的,所以产生了放大。同样由于设置页面标题的代码在 iframe 内部,只能给 iframe 的窗口设置标题。再者 fixed 的定位限定在了 iframe 内部,自然不会『贴』在窗口上。一切的现象都能够得到解释,那么问题来了:如果真是被劫持了,那么 https 的页面究竟怎样被劫持的呢?
-
第三种假设:页面被劫持
- 现象都能解释的通,但还是得确认一下。于是在测试页面里判断是否我们的页面被封装在了 iframe 里,这里只用检测『本身』和『顶层』的页面是否是同一个。果然当出现问题的时候,两者并非同一个页面,那就存在 iframe 形式的劫持。成功。
- 那么是如何被劫持的呢?是否页面入口是 http 的?不幸的是,页面入口是支持 http 的,只是在 JS 的业务逻辑里会将 http 转成 https 重新请求一遍页面,这是方便但是不安全的,而且目前也有从 http 过来的流量,看起来像掩耳盗铃。但实际的测试页是 https 的,同样有问题的发生,所以即便页面 http 是开放的,也不是这次的原因。失败。
- 那是否是页面有引用 http 的静态文件导致的呢?因为网上查到有人反馈运营商会劫持并修改 https 页面中引用的 http 的静态文件,从而达到间接劫持的效果。但测试页同样驳回了这种想法,因为页面是最简单的逻辑,所有的代码都是内联而非地址引用的。失败。
- 究竟是哪个环节被劫持了呢?毫无头绪下只能求助 @flex 爸爸了。在梳理了整个请求流程后,发现前端所有的静态文件,包括入口的 html 都上线到了 cdn 上,请求到了 cdn 后,cdn 是用 http 来回源的,可能是这段路径受到了劫持。这正好与『基本每次上线都会出现』的现象一致。在配置好回源的 https 协议后,问题总算不再出现。至此,整个排查也告一段落。
回头看来,这并非一次目标明确的排查,整个过程在盲目的怀疑中徘徊了很久,其中也有很多因素制约着,比如没有完善的错误日志收集体系、没有 webview 的调试工具,前端的视野限制了我们的思维。但也有一些手段起了很大的帮助,比如搭建了一个最精简的测试环境,便于收敛分析;也关注了一些细节,比如详细分析了问题的场景和现象,进而协助目标的定位。
结果是简单的,过程还是蛮令人回味。还是那句话:排除所有不可能,剩下的一定是事情的真相。