场景描述
很多时候我们需要在离开页面的时候,需要发送预设的埋点事件,做web的页面埋点需求,用于后续的数据统计。但是我们知道,正常的单页面应用Ajax请求会在页面卸载的时候被取消,这个Ajax上传的数据很大程度上会丢失。
解决方案
- 发送同步Ajax。
- 动态图片。
- navigator.sendBeacon。
- 最终方案(保证数据有效性,并减小对用户体验的不良影响)。
发送同步Ajax
通过修改Ajax请求为同步请求,阻塞页面,等待请求结束之后,再触发页面跳转。
用法示例
// ajax 请求
function ajaxrequest(type) {
if (XMLHttpRequest) {
var xhrRequest = new XMLHttpRequest();
xhrRequest.open(method, `${url}?${params.join('&')}`, type);
xhrRequest.send();
}
}
发送同步Ajax案总结
优势
- 在网络良好的情况下,可以保证信息上报的稳定性。
劣势
- 会存在阻塞页面跳转的情况,尤其在网络差的环境下,用户体验很不好。
- 部分浏览器已经开始不支持ajax同步请求
动态图片
道理和同步ajax差不多,都是通过阻塞的方式去完成请求,通过在unload事件处理器中,创建一个图片元素并设置它的 src 属性的方法来延迟卸载以保证数据的发送。因为绝大多数浏览器会延迟卸载以保证图片的载入,所以数据可以在卸载事件中发送。但是需要服务端配合解析请求url。
用法示例
// 伪装image请求
function imageRequest() {
const params = [];
Object.keys(datas).forEach((key) => {
params.push(`${key}=${encodeURIComponent(datas[key])}`);
});
var img = document.createElement('img');
img.onload = function() {
console.log('success');
img = null;
};
img.onerror = function() {
console.log('error');
img = null;
};
img.src = `${url}?${params.join('&')}`;;
}
动态图片方案总结
优势
- 在有网络的情况下,可以保证信息上报的稳定性。
劣势
- 该方案需要服务端配合,构建好传输数据的格式,且只能get的形式。
- 会存在阻塞页面跳转的情况,用户体验不是很良好。
navigator.sendBeacon
这个方法主要用于满足统计和诊断代码的需要,这些代码通常尝试在卸载(unload)文档之前向web服务器发送数据。过早的发送数据可能导致错过收集数据的机会。然而,对于开发者来说保证在文档卸载期间发送数据一直是一个困难。因为用户代理通常会忽略在 unload 事件处理器中产生的异步 XMLHttpRequest。
用法示例
function sendBeaconRequest() {
if (window.navigator.sendBeacon) {
window.navigator.sendBeacon(url, JSON.stringify(datas));
}
}
sendBeacon 问题总结
优势
- 用户无感知,不阻塞页面跳转,不影响用户体验。
劣势
- 存在兼容性问题。
- 大量发送数据,无法保证数据发送的可靠性。在ios上测试,能连续发送15次左右,就无法继续发送。
最终方案
保证数据有效性、准确性的基础上,并减小对用户体验的不良影响,本方案采用动态图片的方案用于用户行为的上报。
方案
通过管理所有的Ajax请求以及监听监听页面的foreunload事件(浏览器有两个事件可以用来监听页面关闭,beforeunload和unload。 beforeunload是在文档和资源将要关闭的时候调用的。==unload事件好像有点问题==,这个时候初始化image并不会发送),然后给此时未完成的Ajax请求abort。未完成的请求,通过动态image的方案发送到服务器端。
unload则是在页面已经正在被卸载时发生,此时文档所处的状态是:1.所有资源仍存在(图片,iframe等);2.对于用户所有资源不可见;3.界面交互无效(window.open, alert, confirm 等);4.错误不会停止卸载文档的过程。
优劣势见上述动态image方案。
用法示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id='final'>finalProject</div>
</body>
</html>
<script type="text/javascript">
// 本地启动的node服务
let url = 'http://localhost:8090/uploadUserAction';
let method = 'GET';
let datas = {
data1: 'haha',
token: 'test'
};
let params = [];
Object.keys(datas).forEach((key) => {
params.push(`${key}=${encodeURIComponent(datas[key])}`);
});
// Ajax 请求列表
let requestList = [];
let final = document.getElementById('final');
// step1: 异步ajax请求
final.onclick = function () {
// 模拟同时发送10个请求,服务端也做了延时,用于模仿请求
for(let a =0; a < 10; a++){
ajaxrequest(a);
}
setTimeout(function() {
pushNewPage();
}, 1500);
}
// ajax 请求
function ajaxrequest(key) {
var xhr = new XMLHttpRequest();
// 生成唯一的key 用于识别xhr
xhr.open(method, `${url}?${params.join('&')}&key=${key}`);
xhr.send(datas);
requestList.push({
key,
xhr
})
// 监听ajax请求变化
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
// 当请求完成的时候,从请求列表移除
requestList.forEach((item, index) => {
if (item.key === key) requestList.splice(index, 1);
});
}
}
}
// step2: 监听页面即将卸载。处理未完成的请求
window.addEventListener('beforeunload', function(event) {
if (requestList && requestList.length) {
requestList.map((item, index) => {
item.xhr.abort();
imageRequest(item.key);
});
}
});
// 监听页面卸载。处理未完成的请求
window.addEventListener('unload', function(event) {});
// step3: 伪装成image请求
function imageRequest(key) {
var img = document.createElement('img');
img.onload = function() {
console.log('success');
img = null;
};
img.onerror = function() {
console.log('error');
img = null;
};
img.src = `${url}?${params.join('&')}&key=${key}`;;
}
// 页面跳转
function pushNewPage() {
window.location.href = 'http://www.baidu.com'
}
</script>
1-3方案代码示例
<html>
<head>
<title>test</title>
</head>
<body>
<div id='asyncAjax'>asyncAjax</div>
<div id='syncAjax'>syncAjax</div>
<div id='dynamicImage'>dynamicImage</div>
<div id='sendBeacon'>sendBeacon</div>
<div id='final'>finalProject</div>
</body>
</html>
<script type="text/javascript">
// 本地启动的node服务
var url = 'http://localhost:8090/uploadUserAction';
var method = 'GET';
var datas = {
data1: '111',
token: 'test'
};
var params = [];
Object.keys(datas).forEach((key) => {
params.push(`${key}=${encodeURIComponent(datas[key])}`);
});
var asyncAjax = document.getElementById('asyncAjax');
var syncAjax = document.getElementById('syncAjax');
var dynamicImage = document.getElementById('dynamicImage');
var sendBeaconClick = document.getElementById('sendBeacon');
var delayed = document.getElementById('delayed');
// 异步ajax请求
asyncAjax.onclick = function () {
ajaxrequest(true);
// pushNewPage();
}
// 同步ajax请求
syncAjax.onclick = function () {
ajaxrequest(false);
// pushNewPage();
}
// 动态图片请求
dynamicImage.onclick = function () {
imageRequest();
pushNewPage();
}
// sendBeacon 发送请求
sendBeaconClick.onclick = function () {
sendBeaconRequest();
// pushNewPage();
}
// ajax 请求
function ajaxrequest(type) {
if (XMLHttpRequest) {
var xhrRequest = new XMLHttpRequest();
xhrRequest.open(method, `${url}?${params.join('&')}`, type);
xhrRequest.send();
}
}
// 伪装image请求
function imageRequest() {
var img = document.createElement('img');
img.onload = function() {
console.log('success');
img = null;
};
img.onerror = function() {
console.log('error');
img = null;
};
img.src = `${url}?${params.join('&')}`;;
}
function sendBeaconRequest() {
if (window.navigator.sendBeacon) {
window.navigator.sendBeacon(url, JSON.stringify(datas));
}
}
// 页面跳转
function pushNewPage() {
window.location.href = 'http://www.baidu.com';
}
</script>
node代码示例
const express = require('express');
const app = express();
const Response = require('express/lib/response');
Response.sendFormatData = function (code, data, message = 'succeed') {
this.send({
code,
data,
message,
});
};
app.listen(8090, () => {
console.log('服务启动')
});
app.all('*', function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('Access-Control-Allow-Methods', '*');
res.header('Content-Type', 'application/json;charset=utf-8');
next();
});
app.get('/uploadUserAction', (req, res) => {
console.log(req.url)
setTimeout(function() {
res.sendFormatData('200', {data: 'succeed'},'请求成功')
}, 1000)
})