如何处理页面关闭时发送HTTP请求?

简介: 在实际项目开发中,可能会遇到这样的业务问题:如何在用户离开或关闭页面时发送HTTP请求给服务端?可能有人会觉得页面都关闭了,还需要发送什么请求,完全没必要噻。但如果真有这样的业务需求落到自己的头上,那么我们应该如何来实现呢?

在实际项目开发中,可能会遇到这样的业务问题:如何在用户离开或关闭页面时发送HTTP请求给服务端?可能有人会觉得页面都关闭了,还需要发送什么请求,完全没必要噻。但如果真有这样的业务需求落到自己的头上,那么我们应该如何来实现呢?

注:本文章基于Chrome 76,高版本的Chrome浏览器测试效果可能会有差异

关闭或离开页面

可能使用Vue的朋友会比较熟悉beforeDestoryonBeforeUnMounted这两个API,用来处理组件销毁前的事件。其实js中也有类似的方法:beforeunload

beforeunload 会在浏览器关闭页面或刷新页面时触发,可以点击确定按钮关闭或刷新,也可以取消关闭或刷新,使用方法:

window.addEventListener('beforeunload', (event) => {
  // 阻止浏览器默认事件,也就是阻止关闭和刷新页面
  event.preventDefault();
  // chrome浏览器需要设置返回值
  event.returnValue = true;
});

如果是离开页面,应该怎么处理呢?假设我们在页面上有一个链接,点击后会跳转到另一个页面:

<a href="https://baidu.com" id="link">点击跳转</a>
// js
document.getElementById("link").addEventListener('click', (e) => {
    e.preventDefault(); // 阻止浏览器默认事件,点击链接就不会发生跳转
    window.location = e.target.href;
})

明白了这两个问题后,我们再来看接下来的问题

HTTP请求canceled?

js是单线程的,因此网络请求,包括fetch和XMLHttpRequest请求,被设计成是异步且非阻塞的。异步操作有一个好处,就是它不会占用主进程,但是这也会带来问题,如果主进程销毁了,例如页面关闭或者离开当前页面,那么原来异步进行的网络请求可能会被忽略。直观的体现就是我们可以在network中看到请求已经canceled。举个栗子:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <a href="/other.html" id="link">离开页面</a>

    <script>
      document.getElementById("link").addEventListener("click", (e) => {
        fetch("http://localhost:8088/log", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            data: "data",
          })
        });
      });
    </script>
  </body>
</html>

关于这个例子(下同)有几点需要说明:

  • 我们使用的是ES6新增的fetch()发送HTTP请求,而不是额外引入axios,关于fetch的使用可以参考阮一峰老师的教程:Fetch API 教程
  • fetch()中传入的是请求后端接口地址,我们这里使用的是express框架搭建的简易后端,本次案例中未实现,详细的使用后面会有单独的文章介绍
  • 案例模拟的是离开页面的场景,关闭页面需要借助于beforeunloadunload,两者相差不大,掌握原理才是最重要的

执行过程(下同):

  • 打开控制台,并选择network,将网络状态选为slow 3g,这么做的目的是让我们能更清晰的看到执行过程
  • 点击“离开页面”,我们可以看到会有一个fetch请求处于pending状态
  • 然后页面跳转到other.html,可以发现刚才的log请求的状态变为了canceled,也就是被取消了

运行结果:

image-20220625212805843.png

可以看到,我们刚刚的log请求处于被取消的状态,如果是在实际业务场景中,那么就有可能导致我们的业务请求没有能发送到服务端。那么我们应该怎么解决呢?

如何解决这个问题

解决HTTP请求被canceled的问题,常见的一般有这么几个解决方案:

  • async/await
  • fetch + keepalive
  • navigator.sendBeacon()
  • ping

接下来就逐一讨论:

async/await

fetch()接口返回的是一个Promise()对象,因此我们可以等待fetch()接口完成后才执行页面跳转,如果使用的是axios,这里也是一样的:

