新聞中心
在某些情況下,當(dāng)用戶跳轉(zhuǎn)到其他頁(yè)面或者提交一個(gè)表單的時(shí)候,我需要發(fā)送一個(gè) HTTP 請(qǐng)求,用于把一些數(shù)據(jù)記錄到日志中。思考如下場(chǎng)景——當(dāng)一個(gè)鏈接被點(diǎn)擊時(shí),需要發(fā)送一些信息到外部服務(wù)器:

成都創(chuàng)新互聯(lián)公司長(zhǎng)期為1000+客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對(duì)不同對(duì)象提供差異化的產(chǎn)品和服務(wù);打造開(kāi)放共贏平臺(tái),與合作伙伴共同營(yíng)造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為范縣企業(yè)提供專業(yè)的成都網(wǎng)站制作、成都做網(wǎng)站,范縣網(wǎng)站改版等技術(shù)服務(wù)。擁有10年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開(kāi)發(fā)。
Go to Page
這個(gè)示例并不復(fù)雜。鏈接的跳轉(zhuǎn)行為仍然會(huì)正常的執(zhí)行(我并沒(méi)有使用 e.preventDefault() 去阻止),但是在這個(gè)行為發(fā)生之前,單擊事件會(huì)觸發(fā)一個(gè) POST 請(qǐng)求。我們只需要它發(fā)送到我們正在訪問(wèn)的服務(wù)即可,而不需要等待這個(gè)請(qǐng)求返回。
乍一看你可能會(huì)覺(jué)得處理這個(gè)請(qǐng)求是同步的,請(qǐng)求發(fā)出后,在我們繼續(xù)跳頁(yè)面的同時(shí),其他服務(wù)器會(huì)成功地處理這個(gè)請(qǐng)求。但事實(shí)上,情況并非總是如此。
瀏覽器不能保證持續(xù)保持 HTTP 請(qǐng)求的打開(kāi)狀態(tài)
當(dāng)頁(yè)面因?yàn)槟承┰虮唤K止時(shí),瀏覽器是沒(méi)法保證正在進(jìn)行中的 HTTP 請(qǐng)求能夠成功完成(了解更多[1]關(guān)于頁(yè)面的“終止”以及頁(yè)面生命周期的其他狀態(tài))。這些請(qǐng)求的可信度取決于多個(gè)因素 —— 網(wǎng)絡(luò)連接、程序性能甚至是外部服務(wù)器自身的配置。
因此,這種情況下發(fā)出的數(shù)據(jù)可靠性很糟,如果你的業(yè)務(wù)決策依賴這些日志數(shù)據(jù),這可能會(huì)帶來(lái)一個(gè)潛在的重大隱患。
為了說(shuō)明這種場(chǎng)景的不可靠性,我編寫(xiě)了一個(gè)基于 Express 的簡(jiǎn)單應(yīng)用,并使用以上代碼實(shí)現(xiàn)了一個(gè)頁(yè)面。當(dāng)點(diǎn)擊鏈接時(shí),瀏覽器會(huì)導(dǎo)航到 /other,但此之前,會(huì)觸發(fā)一個(gè) POST 請(qǐng)求。
開(kāi)始之前,我會(huì)將開(kāi)發(fā)者工具的“網(wǎng)絡(luò)”標(biāo)簽打開(kāi),使用“低速3G”連接速度。一旦頁(yè)面加載完成,我就清除日志,事情看起來(lái)相當(dāng)正常:
圖片1
但是一旦我單擊了鏈接,事情就不太對(duì)了。當(dāng)頁(yè)面導(dǎo)航發(fā)生的時(shí)候,POST 請(qǐng)求就被取消了。
圖片2
這使得我們對(duì)外部服務(wù)實(shí)際上能夠處理完這個(gè)請(qǐng)求沒(méi)有足夠的信心。為了驗(yàn)證這個(gè)行為,當(dāng)我們以編程方式使用 ??window.location?? 導(dǎo)航時(shí),相同的情況也會(huì)發(fā)生:
document.getElementById('link').addEventListener('click', (e) => {
+ e.preventDefault();
// Request is queued, but cancelled as soon as navigation occurs.
fetch("/log", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
some: 'data'
}),
});
+ window.location = e.target.href;
});
無(wú)論導(dǎo)航是如何或何時(shí)發(fā)生的,以及活動(dòng)頁(yè)面是如何終止的,那些未完成的請(qǐng)求都有被拋棄的風(fēng)險(xiǎn)。
但是它們?yōu)槭裁磿?huì)被取消呢?
問(wèn)題的根源在于,默認(rèn)情況下 XHR 請(qǐng)求(通過(guò) fetch 或 XMLHttpRequest)是異步且非阻塞的。一旦請(qǐng)求進(jìn)入隊(duì)列,請(qǐng)求的實(shí)際工作就會(huì)交給后臺(tái)的瀏覽器級(jí) API。
從性能考慮,這是正確的行為——你并不會(huì)希望主線程被請(qǐng)求給堵塞。但是這會(huì)帶來(lái)一個(gè)風(fēng)險(xiǎn),就是當(dāng)頁(yè)面進(jìn)入“終止”狀態(tài)時(shí),這些請(qǐng)求會(huì)被拋棄,這就導(dǎo)致了在后臺(tái)運(yùn)行的服務(wù)不能保證正確完成。這是谷歌對(duì)于這個(gè)特定生命周期狀態(tài)的總結(jié)[2]:
頁(yè)面瀏覽器開(kāi)始卸載頁(yè)面并對(duì)其內(nèi)存清理時(shí),該頁(yè)面就進(jìn)入終止?fàn)顟B(tài)。在此狀態(tài)下,不會(huì)執(zhí)行任何新任務(wù)[3],同時(shí)正在處理中的任務(wù)如果運(yùn)行時(shí)間過(guò)長(zhǎng)可能會(huì)被殺死。
簡(jiǎn)單來(lái)說(shuō),瀏覽器的設(shè)計(jì)是基于這樣的假設(shè):只要頁(yè)面關(guān)閉時(shí),后臺(tái)隊(duì)列中的任何進(jìn)程都不需要再繼續(xù)執(zhí)行。
所以我們有沒(méi)有別的選擇?
似乎避免這個(gè)問(wèn)題最直接的方法是盡可能地延遲用戶操作,直到請(qǐng)求的響應(yīng)返回。在過(guò)去,通過(guò)使用 XMLHttpRequest 支持的同步標(biāo)志[4]來(lái)實(shí)現(xiàn)。但這是錯(cuò)誤的,因?yàn)槭褂眠@種方式會(huì)完全的阻斷主線程,從而造成一大堆的性能問(wèn)題——關(guān)于這個(gè)問(wèn)題我曾寫(xiě)過(guò)一些東西[5]——所以不要考慮這種方式了。事實(shí)上,平臺(tái)也正在移除這種方式(Chrome v80+ 已經(jīng)將其移除[6])。
即使你仍打算采用這種方式,也最好使用 Promise 并在其響應(yīng)返回時(shí)執(zhí)行 resolve。這樣你就可以安全地執(zhí)行該行為。對(duì)上面我們示例的代碼進(jìn)行修改:
document.getElementById('link').addEventListener('click', async (e) => {
e.preventDefault();
// Wait for response to come back...
await fetch("/log", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
some: 'data'
}),
});
// ...and THEN navigate away.
window.location = e.target.href;
});
這樣就可以完成工作了,但存在的缺點(diǎn)也不容忽視。
首先,它會(huì)使期望的行為延遲發(fā)生,這會(huì)降低用戶體驗(yàn)。 收集分析數(shù)據(jù)當(dāng)然會(huì)給商務(wù)(或許也會(huì)對(duì)潛在用戶)帶來(lái)收益,但為此收益讓既有用戶付出代價(jià)就不是一個(gè)好的選擇了。更不用說(shuō),作為外部依賴,服務(wù)本身的任何延遲或其他性能問(wèn)題都將暴露給用戶。如果因?yàn)榉治龇?wù)的超時(shí)導(dǎo)致了客戶無(wú)法完成高價(jià)值的操作,那么所有人都將蒙受損失。
其次,這種方法并不像聽(tīng)起來(lái)那樣可靠,因?yàn)橐恍┙K止行為不能通過(guò)編程方式延遲。 例如,??e.preventDefault()?? 在延遲關(guān)閉瀏覽器標(biāo)簽時(shí)是不起作用的。所以,最好的情況下,這種方式可以涵蓋一些用戶行為的數(shù)據(jù)收集,但缺乏足夠的可信度。
指示瀏覽器保持未完成的請(qǐng)求
值得高興的是,絕大多數(shù)瀏覽器都內(nèi)置了保持未完成 HTTP 請(qǐng)求的能力,而且不需要犧牲用戶體驗(yàn)。
使用 Fetch 的 keepalive 標(biāo)志
當(dāng)使用 fetch() 方法時(shí),如果把 keeplive 標(biāo)志[7]設(shè)置為 true,即便頁(yè)面被終止請(qǐng)求也會(huì)保持連接。對(duì)我們最初的用例進(jìn)行修改如下:
Go to Page
當(dāng)單擊鏈接時(shí),頁(yè)面進(jìn)行跳轉(zhuǎn),但是請(qǐng)求沒(méi)有被取消。
圖片3
事實(shí)上,我們是留下了一個(gè)(unknown)狀態(tài),這只是因?yàn)榛顒?dòng)頁(yè)面不會(huì)等待接收任何類型的響應(yīng)。
只需要添加這樣一行代碼,使得修復(fù)這個(gè)問(wèn)題看起來(lái)很簡(jiǎn)單,特別是當(dāng)它被常見(jiàn)瀏覽器的 API 支持時(shí)。但如果你想尋找一個(gè)更專業(yè)的接口方式,還有另外一種幾乎相同受到瀏覽器支持的方法。
使用 Navigator.sendBeacon() 方法
??sendbeacon()?? 方法專門(mén)用于發(fā)送單向請(qǐng)求(beacons[8])。一個(gè)基本的實(shí)現(xiàn)是這樣的,發(fā)送一個(gè)帶有 JSON 字符串和一個(gè) Content-Type 是 "text/plain" 的 POST 請(qǐng)求:
navigator.sendBeacon('/log', JSON.stringify({
some: "data"
}));
但是這個(gè) API 并不允許你設(shè)置自定義的 headers。所以,為了方便我們使用 "application/json" 格式發(fā)送數(shù)據(jù),我們需要使用 Blob 做一點(diǎn)小的調(diào)整:
Go to Page
最后,我們可以得到相同的結(jié)果——請(qǐng)求在頁(yè)面跳轉(zhuǎn)之后也可以完成。但是,還有一些情況下可能會(huì)讓它比 fetch() 更有優(yōu)勢(shì): beacons 以低優(yōu)先級(jí)發(fā)送。
為了演示說(shuō)明,以下是 Network 選項(xiàng)卡中同時(shí)使用帶 keepalive 的 fetch() 和 sendBeacon() 時(shí)的情況:
圖片4
默認(rèn)情況下,fetch() 獲得一個(gè) “高” 優(yōu)先級(jí),而 beacon(上圖中的 “ping” 類型) 具有 “最低” 優(yōu)先級(jí)。對(duì)于那些對(duì)頁(yè)面功能不是很重要的請(qǐng)求,這是一件好事。直接引用 Beacon規(guī)范[9]:
該規(guī)范定義了一個(gè)接口,該接口 […] 在確保此類請(qǐng)求仍然得到處理并交付到目的地的情況下,最大限度地減少了其與其他時(shí)間敏感操作的資源競(jìng)爭(zhēng)。
換個(gè)說(shuō)法就是,sendBeacon() 方法確保了那些程序中真正的關(guān)鍵過(guò)程和用戶體驗(yàn)不會(huì)受到影響。
給 ping 屬性榮譽(yù)提名
值得一提的是越來(lái)越多的瀏覽器開(kāi)始支持 ping 屬性[10]。當(dāng)在鏈接上設(shè)置該屬性時(shí),鏈接被點(diǎn)擊時(shí)會(huì)觸發(fā)一個(gè)小型的 POST 請(qǐng)求:
Go to Other Page
這些請(qǐng)求 headers 里會(huì)帶著鏈接所在頁(yè)面的地址(ping-from)以及鏈接 href 指向的地址(ping-to):
headers: {
'ping-from': 'http://localhost:3000/',
'ping-to': 'http://localhost:3000/other'
'content-type': 'text/ping'
// ...other headers
},
這在技術(shù)上很接近發(fā)送一個(gè) beacon,但是有一些需要注意的限制:
1. 它被嚴(yán)格的限制只能在超鏈接使用。你不能將它用于跟蹤與其他交互相關(guān)的數(shù)據(jù),比如按鈕點(diǎn)擊或表單提交。
2. 大部分瀏覽器支持的很好,但不是所有[11]。在撰寫(xiě)本文時(shí),F(xiàn)irefox還沒(méi)有默認(rèn)啟用這個(gè)功能。
3. 你不能使用其發(fā)送自定義的數(shù)據(jù)。如前面提到的,除了請(qǐng)求本身包含的 header 信息外,你最多在 header 中額外獲得幾個(gè) ping-*。
考慮以上所有因素,如果你只是要求發(fā)送簡(jiǎn)單的請(qǐng)求,并且不想編寫(xiě)任何自定義 JavaScript,那么 ping 是一個(gè)很好的工具。但如果你需要發(fā)送一些更有意義的東西,這就不是最好的選擇。
那么,究竟應(yīng)該如何選擇?
是使用 keep-alive 標(biāo)志的 fetch,還是用 sendBeacon 來(lái)發(fā)送頁(yè)面終止時(shí)的請(qǐng)求肯定需要權(quán)衡。以下建議或許可以幫助你在不同情況下做出正確的選擇:
以下情況可以選擇 fetch() + keepalive:
- 你需要簡(jiǎn)單的發(fā)送自定義 headers 的請(qǐng)求
- 你需要使用 GET 而非 POST
- 你需要兼容老舊的瀏覽器(例如 IE),并已經(jīng)有了一個(gè) fetch 方法的 polyfill
以下情況使用 sendBeacon() 或許更好:
- 你只需要發(fā)送一個(gè)簡(jiǎn)單的服務(wù)請(qǐng)求,而不需要太多的定制化
- 你喜歡更簡(jiǎn)約更優(yōu)雅的代碼方式
- 你需要保證該請(qǐng)求不會(huì)和其他更重要的請(qǐng)求競(jìng)爭(zhēng)資源
不要再踩我踩過(guò)的坑
我之所以會(huì)去深入探究頁(yè)面終止時(shí)瀏覽器是如何處理進(jìn)行中的請(qǐng)求,是因?yàn)橐欢螘r(shí)間以前,我的團(tuán)隊(duì)發(fā)現(xiàn),當(dāng)我們開(kāi)始在表單提交時(shí)發(fā)送特定分析請(qǐng)求后,該類型的分析日志的收集率突然發(fā)生了變化。這一變化是突然而顯著的——比之前下降了約30%。
通過(guò)深入研究這個(gè)問(wèn)題產(chǎn)生的原因,找到了避免它的工具,從而挽救了局面。所以,如果可以的話,我希望我對(duì)這些小挑戰(zhàn)的理解,能夠幫助你們避免那些我們?cè)冗^(guò)的坑。讓記日志變得更加愉快!
參考資料
[1]了解更多: ?https://developers.google.com/web/updates/2018/07/page-lifecycle-api?
[2]這是谷歌對(duì)于這個(gè)特定生命周期狀態(tài)的總結(jié): ?https://developers.google.com/web/updates/2018/07/page-lifecycle-api#states?
[3]新任務(wù): ?https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-task?
[4]同步標(biāo)志: ?https://xhr.spec.whatwg.org/#synchronous-flag?
[5]關(guān)于這個(gè)問(wèn)題我曾寫(xiě)過(guò)一些東西: ?https://macarthur.me/posts/use-web-workers-for-your-event-listeners?
[6]已經(jīng)將其移除: ?https://developers.google.com/web/updates/2019/12/chrome-80-deps-rems?
[7]keeplive 標(biāo)志: ?https://fetch.spec.whatwg.org/#request-keepalive-flag?
[8]beacons: ?https://w3c.github.io/beacon/#sec-processing-model?
[9]Beacon規(guī)范: ?https://www.w3.org/TR/beacon/?
[10]ping 屬性: ?https://css-tricks.com/the-ping-attribute-on-anchor-links/?
[11]但不是所有: ?https://caniuse.com/ping?
[12]參考原文: ??https://css-tricks.com/send-an-http-request-on-page-exit/??
文章名稱:離開(kāi)頁(yè)面時(shí),你知道如何可靠地發(fā)送一個(gè)HTTP請(qǐng)求嗎?
本文URL:http://m.fisionsoft.com.cn/article/cdojjcp.html


咨詢
建站咨詢
