我們經(jīng)常遇到一個經(jīng)典場景:用戶即將關(guān)閉頁面或瀏覽器標(biāo)簽,而我們需要在此刻抓住最后的機會,向服務(wù)器發(fā)送一些重要信息。
然而,這看似簡單的需求,在實踐中卻充滿了挑戰(zhàn)。傳統(tǒng)的異步請求(如 fetch
或 XMLHttpRequest
)在頁面卸載事件中極有可能被瀏覽器中斷,導(dǎo)致請求失敗。
問題的根源:為什么常規(guī)請求會失???
當(dāng)用戶關(guān)閉一個標(biāo)簽頁時,瀏覽器會觸發(fā)一系列頁面卸載(Unload)事件,如 pagehide
和 unload
。
在這個過程中,任何在 unload
事件處理器中發(fā)起的標(biāo)準(zhǔn)異步 fetch
或 XMLHttpRequest
請求都會面臨一個問題:請求剛剛發(fā)出,頁面就已經(jīng)被銷毀了。
由于頁面的 JavaScript 執(zhí)行環(huán)境已不復(fù)存在,瀏覽器沒有義務(wù)繼續(xù)完成這個請求,因此會主動取消它。
過去,開發(fā)者為了解決這個問題,會使用同步的 XMLHttpRequest
。它會強制阻塞主線程,直到請求完成。這種方法雖然“有效”,但對用戶體驗是毀滅性的——它會導(dǎo)致瀏覽器 UI 卡死,頁面無法響應(yīng),直到網(wǎng)絡(luò)請求結(jié)束。
那么,我們該如何在不破壞用戶體驗的前提下,可靠地發(fā)送這“最后一封信”呢?
現(xiàn)代解決方案一:navigator.sendBeacon()
navigator.sendBeacon()
是 W3C 專門為解決此類問題而設(shè)計的 API。它的核心使命就是:以異步、非阻塞的方式,可靠地將少量數(shù)據(jù)發(fā)送到服務(wù)器。
工作原理
當(dāng)我們調(diào)用 sendBeacon()
時,瀏覽器會將這個請求添加到一個內(nèi)部隊列中,然后立即返回,不會阻塞頁面卸載。瀏覽器會保證在合適的時機(例如在后臺)發(fā)送這個請求,即使發(fā)起請求的頁面已經(jīng)關(guān)閉。
特點
- 可靠性高:由瀏覽器保證發(fā)送,不受頁面卸載影響
- 異步非阻塞:不影響用戶關(guān)閉頁面的速度和體驗
- 使用簡單:API 非常直觀
- 數(shù)據(jù)有限制:只能單向發(fā)送 POST 請求,且無法自定義請求頭(Headers)
代碼示例
假設(shè)我們需要在用戶離開頁面時,發(fā)送一條包含頁面停留時間的分析日志。
// 推薦使用 'pagehide' 事件,它比 'unload' 更可靠
window.addEventListener('pagehide', (event) => {
// event.persisted 為 true 表示頁面進入了往返緩存 (bfcache),并未真正卸載
// 這種情況下我們通常不發(fā)送信標(biāo)
if (event.persisted) {
return;
}
const analyticsData = {
timeOnPage: Math.round(performance.now()),
lastAction: 'close_tab',
};
// 將數(shù)據(jù)轉(zhuǎn)換為 Blob,這是 sendBeacon 支持的格式之一
const blob = new Blob([JSON.stringify(analyticsData)], {
type: 'application/json; charset=UTF-8',
});
// 使用 sendBeacon 發(fā)送數(shù)據(jù)
// 該方法會返回 true (成功加入隊列) 或 false (數(shù)據(jù)過大或格式錯誤)
const success = navigator.sendBeacon('/log-analytics', blob);
if (success) {
console.log('分析日志已成功加入發(fā)送隊列。');
} else {
console.error('無法發(fā)送分析日志。');
}
});
現(xiàn)代解決方案二:fetch()
與 keepalive: true
fetch
API 作為現(xiàn)代網(wǎng)絡(luò)請求的基石,也提供了一種優(yōu)雅的解決方案。通過在 fetch
的 init
對象中設(shè)置 keepalive: true
,我們可以告訴瀏覽器:“這個請求很重要,請在頁面卸載后繼續(xù)完成它。”
工作原理
fetch({ keepalive: true })
的工作方式與 sendBeacon
類似。它將一個 fetch
請求標(biāo)記為“持續(xù)活動”,使其生命周期可以超越當(dāng)前頁面。瀏覽器會像處理 sendBeacon
請求一樣,在后臺處理它。
特點
- 靈活性高:相比
sendBeacon
,它支持更多的 HTTP 方法(如 POST
, PUT
等),并允許有限的請求頭自定義 - API 統(tǒng)一:如果我們項目中已經(jīng)大量使用
fetch
,使用 keepalive
可以保持代碼風(fēng)格一致 - 同樣無法處理響應(yīng):和
sendBeacon
一樣,由于頁面已經(jīng)關(guān)閉,我們無法在前端讀取或處理服務(wù)器返回的響應(yīng)
代碼示例
假設(shè)我們需要在用戶關(guān)閉頁面時,自動保存文本編輯器中的草稿。使用 PUT
請求可能更符合 RESTful 風(fēng)格。
window.addEventListener('pagehide', (event) => {
if (event.persisted) {
return;
}
const draftContent = document.getElementById('editor').value;
if (!draftContent) return;
const draftData = {
content: draftContent,
timestamp: Date.now(),
};
// 使用 fetch 和 keepalive: true
// 注意:即使請求成功,這里的 .then 和 .catch 也可能不會執(zhí)行,因為頁面正在卸載
try {
fetch('/api/drafts/save', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(draftData),
// 這是關(guān)鍵!
keepalive: true,
});
console.log('保存草稿的請求已提交。');
} catch (e) {
// 這個 catch 塊很可能不會捕獲到網(wǎng)絡(luò)錯誤
console.error('提交保存草稿請求時發(fā)生錯誤:', e);
}
});
如何選擇?
如果我們的需求是發(fā)送簡單的分析或日志數(shù)據(jù),navigator.sendBeacon()
是最直接、最符合語義的選擇。而如果我們需要更大的靈活性,比如使用 PUT
方法更新資源或需要設(shè)置特定的請求頭,fetch({ keepalive: true })
是更好的選擇。
這兩個現(xiàn)代 API,我們可以在不犧牲用戶體驗的前提下,確保關(guān)鍵數(shù)據(jù)的成功送達。
該文章在 2025/10/9 10:51:35 編輯過