document.getElementById("link").addEventListener("click", (e) => {
    e.preventDefault(); // 阻止默认跳转行为
    fetch("http://localhost:8088/log", {
        method: "POST",
        headers: {
        "Content-Type": "application/json",
        },
        body: JSON.stringify({
            someData: "222",
        })
    }).then(() => {
        window.location = e.target.href; // 页面跳转
    });
});

或者使用async/await

document.getElementById("link").addEventListener("click", async (e) => {
    e.preventDefault(); // 阻止默认跳转行为
    await fetch("http://localhost:8088/log", {
        method: "POST",
        headers: {
        "Content-Type": "application/json",
        },
        body: JSON.stringify({
            some: "222",
        })
    });// 同步
    window.location = e.target.href; // 页面跳转
});

运行结果:

我们可以明显的看到,虽然点击了链接,但是需要等到请求结束后才会执行跳转,也就是会有一个等待的过程。如果网络请求事件太长,这将会是一个很糟糕的体验。

image-20220625214948827.png

fetch + keepalive

keepalivefetch的一个属性,目的是告诉浏览器,即使页面卸载了,也要在后台保持连接,继续发送数据。用法也比较简单,直接传入true即可,默认是false:

document.getElementById("link").addEventListener("click", (e) => {
    fetch("http://localhost:8088/log", {
        method: "POST", // fetch支持GET、POST、PUT、DELETE的请求方法
        headers: {
        "Content-Type": "application/json",
        }, // 请求头
        body: JSON.stringify({
            some: "222",
        }), // 请求数据
        keepalive: true, // 保持在后台连接
    });
});

使用keepalive是简单且有效的,那如果我们想要追求更简单的方式呢

navigator.sendBeacon()

navigator.sendBeacon()方法可用于通过http post的方式将少量数据异步传输到服务器,它的实现原理和传统的XMLHttpRequest有所区别。使用方式一般有两种:

navigator.sendBeacon(url);
navigator.sendBeacon(url, data);

其中,data表示需要发送的Blob、FormData、ArrayBuffer等类型的数据。使用navigator.sendBeacon()无法自定义请求头部,我们可以借助于Blob对象来简单封装请求头和请求数据:

document.getElementById("link").addEventListener("click", (e) => {
    // 自定义请求头
    // const blob = new Blob([JSON.stringify({ some: "data" })], { type: 'application/json; charset=UTF-8' });
    // navigator.sendBeacon('http://localhost:8088/log', blob);
    // 直接发送
    navigator.sendBeacon('http://localhost:8088/log');
});

ping

这或许是最简单的解决方式了吧,ping包含一个以空格分割的url列表,传入的值为字符串,在超链接(a标签)中使用时,浏览器会在后台发送带有正文ping的POST请求。使用方式如下:

<a href="/other.html" id="link" ping="http://localhost:8088/log">离开页面</a>

直接使用在a标签上面即可,注意一定是有意义的a标签,这样写就不行了:

<a id="link" ping="http://localhost:8088/log">离开页面</a>

兼容性:

image-20220625223130672.png

对比四种方式的优缺点

目前能想到的就这四种,说了这么多,简单总结一下它们的优缺点,其实并没有谁对谁错,只是使用的场景不同。

async/await

  • 需要先阻止浏览器的默认事件,等到请求结束后再执行
  • 由于需要等待网络请求执行完成,因此会导致用户长时间得不到反馈
  • 如果业务场景不在乎等待时间,可以考虑

fetch + keepalive

  • fetch接口自带的属性,无需额外引入
  • 如果请求需要支持GET、POST、PUT、DELETE等,可以选择使用fetch
  • 兼容性较好,除了IE不支持

navigator.sendBeacon()

  • HTTP请求只能是POST请求
  • 发送的数据量少,并且需要更加简洁的API
  • 该请求的优先级较低,不会与其他HTTP请求竞争资源
  • 兼容性较好,除了IE不支持

