JavaScript、浏览器、窗口关闭 - 发送 AJAX 请求或在窗口关闭时运行脚本

JavaScript, browsers, window close - send an AJAX request or run a script on window closing

提问人:zozo 提问时间:5/28/2011 最后编辑:Brian Tompsett - 汤莱恩zozo 更新时间:2/24/2022 访问量:76657

问:

我正在尝试找出用户何时离开指定页面。当他使用页面内的链接导航离开时,没有问题,但我有点需要标记一些东西,比如当他关闭窗口或键入另一个 URL 并按回车键时。第二个不是那么重要,但第一个是。所以问题来了:

我如何查看用户何时关闭了我的页面(捕获 window.close 事件),然后...并不重要(我需要发送 AJAX 请求,但如果我能让它运行警报,我可以完成剩下的工作)。

JavaScript 浏览器 窗口 dom-events

评论


答:

-5赞 Jonathon 5/28/2011 #1

用:

<body onUnload="javascript:">

它应该捕获除关闭浏览器程序之外的所有内容。

评论

0赞 Jonathon 5/28/2011
嗯(-1),我很想知道这种方法有什么问题。我自己从未使用过它,但多次遇到它作为您确切问题的解决方案。
2赞 Felix 5/28/2011
我已经发布了一个答案,其中我解释了 -1。
52赞 Felix 5/28/2011 #2

有 和 javascript 事件,但这些事件对于 Ajax 请求不可靠(不能保证在其中一个事件中启动的请求会到达服务器)。unloadbeforeunload

因此,强烈建议不要这样做,您应该寻找替代方案。

如果您确实需要这个,请考虑使用“ping”风格的解决方案。每分钟发送一次请求,基本上告诉服务器“我还在这里”。然后,如果服务器超过两分钟没有收到此类请求(您必须考虑延迟等),则认为客户端处于脱机状态。


另一种解决方案是使用或执行 Sjax 请求(同步 JavaScript 和 XML),但完全不建议这样做。这样做基本上会冻结用户的浏览器,直到请求完成,他们不会喜欢(即使请求花费的时间很少)。unloadbeforeunload

评论

0赞 Jonathon 5/28/2011
好吧,如果他正在注销,那么我已经看到了卸载打开一个弹出窗口的工作示例,然后将用户注销(这解决了非担保人问题吗?
4赞 Felix 5/28/2011
哈哈我敢打赌,那里有弹出窗口阻止程序不允许在这些事件上弹出。此外,当用户关闭选项卡/窗口时打开弹出窗口是很糟糕的用户体验。
0赞 Jonathon 5/28/2011
我认为问题是请求需要时间,并且浏览器可能在完成时完成卸载(所以它可能永远不会完成)?
0赞 zozo 5/28/2011
我实际上正在做我还在这里的事情。但是有点慢,降低了用户体验。此外,缩短“我还在这里”的间隔可能会以高用户率杀死服务器。所以我试图用别的东西代替它。当你说请求不可靠时,你的意思是不可靠吗?我可以使用它,并以更高的间隔发送消息,成功率超过 50%。我认为这可以提高双方(服务器和客户端)的性能。
1赞 Felix 5/30/2011
我认为这将是你最好的选择(尽管如果你想 100% 确定,你仍然必须进行 ping)。顺便说一句,执行此操作的方法是在您的处理程序中(执行 a 行不通)。return "a string";beforeunloadconfirm()
-1赞 Ramin Farajpour 3/8/2014 #3

我同意 Felix 的想法,我已经用该解决方案解决了我的问题,现在我想清除服务器端解决方案:

1.从客户端向服务器发送请求

2.保存变量中最后一次请求的时间

3.检查服务器时间,并与上次接收的变量进行比较 请求

4.如果结果超过您预期的时间,请开始运行 要在 Windows 关闭时运行的代码...

3赞 Pradeep Yadav 2/24/2015 #4

我也想实现相同的功能,并从 Felix 那里得到了这个答案(不能保证在这些事件之一中发起的请求会到达服务器)。

为了使请求到达服务器,我们尝试了以下代码:-

onbeforeunload = function() {

    //Your code goes here.

    return "";
} 

我们正在使用IE浏览器,现在当用户关闭浏览器时,他会收到确认对话框,因为&等待用户的确认,并且此等待时间使请求到达服务器。return "";

评论

0赞 Michal Politzer 3/30/2017
如果 beforeunload 函数中不止 retrun string 语句,则当前的 Chrome 似乎完全忽略了这一点。所以你可以返回“你想关闭吗?”,但你不能做任何其他事情......
1赞 codepuzzler 10/26/2017 #5

所选答案是正确的,您不能保证浏览器发送 xhr 请求,但根据浏览器的不同,您可以在选项卡或窗口关闭时可靠地发送请求。

通常,浏览器会在 xhr.send() 实际执行之前关闭。Chrome 和 edge 看起来像是在关闭窗口之前等待 javascript 事件循环清空。它们还会在与 javascript 事件循环不同的线程中触发 xhr 请求。这意味着,如果可以使事件循环保持足够长的时间,则 xhr 将成功触发。例如,我测试了发送 xhr 请求,然后数到 100,000,000。对我来说,这在 chrome 和 edge 中都非常一致。如果您使用的是 angularjs,则将对 $http 的调用包装在 $apply 中可以完成相同的操作。

IE似乎有点不同。我不认为IE会等待事件循环清空,甚至不会等待当前堆栈帧清空。虽然它偶尔会正确发送请求,但似乎更频繁地(80%-90% 的时间)是 IE 将在 xhr 请求完全执行之前关闭窗口或选项卡,这导致仅发送部分消息。基本上,服务器收到一个 post 请求,但没有正文。

为了后人,这是我使用附加的代码作为 window.onbeforeunload 侦听器函数:

  var xhr = new XMLHttpRequest();
  xhr.open("POST", <your url here>);
  xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
  var payload = {id: "123456789"};
  xhr.send(JSON.stringify(payload));

  for(var i = 0; i < 100000000; i++) {}

我测试了:

Chrome浏览器61.0.3163.100

IE浏览器 11.608.15063.0CO

边缘 40.15063.0.0

评论

0赞 Greg 11/1/2017
在 for 循环上花费的时间会因 CPU、JS 引擎和其他因素而异。一个稍微不那么坏的主意是阻塞事件循环 100 毫秒或任何其他在实验后被认为足够长的值
40赞 Useful Angle 11/4/2017 #6

1)如果你正在寻找一种在所有浏览器中工作的方法,那么最安全的方法是向服务器发送同步AJAX。这不是一个好方法,但至少要确保你没有向服务器发送太多数据,并且服务器速度很快。

