浏览器不保证保留打开的 HTTP 请求
当浏览器中的某个页面发生终止时,不能保证进程中的HTTP
请求会成功(请参阅有关“终止”和页面生命周期的其他状态的更多信息)。这些请求的可靠性可能取决于几件事——网络连接、应用程序性能,甚至外部服务本身的配置。
因此,在这些时刻发送数据可能并不可靠,如果您依赖这些日志来做出对数据敏感的业务决策,这会带来潜在的重大问题。
但是为什么他们被取消了?
问题的根源在于,默认情况下,XHR 请求(通过fetch
或XMLHttpRequest
)是异步且非阻塞的。一旦请求被排队,请求的实际工作就会被移交给幕后的浏览器级 API。
由于它与性能有关,这很好——您不希望请求占用主线程。但这也意味着当页面进入“终止”状态时,它们有被遗弃的风险,无法保证任何幕后工作都能完成。以下是 Google对特定生命周期状态的总结:
一旦页面开始被浏览器卸载并从内存中清除,页面就处于终止状态。在这种状态下没有新的任务可以启动,并且正在进行的任务如果运行时间过长可能会被杀死。
简而言之,浏览器的设计假设当一个页面被关闭时,没有必要继续处理它排队的任何后台进程。
指示浏览器保留未完成的请求
值得庆幸的是,有一些选项可以保留绝大多数浏览器中内置的未完成HTTP
请求,并且不需要损害用户体验。
使用 Fetch 的keepalive
标志
如果在使用时将该keepalive标志设置为,则相应的请求将保持打开状态,即使发起该请求的页面已终止。使用我们最初的示例,这将使实现看起来像这样:true
fetch()
<a href="/some-other-page" id="link">Go to Page</a>
<script>
document.getElementById('link').addEventListener('click', (e) => {
fetch("/log", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
some: "data"
}),
keepalive: true
});
});
</script>
像这样的单行代码很容易解决,尤其是当它是常用浏览器 API 的一部分时。但是,如果您正在寻找具有更简单界面的更专注的选项,那么还有另一种具有几乎相同浏览器支持的方法。
使用Navigator.sendBeacon()
该Navigator.sendBeacon()
函数专门用于发送单向请求(信标)。一个基本的实现看起来像这样,发送一个POST
带有字符串化的 JSON 和一个“text/plain” Content-Type
:
navigator.sendBeacon('/log', JSON.stringify({
some: "data"
}));
但是此 API 不允许您发送自定义标头。因此,为了让我们以“application/json”的形式发送数据,我们需要做一些小调整并使用Blob
:
<a href="/some-other-page" id="link">Go to Page</a>
<script>
document.getElementById('link').addEventListener('click', (e) => {
const blob = new Blob([JSON.stringify({ some: "data" })], { type: 'application/json; charset=UTF-8' });
navigator.sendBeacon('/log', blob));
});
</script>
最后,我们得到了相同的结果——即使在页面导航之后也允许完成的请求。但是还有更多的事情可能会使其具有优势fetch()
:信标以低优先级发送。
那么,我应该接触哪一个?
使用fetch
withkeepalive
或sendBeacon()
发送你的最后一秒请求肯定有权衡。为了帮助辨别哪种方法最适合不同的情况,需要考虑以下几点:
如果fetch()
:_keepalive
- 您需要轻松地随请求传递自定义标头。
- 您想向
GET
服务发出请求,而不是POST
. - 您正在支持较旧的浏览器(如 IE)并且已经
fetch
加载了一个 polyfill。
但sendBeacon()
在以下情况下可能是更好的选择:
- 您正在发出不需要太多自定义的简单服务请求。
- 您更喜欢更简洁、更优雅的 API。
- 您希望确保您的请求不会与应用程序中发送的其他高优先级请求竞争。