於 window.onbeforeunload 發送HTTP Request

Amdis Liu
Amdis Liu
Sep 5 · 11 min read
Image for post
Image for post

前言

我這次的開發組合是跑在 Electron 裡的 React single page application (SPA),一開始天真地以為不就是在 window.onbeforeunload handler 裡寫個 request 而已?包 Electron 以也一樣啦!

結果完全不是這麼一回事,在網路上幾乎沒找到什麼完整的文章,花了比想像中多的時間做實驗與確認在 Electron 中依然正常運作,因此以此篇文章記錄下來,希望可以幫助其他開發者省下一些時間。

以下紀錄找過的方案們、優缺點跟包上 Electron 的小提醒。最後我選擇使用的是 Fetch + keepalive 的方式,因為對於我的環境,這是更動最少的做法。

我的開發環境為:

Mac OSX Catalina
Chrome 84
React 16.13.1
Electron: v7.1.11 (chrome 78)
Axios 0.19.2

下面將提到:

  1. 考慮過的方案們與其優缺點分析及實驗結果
  2. Chrome 將禁止於頁面關閉時發送 sync XHR
  3. Electron v7 & v8 不支援 Fetch keepalive
  4. 使不上力的 Axios

考慮過的方案們與其優缺點分析和實驗結果

fetch('/analytics', {     
method: 'POST',
body: "close-app",
keepalive: true
});
Image for post
Image for post
Request keepalive feature compatibility

根據 MDN 的敘述,這是個被視為可替代 Beacon API 的屬性。可以讓 request 的生命週期大於被卸載的頁面

The keepalive option can be used to allow the request to outlive the page. Fetch with the keepalive flag is a replacement for the Navigator.sendBeacon() API.

keepalive 被列為 experimental feature,預設值為 false ,設定此 flag 為 true 後,直接可在 window.onbeforeunload 中使用,成功地在關閉分頁後送出 request,不需藉助 Service Worker。

優點:實作簡單。

缺點:跨平台相容性不理想。

實驗結果:成功。

用 Service Worker 來送 request 時,不需要 enable keepalive

// service-worker.js
self.addEventListener('message', (event) => {
if (event.data.type === 'fetch') {
fetch(
event.data.url,
event.data.config,
);
}
});
// component side
window.addEventListener('beforeunload', () => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.controller?.postMessage({
type: 'fetch',
url: '...',
config: { ... },
});
}
});

優點:Service Worker & Fetch 跨平台與跨版本的支援度比使用 Fetch keepalive 好很多。

缺點:註冊、管理與使用 service worker 本身比較麻煩,還要等待 install、update、activate 等生命週期 ,使得 navigator.serviceWorker.controller 不保證迅速可用。

實驗結果:成功。

Image for post
Image for post
Fetch API compatibility
Image for post
Image for post
Service Worker API compatibility

有人想過要用 tab close 時跳出 confirm dialog 來偷點時間送出 request 的嗎?

window.addEventListener('beforeunload', (event) => {
event.preventDefault();
event.returnValue = 'Don\'t go';
fetch(...);
});

在 Desktop Chrome 84 上是可行的,且不需要 enable keepalive 就可以。

實驗結果:成功。

Image for post
Image for post
Beacon API compatibility
// api interface
navigator.sendBeacon(url, data);

MDN 說明來看,這個 API 本身就是設計來在 document unload 前送出 analytics & diagnostics 資料 (偏向不需要花時間去運算少量資料)。

從 API 的介面來看可知,無法更改 HTTP method,其預設的 method 是 POST。

優點:跨平台瀏覽器的支援性非常好 (硬要不看 IE),不要太舊的瀏覽器都可以運作。

缺點不能更改 HTTP method

實驗結果:未嘗試,不符合我需要的 HTTP method 。

var request = new XMLHttpRequest();
// `false` 表示是 sync request,預設為 true
request.open('GET', '/bar/foo.txt', false);
request.send(null);
Image for post
Image for post

