微信 H5 页面兼容性——卸载页面时可靠的发送 HTTP 请求
有时候,当用户离开网页时,需要向服务器提交一些用户行为的统计信息。这时,传统的请求方式,数据可能无法发送,因为浏览器已经把页面卸载了。
为了能够成功发出请求,并让服务器处理,需要解决两个问题:
- 监听微信 H5 页面的卸载;
- 在页面卸载事件里,在浏览器后台保持 HTTP 连接,继续发送数据。可靠的发送请求。
1. 监听 H5 页面的卸载
页面的卸载,细分为:
- 锁屏
- 切换页面
- 切换到其它 app
- 关闭当前页面
以上情况,都会导致手机将浏览器进程切换到后台,然后为了节省资源,可能就会杀死浏览器进程。为了解决这个问题,就诞生了 Page Visibility API
。不管手机或桌面电脑,所有情况下,这个 API 都会监听到页面的可见性发生变化。
这个新的 API 的意义在于,通过监听网页的可见性,可以预判网页的卸载,还可以用来节省资源,减缓电能的消耗。比如,一旦用户不看网页,下面这些网页行为都是可以暂停的。
- 对服务器的轮询
- 网页动画
- 正在播放的音频或视频
只要页面的可见性发生了变化,就会触发 visibilitychange
事件。因此,可以通过监听这个事件(通过 document.addEventListener()
方法或 document.onvisibilitychange
属性),跟踪页面可见性的变化。
document.addEventListener('visibilitychange', function () {
// 用户离开了当前页面
if (document.visibilityState === 'hidden') {
document.title = '页面不可见';
}
// 用户打开或回到页面
if (document.visibilityState === 'visible') {
document.title = '页面可见';
}
});
如果是在使用 vant 组件库,以 vue3 项目为例,可以通过监听 pageVisibility
事件,来处理 visible (可见)/ hidden (影藏)逻辑
const pageVisibility = usePageVisibility();
watch(pageVisibility, (value) => {
console.log('VisibilityState: ', value); // type VisibilityState = 'visible' | 'hidden'; 页面当前的可见状态,visible 为可见,hidden 为隐藏
});
2. 微信浏览器中页面卸载兼容问题
手机锁屏、切换页面、切换到其它 app ,通过监听 visibilitychange
事件可以实现。但是点击浏览器左上角叉,或者 IOS 设备底部返回上一步却无法监听到。
这时,android
系统设备可以通过 unload
监听到点击浏览器左上角叉,IOS
系统设备可以通过 pagehide
监听点击设备底部返回上一步。
const isIOS = (): boolean => {
const u = navigator.userAgent;
return !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
};
// android 系统设备
function unloadHandler() {
const isIOSDevice = isIOS();
if (!isIOSDevice) {
// 处理逻辑
}
}
window.addEventListener('unload', unloadHandler, false);
window.removeEventListener('unload', unloadHandler, false);
// IOS 系统设备
function pagehideHandler() {
const isIOSDevice = isIOS();
if (isIOSDevice) {
// 处理逻辑
}
}
window.addEventListener('pagehide', pagehideHandler, false);
window.removeEventListener('pagehide', pagehideHandler, false);
3. 浏览器处理 HTTP 请求
当浏览器中的某个页面发生终止时,不能保证进程中的 HTTP 请求会成功。这些请求的可靠性可能取决于几件事——网络连接、应用程序性能,甚至外部服务本身的配置。
默认情况下,XHR
请求(通过 fetch
或 XMLHttpRequest
)是异步且非阻塞的。一旦请求排队,请求的实际工作就会被移交给幕后的浏览器级 API。
一旦页面开始被浏览器卸载并从内存中清除,页面就处于终止状态。在这种状态下没有新的任务可以启动,并且正在进行的任务如果运行时间过长可能会被杀死。
避免此问题的最明显方法可能是尽可能延迟用户操作,直到请求返回响应。通过使用 async/await
直到请求返回响应再真正卸载页面。
document.getElementById('link').addEventListener('click', async (e) => {
e.preventDefault();
// Wait for response to come back...
await fetch("/log", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
some: 'data'
}),
});
// ...and THEN navigate away.
window.location = e.target.href;
});
这样做会反生延迟,让用户看起来页面卡死了,跳转其他页面没反应。
这个问题可以 使用 Fetch API
的 keepalive
标志解决,告诉浏览器在后台保持连接,继续发送数据。
document.getElementById('link').addEventListener('click', (e) => {
fetch("/log", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
some: 'data'
}),
keepalive: true
});
});
4. Navigator.sendBeacon()
navigator.sendBeacon() 方法可用于通过 HTTP POST 将少量数据 异步 传输到 Web 服务器。它主要用于将统计数据发送到 Web 服务器,同时避免了用传统技术(如:XMLHttpRequest)发送分析数据的一些问题。
通常尝试在卸载(unload)文档之前向 Web 服务器发送数据。 异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能,这意味着:
- 数据发送是可靠的。
- 数据异步传输。
- 不影响下一导航的载入。
- 数据是通过 HTTP POST 请求发送的。
document.addEventListener("visibilitychange", function logData() {
if (document.visibilityState === "hidden") {
navigator.sendBeacon("/log", analyticsData);
}
});
5. fetch:keepalive
和 Navigator.sendBeacon()
使用选择
适合用 fetch:keepalive
方法:
- 需要轻松地随请求传递自定义标头。
- 想向
GET
服务发出请求,而不是POST
。 - 正在支持较旧的浏览器(如 IE)并且已经加载了
polyfill
。
sendBeacon()
在以下情况下可能是更好的选择:
- 正在发出不需要太多自定义的简单服务请求。
- 更喜欢更简洁、更优雅的 API。
- 希望确保您的请求不会与应用程序中发送的其他高优先级请求竞争。