前段时间重构一个页面,页面中存在通过第三方 JavaScript 代码插入的动态广告(正常的产品需求),上线后发现第三方的广告资源存在重复请求的问题。由于控制广告插入的 JavaScript 代码由第三方提供,我们只负责按照他们要求的方式引入即可,所以对 JavaScript 代码内容并不了解,在这种情况下开始了艰难的排查过程。虽困难重重,但最终还是找到了原因,在此过程中有些收获,现将排查过程抽象如下:
注:以下过程和截图皆在 Chrome 浏览器中进行。
一、代码
<div id=”container”>
<iframe src=”/iframe-1″ frameborder=”0″></iframe>
<iframe src=”/iframe-2″ frameborder=”0″></iframe>
<iframe src=”/iframe-3″ frameborder=”0″></iframe>
</div>
<script>
document.getElementById(‘container’).innerHTML += ‘<p> 上面是 iframe</p>’;
</script>
代码大意:页面上先渲染 3 个 iframe(目前页面插入广告仍然以 iframe 作为主要实现形式),然后在最后一个 iframe 后面追加一个 p 元素
二、现象
1. 页面:渲染正常
2.Network:存在重复的异常请求(Status 是 canceled)
三、排查过程
1. 重复请求从何而来?
既然是解决重复请求的问题,那么重复请求从何而来是我们要解决的第一问题。由于请求是从第三方的 JavaScript 代码中发出的,去读第三方压缩后的 JavaScript 代码更像无头苍蝇。整个过程就像在围城之外徘徊,心急如焚。后来静下心来发现 Chrome 的 devtools 中一个很关键的排查助力神器:Network 下的 Initiator
此列是什么意思呢?通俗地说就是触发请求的位置。通过对比发现,同一个重复的请求发起的位置并不相同,以 /iframe- 1 为例:点击第一个请求的 Initiator,跳转的位置(标黄位置):
点击第二个请求的 Initiator,跳转的位置(标黄位置):
通过观察可以发现,第一个 /iframe- 1 请求是由于正常渲染 iframe 元素自动触发的,第二个 /iframe- 1 请求是在执行 JavaScript 代码(作用拼接 DOM 节点)时触发的,然而对于触发第二个 /iframe- 1 请求的那行 JavaScript 代码,其真实意图仅仅是拼接一个 p 元素而已,并不期望其他额外的事情(比如触发新的请求)发生。另外,对于 /iframe- 2 和 /iframe- 3 的第二次请求的触发点都是那段拼接 DOM 节点的 JavaScript 代码,至此,产生问题的罪魁祸首已经浮出水面,接下来我们分析下产生重复请求的原因。
2. 为什么会重复请求?
产生重复请求的 JavaScript 代码
document.getElementById(‘container’).innerHTML += ‘<p> 上面是 iframe</p>’;
翻译成:
document.getElementById(‘container’).innerHTML = document.getElementById(‘container’).innerHTML + ‘<p> 上面是 iframe</p>’;
意思就明了多了:先获取 id 为 container 的 div 元素的所有内部 HTML,将其拼接 p 元素后,再赋值给 container 的 innerHTML。
这个过程会导致 iframe 元素的重新渲染,也就会引发 iframe 对应的请求重新触发。
所以,同一个请求会触发两次的原因:页面加载时渲染 iframe 元素会触发第一次请求,执行 JavaScript 代码导致 iframe 重新渲染触发第二次请求。
找到了问题的原因,解决问题的办法也就水到渠成了,将
document.getElementById(‘container’).innerHTML += ‘<p> 上面是 iframe</p>’;
改为:
var div = document.createElement(‘div’);
var text = document.createTextNode(‘ 广告 ’);
div.appendChild(text);
document.getElementById(‘container’).appendChild(div);
问题解决了,不过,还有一个疑问:为什么渲染 iframe 产生的第一个请求的状态是 canceled?
3. 为什么重复的请求的 Status 是 canceled?
首先 Status 是 canceled 代表什么意思呢?从其字面意思理解,代表此请求被取消了,即此请求在发给服务器端之前就被浏览器取消了,也就是说此请求根本就没有从浏览器发出去,更不可能到达服务器,所以状态是 canceled 而不是 HTTP 状态码也就不难理解了。
那第一次的请求为什么会被浏览器取消呢?用关键词“chrome cancel request”谷歌了一下,在 stack overflow 上找到了一个比较全面的解答,截图如下:
其中红色标注即为我们要寻找的答案。
根据截图大概梳理一下,Chrome 浏览器会取消请求的几种场景:
触发请求的 DOM 元素被删除了(比如 img 元素还没有加载完就被删除了)
做了一些不必要的数据加载(比如开始加载 iframe 后改变其 src 或重写其内容)
大量的请求指向同一个服务器,并且前面请求的网络问题表明后续的请求也走不通(DNS 查询错误,前面的请求报 400)
至此,整个过程中的疑问点一一解开了。
四、总结
现在再回顾此 bug,产生的原因并不高深,但整个排查过程确实值得总结。小结一下:
1. 对于第三方库报错,切莫妄图通过通读并熟悉整个库后解决问题,通读代码只会浪费解决问题的时间,弄明白调用关系才是王道
2.Chrome 开发者工具中的 Network > Initiator 代表请求是从哪里触发的,对于定位请求非常有用,尤其是对于一些第三方库中发出的请求
3. 请求状态为 canceled,表示请求被浏览器取消了,并没有从浏览器发出去,更不可能进到服务器
4.Chrome 浏览器取消请求的几种情景,见上图
5.element.innerHTML += HTMLStr 表示将原有的子节点和新的节点拼接后再重新赋值,会导致节点元素重新渲染,节点内容中含有 iframe 时慎用