前段時間重構一個頁面,頁面中存在通過第三方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 calcel 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時慎用