ping

  • 足够简单,仅依靠HTML就能完成,无需借助JavaScript
  • 不会阻塞页面后续行为,与navigator.sendBeacon()类似;并且支持跨域
  • 目前支持者a标签,其他元素设置ping属性是没有效果的
  • 只能是POST请求,不能发送GET请求
  • 无法自定义请求数据
  • 兼容性很好,除了IE不支持,FireFox默认未启用,需要再FireFox设置中开启

总结

本文总结了前端处理页面关闭时如何保证HTTP请求能顺利发送出去,总的来说有四种方式。每一种方式都有优缺点,需要读者自己权衡如何在实际业务场景种使用。比较推荐的是fetch + keepalivenavigator.sendBeacon()。复现文章中的代码,需要先检查一下浏览器版本以及是否清空了缓存。

相关文章
|
4天前
|
JSON API 数据格式
Python网络编程:HTTP请求(requests模块)
在现代编程中,HTTP请求几乎无处不在。无论是数据抓取、API调用还是与远程服务器进行交互,HTTP请求都是不可或缺的一部分。在Python中,requests模块被广泛认为是发送HTTP请求的最简便和强大的工具之一。本文将详细介绍requests模块的功能,并通过一个综合示例展示其应用。
|
9天前
|
Python
HTTP 请求
【8月更文挑战第11天】
25 12
|
13天前
|
存储 物联网 测试技术
Golang中的HTTP请求凝聚器
Golang中的HTTP请求凝聚器
|
6天前
|
数据采集 API 开发者
.NET 8新特性:使用ConfigurePrimaryHttpMessageHandler定制HTTP请求
在.NET 8中,通过`ConfigurePrimaryHttpMessageHandler`方法,开发者能更精细地控制HTTP请求,这对于构建高效爬虫尤为重要。此特性支持定制代理IP、管理Cookie与User-Agent,结合多线程技术,有效应对网络限制及提高数据采集效率。示例代码展示了如何设置代理服务器、模拟用户行为及并发请求,从而在遵守网站规则的同时,实现快速稳定的数据抓取。
.NET 8新特性:使用ConfigurePrimaryHttpMessageHandler定制HTTP请求
|
8天前
|
缓存 数据安全/隐私保护 UED
代理服务器在HTTP请求中的应用:Ruby实例
代理服务器在HTTP请求中的应用:Ruby实例
|
8天前
|
开发者 Python
深入解析Python `requests`库源码,揭开HTTP请求的神秘面纱!
深入解析Python `requests`库源码,揭开HTTP请求的神秘面纱!
22 1
|
8天前
Ueditor——请求后台配置项http错误,上传功能将不能正常使用
Ueditor——请求后台配置项http错误,上传功能将不能正常使用
17 0
Ueditor——请求后台配置项http错误,上传功能将不能正常使用
|
8天前
|
数据采集 开发框架 .NET
HttpClient在ASP.NET Core中的最佳实践:实现高效的HTTP请求
在现代Web开发中,高效可靠的HTTP请求对应用性能至关重要。ASP.NET Core提供的`HttpClient`是进行这类请求的强大工具。本文探讨其最佳实践,包括全局复用`HttpClient`实例以避免性能问题,通过依赖注入配置预设头部信息;使用代理IP以防IP被限制;设置合理的`User-Agent`和`Cookie`来模拟真实用户行为,提高请求成功率。通过这些策略,可显著增强爬虫或应用的稳定性和效率。
HttpClient在ASP.NET Core中的最佳实践:实现高效的HTTP请求
|
4天前
|
数据采集 存储 监控
Haskell爬虫中日志记录:监控HTTP请求与响应
Haskell爬虫中日志记录:监控HTTP请求与响应
|
11天前
|
安全 API
Haskell HTTP请求:如何解读响应状态
Haskell HTTP请求:如何解读响应状态