2)您还可以使用异步AJAX请求,并在服务器上使用ignore_user_abort函数(如果您使用的是PHP)。但是,ignore_user_abort很大程度上取决于服务器配置。确保你测试得很好。

3)对于现代浏览器,您不应该发送AJAX请求。您应该使用新的 navigator.sendBeacon 方法异步向服务器发送数据,而不会阻止下一页的加载。由于您希望在用户移出页面之前将数据发送到服务器,因此可以在卸载事件处理程序中使用此方法。

$(window).on('unload', function() {
    var fd = new FormData();
    fd.append('ajax_data', 22);
    navigator.sendBeacon('ajax.php', fd);
});

似乎还有一个用于 sendBeacon 的 polyfill。如果方法本身不可用,则求助于发送同步 AJAX。

对于移动设备很重要:请注意,不保证为移动设备触发 unload 事件处理程序。但是 visibilitychange 事件是肯定被触发的。因此,对于移动设备,您的数据收集代码可能需要进行一些调整。

你可以参考我的博客文章,了解所有 3 种方式的代码实现。

评论

0赞 Baishu 2/24/2018
在2018年,这是最好的回应。使用信标 API。阅读答案中提到的博客文章 ( usefulangle.com/post/62/... ) 了解更多详情。
0赞 Mr. Developerdude 6/10/2021
我知道它的存在,但不记得它的名字。这是正确答案 IMO “navigator.sendBeacon” <--
96赞 Baishu 2/24/2018 #7

2021 年更新

TL;博士

Beacon API 是此问题的解决方案(几乎在每个浏览器上).

即使用户退出页面,信标请求也应该完成.

您应该何时触发 Beacon 请求?

这将取决于您的用例。如果您希望捕获任何用户出口,(not ) 是开发人员在现代浏览器中可以可靠地观察到的最后一个事件visibilitychangeunload

注意:只要 的实现在浏览器之间不一致,就可以通过 lifecycle.js 库进行检测。visibilitychange

# lifecycle.js (1K) for cross-browser compatibility
# https://github.com/GoogleChromeLabs/page-lifecycle

<script defer src="/path/to/lifecycle.js"></script>
<script defer>
lifecycle.addEventListener('statechange', function(event) {

  if (event.originalEvent == 'visibilitychange' && event.newState == 'hidden') {
    var url = "https://example.com/foo";
    var data = "bar";

    navigator.sendBeacon(url, data);
  }
});
</script>

即使用户离开页面,信标请求也应该运行完成 - 切换到另一个应用程序等 - 而不会阻止用户工作流程。

在后台,它会发送 POST 请求以及用户凭据 (cookie),但须遵守 CORS 限制。

    var url = "https://example.com/foo";
    var data = "bar";

    navigator.sendBeacon(url, data);

问题是何时发送您的 Beacon 请求.特别是如果您想等到最后一刻才发送会话信息、应用程序状态、分析等。

