如何在瀏覽器關閉發送請求
有的,我們需要在頁面關閉時,統計用戶在該網站瀏覽時長;有時需要告知后台,該用戶已離開...
遇到這樣的情況並不少見。
只是在此之前,有兩件很重要的事情需要區分開來:
- 如何知道瀏覽器是 關閉 還是 刷新
- 關閉時發送請求,使用哪種 請求方式 才好
頁面的生命周期函數
當前有兩種表示方式
-
DOMContentLoaded
—— 瀏覽器加載 HTML,並構建 DOM 樹,但像和樣式這樣的資源可能還沒有加載。
load
—— 瀏覽器加載所有資源(圖像,樣式等)。beforeunload/unload
—— 當用戶離開頁面時。
-
Page Visibility API 教程,Page Lifecycle API 教程
- Active 階段 網頁處於可見狀態,且擁有輸入焦點
- Passive 階段 只可能發生在桌面同時有多個窗口的情況。
- Hidden 階段 用戶的桌面被其他窗口占據,網頁不可見
- Terminated 階段 由於用戶主動關閉窗口,或者在同一個窗口前往其他頁面,導致當前頁面開始被瀏覽器卸載並從內存中清除
- Frozen 階段 網頁處於 Hidden 階段的時間過久,用戶又不關閉網頁,瀏覽器就有可能凍結網頁,使其進入 Frozen 階段。不過,也有可能,處於可見狀態的頁面長時間沒有操作,也會進入 Frozen 階段
- Discarded 階段 處於 Frozen 階段,用戶又不喚醒頁面,那么就會進入 Discarded 階段,即瀏覽器自動卸載網頁,清除該網頁的內存占用。不過,Passive 階段的網頁如果長時間沒有互動,也可能直接進入 Discarded 階段
判斷瀏覽器是 關閉 還是 刷新
用來判斷用戶是否離開
通過三種方式來進行判斷
beforeunload/unload
Terminated 階段
- 兩者結合判斷
理論知識夠了,開始進行嘗試
在此之前,由於頁面關閉時,很多方法執行不了,只能通過連接后台進行判斷,而且也很准確
var url = "http://127.0.0.1:3000/logout";
function getAajx(url) {
var client = new XMLHttpRequest();
client.open("get", url, false); // 第三個參數表明是同步的 xhr
client.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
client.send();
}
function postAajx(url, data) {
var client = new XMLHttpRequest();
client.open("post", url, false); // 第三個參數表明是同步的 xhr
client.setRequestHeader(
"Content-Type",
"application/x-www-form-urlencoded"
);
client.send(data);
}
beforeunload/unload
當瀏覽器窗口關閉或者刷新時,會觸發beforeunload事件。當前頁面不會直接關閉,可以點擊確定按鈕關閉或刷新,也可以取消關閉或刷新
由於刷新和關閉都會執行這個方法,因而在
beforeunload/unload
中無法進行區分,下面是網友們的各種思路
- 鼠標的位置,由於需要點擊關閉按鈕,因而鼠標不會在窗口內
window.addEventListener('beforeunload', function () {
var n = window.event.screenX - window.screenLeft;
var b = n > document.documentElement.scrollWidth - 20;
if ((b && window.event.clientY < 0) || window.event.altKey) {
alert("是關閉而非刷新");
window.event.returnValue = ""; //這里可以放置你想做的操作代碼
} else {
alert("是刷新而非關閉");
}
}, false)
可惜執行不了,主要利用關閉,刷新都會執行 onbeforeunload
方法
var _beforeUnload_time = 0;
var _gap_time = 0;
var is_fireFox = navigator.userAgent.indexOf("Firefox") > -1; //是否是火狐瀏覽器
window.addEventListener(
"unload",
function() {
_gap_time = new Date().getTime() - _beforeUnload_time;
if (_gap_time <= 5) {
postAajx(url, `msg=瀏覽器關閉&time=${_gap_time}`);
} else {
postAajx(url, `msg=瀏覽器刷新&time=${_gap_time}`);
}
},
false
);
window.addEventListener(
"beforeunload",
function() {
_beforeUnload_time = new Date().getTime();
if (is_fireFox) {
postAajx(url, `msg=火狐關閉&time=${_gap_time}`);
}
},
false
);
可以看出,確實是可行,刷新時長都超過 5s
,關閉在 5s
內。基於Chrome瀏覽器
需要更多地去驗證每個瀏覽器的細微差別,若是隨着瀏覽器的不斷升級,將來標准統一,有可能就沒有這樣的細微區別,導致執行不了
Terminated 階段
借助Page Lifecycle API,Page Visibility API 教程,Page Lifecycle API 教程,網友
const getState = () => {
if (document.visibilityState === "hidden") {
return "hidden";
}
if (document.hasFocus()) {
return "active";
}
return "passive";
};
// Stores the initial state using the getState() function (defined above).
let state = getState();
// Accepts a next state and, if there's been a state change, logs the
// change to the console. It also updates the state value defined above.
const logStateChange = nextState => {
const prevState = state;
if (nextState !== prevState) {
console.log(`State change: ${prevState} >>> ${nextState}`);
getAajx(url+ "?origin=" + nextState);
state = nextState;
}
};
// These lifecycle events can all use the same listener to observe state
// changes (they call the getState() function to determine the next state).
["pageshow", "focus", "blur", "visibilitychange", "resume"].forEach(
type => {
window.addEventListener(type, () => logStateChange(getState()), {
capture: true
});
}
);
// The next two listeners, on the other hand, can determine the next
// state from the event itself.
window.addEventListener(
"freeze",
() => {
// In the freeze event, the next state is always frozen.
logStateChange("frozen");
},
{ capture: true }
);
window.addEventListener(
"pagehide",
event => {
if (event.persisted) {
// If the event's persisted property is true the page is about
// to enter the page navigation cache, which is also in the frozen state.
logStateChange("frozen");
} else {
// If the event's persisted property is not true the page is
// about to be unloaded.
logStateChange("terminated");
}
},
{ capture: true }
);
由於各個瀏覽器顯示不同,上述效果不是很准確,特意使用 PageLifecycle.js再重新梳理一遍
<script src="./dist/lifecycle.es5.js"></script>
<script>
console.log(window)
lifecycle.addEventListener('statechange', function(event) {
console.log(event.oldState, event.newState);
getAajx(url+'?origin='+event.oldState+'>>>'+event.newState)
});
</script>
很遺憾,刷新跟關閉一樣,依舊無法判斷出來。
綜上: 目前真的只能靠 方案一:beforeunload/unload
時間來進行判斷了
使用什么方式進行請求
img
動態創建img 利用src
屬性進行通知,同理script的src應該也可以XMLHttpRequest
同步發送ajax請求(上述例子便是如此操作)navigator.sendBeacon()
sendBeacon 這個方法主要用於滿足統計和診斷代碼的需要,這些代碼通常嘗試在卸載(unload)文檔之前向web服務器發送數據
// 兼容性
navigator.sendBeacon = navigator.sendBeacon || new Function('var r = new XMLHttpRequest();r.open("POST",arguments[0],true);r.send(arguments[1]);');
function postForm (url, data) {
let P = new FormData()
P.append('origin', data.origin)
navigator.sendBeacon(url, P)
}
lifecycle.addEventListener('statechange', function(event) {
// getAajx(url+'?origin='+event.oldState+'>>>'+event.newState)
postForm(url, {origin: event.oldState+'>>>'+event.newState })
});
也是可以發送請求的,由於異步,所以跟同步的ajax不同,但是這個確實更合理
總結
- 瀏覽器刷新 關閉 目前只能通過時間大小來判斷了,不完美
- 發送請求
navigator.sendBeacon
這個會更好