统计用户在页面停留时间关闭页面时页面停留时间ajax请求无效
场景说明
当需要统计用户在页面停留时间时需要在关闭页面或刷新页面时发送异步请求到服务器,发送信息给服务端。或者统计网页直播中在线人数,当用户退出页面是需要告知服务器此人已退出等等。这些都需要在页面关闭前及时的将信息传递出去,否则就会产生数据错误。
监听事件
浏览器为我们提供了两个事件可以用来监听页面关闭,beforeunload和unload。
beforeunload
beforeunload是在文档和资源将要关闭的时候调用的, 这时候文档还是可见的,并且在这个关闭的事件还是可以取消的。beforeunload 事件处理函数返回值可以为任意类型,IE 和 Safari 浏览器的 JavaScript 解释器能够调用 toString() 方法,并把它转换为字符串显示在提示对话框中;而对于 Mozilla 浏览器来说,则会视为空字符串显示。如果 beforeunload 事件处理函数没有返回值,则不会弹出任何提示对话框,此时与 unload 事件类型响应效果相同。
比如下面这种写法就会让用户导致在刷新或者关闭页面时候,有个弹窗提醒用户是否关闭。
window.addEventListener('beforeunload',function(e) {
e.returnValue = 'https://oecom.cn';
})
unload
unload则是在页面已经正在被卸载时发生,此时文档所处的状态是:
- 所有资源仍存在(图片,iframe等);
- 对于用户所有资源不可见;
- 界面交互无效(window.open, alert, confirm 等);
- 错误不会停止卸载文档的过程。
基于以上两个方法就可以实现对页面关闭的事件监听了,为了稳妥,可以两个事件都监听。然后对监听函数做处理,让关闭事件只调用一次。
发送异步请求
有了监听事件之后,我们就可以在监听事件中做事情了。要做的当然是发送异步请求,但是你会发现关闭页签基本上不会发送出请求,有时候刷新可以发出去。如果你用的是react或vue这种框架,当组件卸载时调用也是可以执行的。
原因在于在页面关闭的时候,浏览器并不能保证异步的请求能够成功发出去,请求被浏览器abort了。
我们可以采用下面几种方式来进行解决
发送同步的ajax请求
var oAjax = new XMLHttpRequest();
oAjax.open('POST', 'https://www.oecom.cn/api/eventSend', false);//false表示同步请求
oAjax.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
oAjax.onreadystatechange = function() {
if (oAjax.readyState == 4 && oAjax.status == 200) {
var data = JSON.parse(oAjax.responseText);
} else {
console.log(oAjax);
}
};
oAjax.send('stayTime=10&version=2.0');
这种方式有一个弊端,就是需要等待请求响应之后才能关闭,会让用户造成卡顿的感觉,并不是很友好。
使用navigator.sendBeacon发送异步请求
MDN的解释是:
这个方法主要用于满足 统计和诊断代码 的需要,这些代码通常尝试在卸载(unload)文档之前向web服务器发送数据。过早的发送数据可能导致错过收集数据的机会。然而, 对于开发者来说保证在文档卸载期间发送数据一直是一个困难。因为用户代理通常会忽略在卸载事件处理器中产生的异步 XMLHttpRequest 。
从介绍上可以看出,这个方法就是用来在用户离开时发请求的。非常适合这种场景。
navigator.sendBeacon(url, data);
其接受两个参数,url参数表明 data 将要被发送到的网络地址。data 参数是将要发送的 ArrayBufferView 或 Blob, DOMString 或者 FormData 类型的数据。
使用 sendBeacon() 方法会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。此外,代码实际上还要比其他技术简单许多!
需要注意的是该方法在IE上不支持,当然新版的edge是可以。
在支持的数据类型中个人比较推荐Blob,使用blob发送的好处是可以自己定义内容的格式和header
Blob发送
blob = new Blob([`stayTime=10&version=2.0`], {type : 'application/x-www-form-urlencoded'});
navigator.sendBeacon("https://www.oecom.cn/api/eventSend", blob);
请注意:这里我没有实际的开发接口,直接调用的不存在的接口,所以返回值是404。
通过上图我们可以发现其请求方式为post,Content-Type为我们自已设置的。另外还有一个关键点就是网络请求类型是ping。
FormData发送
var fd = new FormData();
fd.append('stayTime', 10);
fd.append('version', 2.0);
navigator.sendBeacon("https://www.oecom.cn/api/eventSend", fd);
使用FormData对象时content-type会被设置成"multipart/form-data",另外也无法自定义header。
按灵活性来说Blob的方式最合适,尤其是当需要接口签名时更是如此。