XMLHttpRequest 是個存在很久的 API ,算是 Fetch 的前身,跨瀏覽器的支援性也非常好。查了網路上不少文章與 stackoverflow 都是使用 XMLHttpRequest 並在設定中 enable synchronous flag。根據實驗結果,在 modern Chrome 的 window beforeunload 中已經不適用。

實驗結果:失敗。

Chrome 將禁止於頁面關閉時發送 sync XHR

Chrome 於 2020–08–11 已宣告在 page dismissal (包含 beforeunload, unload, pagehidevisibilitychange ) 時禁用 synchronous XHR (原文連結:https://www.chromestatus.com/feature/4664843055398912),文中指出window beforeunload 原意是防止使用者頁面資料遺失讓開發者有時間儲存,或是需要送 analytics data 的可以送一下,但許多第三方 library 利用這個 page lifecycle API 做太多事情,花了太多時間進而傷害到使用者體驗,Edge 瀏覽器確定會跟進這個政策,目前不確定 Firefox, Safari 會不會跟上,但光 Chrome 的使用者數就已經讓前端開發者無法忽略這件事情,文中已建議如果可以,請使用 fetch + keepalive 。

Electron v7 & v8 不支援 Fetch keepalive

雖然 caiuse.com 表格顯示 Chrome 68 即開始支援 keepalive 功能,根據 Electron — Chrome 版本對照表,v7.x 為 Chrome 78 而 v8.x 為 Chrome 80 ,應該可以使用,但實際跑起來不行,直到升級至 v9.1.2 (Chrome 83) 後 Fetch keepalive 才正常運作。

使不上力的 Axios

如果有人跟我一樣,SPA 中是使用 Axios 作為發送 HTTP request 的工具, 這裡先說:目前 Axios 在 client side JS 中無法送 synchronous request ,也不支援 keepalive 功能,我自己嘗試在 window 關閉前送 request 的失敗率是 100%

Github documentation 有提供置換 httpAgent & httpsAgent 去 enable keepalive,不過只適用於 NodeJS,我不打算借用 Electron 做這件事,所以並未嘗試。

Image for post
Image for post
Axios Documentation on Github

結論

  1. 沒事真的不要在 window closed 前做太多的事,時間上不允許。

2. 參考資料 #2 提到,beforeunload 基本上是 desktop 在使用的,移動端裝置上請用 Page Visibility API

As for onbeforeunload, it should be noted that it is already unreliable. As Ilya Grigorik points out, “You cannot rely on pagehide, beforeunload, and unload events to fire on mobile platforms.” If you need to save state, you should use the Page Visibility API.

3. 非同步的事情沒辦法做。如果 HTTP request header / body 有任何東西是需要非同步拿取的 (ex. 使用 Microsoft MSAL library 取得 access token,該 lib 使用非同步的方式回傳 token,因為要 check validity & silent refresh token),都會造成 request 送出失敗。

4. 如果使用者就是不願按離開 button 正常離開,硬要用關閉 tab 來離線,後端有個 heartbeat API 是一個好方法。


感謝你讀完這篇文章,如果喜歡我的文章,用力按下旁邊的「拍手」鈕給我些鼓勵吧。鼓勵這種東西,1下不嫌少,50下不嫌多,
讓我明白花時間寫出來的文章是否有幫助到你。也歡迎在下面留言,有任何建議和指正,請務必讓我知道。

Frochu

Frochu — Frontend Ochu ,程式碼的黑手,親手實作的前端知識推動者

Amdis Liu

Written by

Amdis Liu

Web frontend / mobile developer. Editor of publication Frochu: https://medium.com/frochu .

Frochu

Frochu

Frochu — Frontend Ochu ,程式碼的黑手,親手實作的前端知識推動者

Amdis Liu

Written by

Amdis Liu

Web frontend / mobile developer. Editor of publication Frochu: https://medium.com/frochu .

Frochu

Frochu

Frochu — Frontend Ochu ,程式碼的黑手,親手實作的前端知識推動者

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store