如何在浏览器关闭发送请求
有的,我们需要在页面关闭时,统计用户在该网站浏览时长;有时需要告知后台,该用户已离开…
遇到这样的情况并不少见。
只是在此之前,有两件很重要的事情需要区分开来:
如何知道浏览器是 关闭 还是 刷新
关闭时发送请求,使用哪种 请求方式 才好
页面的生命周期函数
当前有两种表示方式
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
这个会更好