如何处理页面关闭时发送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()。复现文章中的代码,需要先检查一下浏览器版本以及是否清空了缓存。

相关文章
|
17天前
|
JSON Java 数据格式
java操作http请求针对不同提交方式(application/json和application/x-www-form-urlencoded)
java操作http请求针对不同提交方式(application/json和application/x-www-form-urlencoded)
69 25
java操作http请求针对不同提交方式(application/json和application/x-www-form-urlencoded)
|
16天前
|
Web App开发 大数据 应用服务中间件
什么是 HTTP Range请求(范围请求)
HTTP Range 请求是一种非常有用的 HTTP 功能,允许客户端请求资源的特定部分,从而提高传输效率和用户体验。通过合理使用 Range 请求,可以实现断点续传、视频流播放和按需加载等功能。了解并掌握 HTTP Range 请求的工作原理和应用场景,对开发高效的网络应用至关重要。
55 15
|
20天前
|
数据采集 JSON 测试技术
Grequests,非常 Nice 的 Python 异步 HTTP 请求神器
在Python开发中,处理HTTP请求至关重要。`grequests`库基于`requests`,支持异步请求,通过`gevent`实现并发,提高性能。本文介绍了`grequests`的安装、基本与高级功能,如GET/POST请求、并发控制等,并探讨其在实际项目中的应用。
29 3
|
25天前
|
前端开发 UED 开发者
CSS Sprites和图标字体在网页图标加载优化中的应用。CSS Sprites通过合并多图标减少HTTP请求,提升加载速度
本文探讨了CSS Sprites和图标字体在网页图标加载优化中的应用。CSS Sprites通过合并多图标减少HTTP请求,提升加载速度;图标字体则以字体形式呈现图标,便于调整样式。文章分析了两者的优缺点及应用场景,并提供了应用技巧和注意事项,旨在帮助开发者提升页面性能,改善用户体验。
23 5
|
1月前
|
JSON API 数据格式
Python中获取HTTP请求响应体的详解
本文介绍了如何使用Python的`requests`和`urllib`库发送HTTP请求并处理响应体。`requests`库简化了HTTP请求过程,适合快速开发;`urllib`库则更为底层,适用于性能要求较高的场景。文章详细演示了发送GET请求、处理JSON响应等常见操作。
48 3
|
1月前
|
安全 API 网络安全
使用OkHttp进行HTTPS请求的Kotlin实现
使用OkHttp进行HTTPS请求的Kotlin实现
|
17天前
|
Web App开发 网络安全 数据安全/隐私保护
Lua中实现HTTP请求的User-Agent自定义
Lua中实现HTTP请求的User-Agent自定义
|
1月前
|
前端开发 JavaScript Java
如何捕获和处理HTTP GET请求的异常
如何捕获和处理HTTP GET请求的异常
|
1月前
|
开发者
HTTP 协议请求方法的发展历程
【10月更文挑战第21天】
|
1月前
|
缓存 安全 前端开发
HTTP 协议的请求方法在实际应用中有哪些注意事项?
【10月更文挑战第29天】HTTP协议的请求方法在实际应用中需要根据具体的业务场景和需求,合理选择和使用,并注意各种方法的特点和限制,以确保网络通信的安全、高效和数据的一致性。