过去,在活动期间发送它是常见的做法,但由移动用户体验驱动的页面生命周期管理的变化扼杀了这种方法。如今,大多数移动工作流程(切换到新选项卡、切换到主屏幕、切换到另一个应用程序......不要触发 UNLOAD 事件unload

如果要在用户退出应用/页面时执行操作,现在建议使用 visibilitychange 事件并检查从被动状态转换为隐藏状态。

document.addEventListener('visibilitychange', function() {
      
  if (document.visibilityState == 'hidden') {
    
     // send beacon request
  }

});

向隐藏状态的转换通常是开发人员可以可靠地观察到的最后一个状态更改(在移动设备上尤其如此,因为用户可以关闭选项卡或浏览器应用本身,并且在这些情况下不会触发 beforeunload、pagehide 和 unload 事件)。

这意味着应将隐藏状态视为用户会话的可能结束。换句话说,保留任何未保存的应用程序状态,并发送任何未发送的分析数据。

本文将详细介绍其详细信息。Page lifecyle API

但是,事件的实现以及跨浏览器的实现并不一致visibilitychangePage lifecycle API

在浏览器实现赶上之前,使用生命周期.js库和页面生命周期最佳实践似乎是一个很好的解决方案。

# lifecycle.js (1K) for cross-browser compatibility
# https://github.com/GoogleChromeLabs/page-lifecycle

<script defer src="/path/to/lifecycle.js"></script>
<script defer>
lifecycle.addEventListener('statechange', function(event) {

  if (event.originalEvent == 'visibilitychange' && event.newState == 'hidden') {
    var url = "https://example.com/foo";
    var data = "bar";

    navigator.sendBeacon(url, data);
  }
});
</script>

有关香草页面生命周期事件(没有生命周期.js)可靠性的更多数字,还有这项研究

广告拦截器

广告拦截器似乎有阻止发送信标请求的选项。

跨站点请求

Beacon 请求是包含 cookie 的 POST 请求,并受 CORS 规范的约束。

评论

4赞 Alfie Robles 12/18/2019
这个答案改变了我的生活!多谢!
1赞 godblessstrawberry 12/5/2020
MDN 说这是无用的 developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeaconunload and beforeunload aren’t the right events to use with sendBeacon. Instead, use visibilitychange
0赞 Baishu 12/29/2020
@godblessstrawberry确实如此。MDN 恢复了他们最近使用的建议:github.com/mdn/sprints/issues/3722。考虑到这一点,我重写了答案。unload
0赞 Baishu 12/29/2020
@AlfieRobles,如果您依赖事件获取重要数据,请检查此答案的更改。unload
0赞 Greg Venech 12/14/2021
这看起来很有希望,但似乎我们不能使用 Beacon 请求设置请求标头?的 MDN 页面没有提到它们,但我找到了这篇文章,其中指出这是可能的。我错了,还是可能需要更新接收 API,以便绕过身份验证(或通过请求有效负载而不是标头处理它们)之类的事情?sendBeaconContent-Type
0赞 Stidljivi 9/5/2019 #8

试试这个。我在javascript中解决了这个问题,在浏览或关闭选项卡时向服务器发送ajax调用。我在刷新页面时遇到了问题,因为在onbeforeunload功能上,包括刷新页面。performance.navigation.type == 1 应该将刷新与关闭隔离开来(在 Mozzila 浏览器上)。

$(window).bind('mouseover', (function () { // detecting DOM elements
    window.onbeforeunload = null;
}));

$(window).bind('mouseout', (function () { //Detecting event out of DOM
      window.onbeforeunload = ConfirmLeave;
}));

function ConfirmLeave() {
   if (performance.navigation.type == 1) { //detecting refresh page(doesnt work on every browser)
   }
   else {
       logOutUser();
   }
}

$(document).bind('keydown', function (e) { //detecting alt+F4 closing
    if (e.altKey && e.keyCode == 115) {
        logOutUser();
    }    
});
function logOutUser() {
   $.ajax({
     type: "POST",
     url: GWA("LogIn/ForcedClosing"), //example controller/method
     async: false
   });
}

评论

0赞 zozo 9/6/2019
您好,欢迎来到 SO。不投反对票(因为你是新:)),但你的答案不会给接受的答案带来任何新的东西。此外,如果我正确阅读了它,每次用户将鼠标移出窗口时,它都会触发 ConfirmLeave 函数,从而导致虚假请求(例如,每次他将鼠标移到任务栏或其他东西上时)。此外,它不处理任何类型的触摸/移动案例(因为事件仅在mouseOut上绑定,在over上删除,并且永远不会绑定回来)。还要考虑简单地按 f5 的情况(在您的示例中也不起作用)。
0赞 zozo 9/6/2019
在 2011 年(当我发布问题时)这是不可能的(或者至少不像今天那么简单),但现在您可以使用 websocket,它基本上为您带来了 ping 风格解决方案的优点,而没有缺点。如果你想检查一下,我很乐意给这样的答案投赞成票(我正在考虑自己发布一个,但是......懒惰:))。
3赞 zozo 9/6/2019 #9

在发布问题多年后,我制定了一种更好的实现方法,包括 nodejs 和 socket.io (https://socket.io)(您可以使用任何类型的套接字,但这是我个人的选择)。

基本上,我打开了与客户的连接,当它挂断时,我只是保存数据/做我需要的任何事情。显然,这不能用于显示任何内容/重定向客户端(因为您是在服务器端这样做),但这是我当时实际需要的。

 io.on('connection', function(socket){
      socket.on('disconnect', function(){
          // Do stuff here
      });
 });

所以。。。现在我认为这将是一种比卸载版本更好的方法(虽然更难实现,因为您需要节点、套接字等,但并不难;如果您第一次这样做,应该需要大约 30 分钟左右)。