如何在用户离开页面时可靠地发送 HTTP 请求

更新日期: 2022-07-03阅读: 662标签: 请求

有几次,当用户执行导航到不同页面或提交表单等操作时,我需要发送带有一些数据的 HTTP 请求以进行记录。考虑这个在点击链接时向外部服务发送一些信息的人为示例:

<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"
    })
  });
});
</script>

这里没有什么非常复杂的事情发生。该链接可以正常运行(我没有使 e.preventDefault() ),但在该行为发生之前,会在单击时触发 POST 请求。无需等待任何形式的响应。我只是希望它被发送到我正在访问的任何服务。

乍一看,您可能希望该请求的分派是同步的,之后我们将继续导航离开页面,而其他服务器成功地处理该请求。但事实证明,情况并非总是如此。

浏览器不保证保留打开的HTTP请求

当浏览器中发生终止页面的情况时,并不能保证进程内的HTTP请求会成功(参见更多关于“终止”和页面生命周期的其他状态)。这些请求的可靠性可能取决于几个方面——网络连接、应用程序性能,甚至外部服务本身的配置。

因此,在这些时刻发送数据可能是不可靠的,如果您依赖这些日志来做出数据敏感的业务决策,那么这可能会带来一个潜在的重大问题。

为了帮助说明这种不可靠性,我使用上面包含的代码设置了一个带有页面的小型 Express 应用程序。单击链接时,浏览器会导航到 /other,但在此之前,会触发 POST 请求。

当一切都发生时,我打开了浏览器的网络选项卡,并且我使用的是“慢 3G”连接速度。一旦页面加载并且我已经清除了日志,事情看起来很安静:


但是一旦链接被点击,事情就会出错,当导航发生时,请求被取消。


这使得我们对外部服务是否能够处理请求缺乏信心。为了验证这种行为,当我们使用window.location以编程方式导航时也会发生这种情况:

document.getElementById('link').addEventListener('click', (e) => {
+ e.preventDefault();

  // Request is queued, but cancelled as soon as navigation occurs. 
  fetch("/log", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    }, 
    body: JSON.stringify({
      some: 'data'
    }),
  });

+ window.location = e.target.href;
});

无论导航以何种方式或何时发生,以及活动页面何时终止,那些未完成的请求都有被放弃的风险。

但是为什么被取消了呢?

问题的根源在于,默认情况下,XHR 请求(通过 fetch 或 XMLHttpRequest)是异步且非阻塞的。一旦请求被排队,请求的实际工作就会被移交给幕后的浏览器级 api

由于它与性能有关,这很好——你不希望请求占用主线程。但这也意味着当页面进入“终止”状态时,它们有被遗弃的风险,无法保证任何幕后工作都能完成。以下是 Google 对特定生命周期状态的总结:

一旦页面开始被浏览器卸载并从内存中清除,页面就处于终止状态。在这种状态下没有新的任务可以启动,并且正在进行的任务如果运行时间过长可能会被杀死。

简而言之,浏览器的设计假设当一个页面被关闭时,没有必要继续处理它排队的任何后台进程。

那么,我们有哪些选择呢?

避免这个问题最明显的方法可能是,尽可能地延迟用户操作,直到请求返回响应。在过去,通过使用XMLHttpRequest中支持的同步标志来实现这一点是错误的。使用它会完全阻塞主线程,导致大量的性能问题——我在过去写过一些这方面的文章——所以这个想法甚至不应该被接受。事实上,它正在退出平台(Chrome v80+已经删除了它)。

相反,如果您打算采用这种类型的方法,那么最好在响应返回时等待Promise解析。在它回来之后,您可以安全地执行该行为。使用我们之前的代码片段,它可能看起来像这样:

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;
});

这可以完成工作,但也有一些不小的缺点。

首先,它会延迟所需行为的发生,从而损害用户体验 。收集分析数据肯定会给业务(以及未来的用户)带来好处,但让当前用户为实现这些好处而支付成本并不理想。更不用说,作为一个外部依赖项,服务本身的任何延迟或其他性能问题都会暴露给用户。如果分析服务的暂停导致客户无法完成一项高价值的行动,那么所有人都是输家。

其次,这种方法并不像它最初听起来那么可靠,因为一些终止行为无法通过编程延迟 。例如, e.preventDefault() 无法延迟某人关闭浏览器选项卡。因此,它充其量只能涵盖为某些用户操作收集数据,但不足以全面信任它。

指示浏览器保留未完成的请求

值得庆幸的是,有一些选项可以保留绝大多数浏览器中内置的未完成的 HTTP 请求,并且不需要损害用户体验。

使用Fetch的keepalive标志

如果在使用fetch()时将keepalive标志设置为true,那么相应的请求将保持打开状态,即使发起该请求的页面被终止。使用我们最初的例子,它的实现如下所示:

<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() 函数专门用于发送单向请求(beacon)。一个基本的实现看起来像这样,发送一个带有字符串化 JSON 和“text/plain” Content-Type 的 POST:

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() 更有优势:beacon以低优先级发送。

为了演示,当同时使用带有 keepalive 的 fetch() 和 sendBeacon() 时,Network 选项卡中显示的内容如下:


