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

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在click. 无需等待任何形式的响应。我只是希望它被发送到我正在访问的任何服务。

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

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

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

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

但是为什么他们被取消了?

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

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

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

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

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

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

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

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请求,并且不需要损害用户体验。

1.使用 Fetch 的keepalive标志

如果在使用时将该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>

2.使用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()获得“高”优先级,而信标(上面称为“ping”类型)具有“最低”优先级。对于对页面功能不重要的请求,这是一件好事。直接取自Beacon 规范

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

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

那么,我应该接触哪一个?

如果fetch():_keepalive

  • 您需要轻松地随请求传递自定义标头。
  • 您想向GET服务发出请求,而不是POST.
  • 您正在支持较旧的浏览器(如 IE)并且已经fetch加载了一个 polyfill。

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

  • 您正在发出不需要太多自定义的简单服务请求。
  • 您更喜欢更简洁、更优雅的 API。
  • 您希望确保您的请求不会与应用程序中发送的其他高优先级请求竞争。

参考:https://css-tricks.com/send-an-http-request-on-page-exit/#top-of-site

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容