默认情况下,fetch() 获得“高”优先级,而beacon(上面称为“ping”类型)具有“最低”优先级。对于对页面功能不重要的请求,这是一件好事。直接取自 Beacon 规范:

该规范定义了一个接口,[…]最大限度地减少与其他时间关键操作的资源争用,同时确保此类请求仍然被处理并交付到目的地。

换句话说,sendBeacon() 确保它的请求不会妨碍那些对您的应用程序和用户体验真正重要的请求。

因为ping属性而被光荣提及

值得一提的是,越来越多的浏览器支持 ping 属性。当附加到链接时,它会触发一个小的 POST 请求:

<a href="http://localhost:3000/other" ping="http://localhost:3000/log">
  Go to Other Page
</a>

这些请求标头将包含单击链接的页面(ping-from),以及该链接的 href 值(ping-to):

headers: {
  'ping-from': 'http://localhost:3000/',
  'ping-to': 'http://localhost:3000/other'
  'content-type': 'text/ping'
  // ...other headers
},

它在技术上类似于发送beacon,但有一些明显的限制:

它严格限制在链接上的使用 ,如果您需要跟踪与其他交互相关的数据,例如按钮点击或表单提交,这将使其无法启动。

浏览器支持很好,但不是很好 。在撰写本文时,Firefox 特别没有默认启用它。

您无法随请求一起发送任何自定义数据 。正如前面提到的,您最多只能得到几个 ping-* 头文件,以及随程序一起出现的任何其他头文件。

综合考虑,如果您可以发送简单的请求并且不想编写任何自定义 JavaScript,那么 ping 是一个很好的工具。但是,如果您需要发送更多实质内容,则可能不是最好的选择。

那么,我应该选择哪一个呢?

使用 fetch 和 keepalive 或 sendBeacon() 发送您的最后一秒请求肯定存在权衡。为了帮助辨别哪种方法最适合不同的情况,需要考虑以下几点:

如果出现以下情况,您可能会使用 fetch() + keepalive:

您需要轻松地随请求传递自定义标头。

您想向服务发出 GET 请求,而不是 POST。

您正在支持较旧的浏览器(如 IE)并且已经加载了 fetch polyfill。

但在以下情况下 sendBeacon() 可能是更好的选择:

您正在进行简单的服务请求,不需要进行太多定制。

您更喜欢更简洁、更优雅的 API。

您希望确保您的请求不会与应用程序中发送的其他高优先级请求竞争。

原文:https://css-tricks.com/send-an-http-request-on-page-exit/
作者:Alex MacArthur
翻译:前端全栈开发者

链接: https://www.fly63.com/article/detial/11847

nginx 301跳转https后post请求失效问题解决

强制把http请求跳转到https,结果发现App有部分的功能不能使用,因为App一共设置了4种请求方式,分别是GET,POST,DELETE和OPTIONS方式,设置301跳转后所有的请求方法都变成了GET方式,导致一些功能无法正常使用.

HTTP请求的11个处理阶段

几乎所以有关Nginx书只要是讲深入点的就会讲到Nginx请求的11个处理阶段,要记住这些真是不易,人脑特别不擅长记住各种东西,只能做些索引罢了,能做到知道这个知识点在哪儿能找到不就行了,可是你去面试还是问这些理论,所以这里汇总下记录如下

http请求过程的7个步骤

HTTP通信机制是在一次完整的HTTP通信过程中,Web浏览器与Web服务器之间将完成下列7个步骤:建立TCP连接、Web浏览器向Web服务器发送请求命令、Web浏览器发送请求头信息、 Web服务器应答

http请求的几种类型

http请求中的8种请求方法:opions 返回服务器针对特定资源所支持的HTML请求方法 ,Get 向特定资源发出请求,Post 向指定资源提交数据进行处理请求

nodejs http请求相关总结

通过node提供的http模块,可以通过其提供的get()和request()两个方法发起http请求,get()是对request()方法的封装,方便发起get请求,如果要实现post请求,那么需要对request()方法进行封装。

ajax异步请求302分析

遇到这样一种情况,打开网页两个窗口a,b(都是已经登录授权的),在a页面中退出登录,然后在b页面执行增删改查,这个时候因为授权原因,b页面后端的请求肯定出现异常(对这个异常的处理,进行内部跳转处理),b页面中的ajax请求的回调中就会出现问题

POST 请求的三种常见数据提交格式

本文所讲的 POST 请求是 HTTP/1.1 协议中规定的众多 HTTP 请求方法的其中最常用的一个。一般使用 POST 请求方法向服务器发送数据(主要是一些创建更新操作),本文讨论的是 POST 请求方法常用的四种数据提交格式。

flutter之网络请求dio封装,拦截器的封装

flutter一直很火的网络请求插件dio,直接上代码,写成一个类,可以直接使用,包含请求的封装,拦截器的封装

HTTP请求过程

当我们在web浏览器的地址栏中输入: www.baidu.com,然后回车,到底发生了什么?DNS域名解析采用的是递归查询的方式,过程是,先去找DNS缓存->缓存找不到就去找根域名服务器->根域名又会去找下一级

nginx是怎么处理http请求的?

nginx首先决定要用配置文件里的哪个server{}块来处理,假设有下面的server{}配置;nginx会根据过来的http请求头里的Host字段里的值,来判断使用哪个server{}。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!