【问题标题】:JavaScript, browsers, window close - send an AJAX request or run a script on window closingJavaScript、浏览器、窗口关闭 - 发送 AJAX 请求或在窗口关闭时运行脚本
【发布时间】:2011-09-03 23:12:47
【问题描述】:

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

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

【问题讨论】:

    标签: javascript browser window dom-events


    【解决方案1】:

    2021 年更新

    TL;DR

    Beacon API 是这个问题的解决方案(在almost every browser)。

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

    你应该什么时候触发你的 Beacon 请求?

    这将取决于您的用例。如果您正在寻找任何用户退出,visibilitychange(不是unload)是现代浏览器中开发人员的last event reliably observable

    注意:只要visibilitychange 的实现是not consistent across browsers,通过lifecycle.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>
    

    详情

    Beacon requests 应该运行完成,即使用户离开页面 - 切换到另一个应用程序等 - 而不会阻塞用户工作流。

        var URL = "https://example.com/foo";
        var data = "bar";
    
        navigator.sendBeacon(URL, data);
    

    问题是何时发送您的 Beacon 请求。特别是如果您想等到最后一刻发送会话信息、分析等。

    过去在unload 事件期间发送它是一种常见的做法,但页面生命周期管理的更改——由移动用户体验驱动——扼杀了这种方法。今天,大多数移动工作流程(切换到新标签页、切换到主屏幕、切换到另一个应用程序...)do not trigger the unload event at any point

    如果您想在用户退出您的应用/页面时执行操作,现在建议使用visibilitychange 事件并检查从passivehidden 状态的转换。

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

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

    这意味着您应该将隐藏状态视为用户会话的可能结束。换言之,保留所有未保存的应用程序状态并发送所有未发送的分析数据。

    Page lifecyle API 的详细信息在in this article 中解释。

    但是,visibilitychange 事件以及 Page lifecycle API is not consistent across browsers 的实现。

    在浏览器实现catches up 之前,使用lifecycle.js 库和page lifecycle best practices 似乎是一个不错的解决方案。

    # 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)的可靠性的更多数字,还有this study

    【讨论】:

    • 这个答案改变了我的生活!非常感谢!
    • MDN 说 unload and beforeunload aren’t the right events to use with sendBeacon. Instead, use visibilitychange 所以这没用 developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon
    • @godblessstrawberry 确实如此。 MDN 最近恢复了他们对使用 unload 的建议:github.com/mdn/sprints/issues/3722。我已经重写了答案以考虑到这一点。
    • @AlfieRobles 如果您依赖 unload 事件获取重要数据,请检查对此答案的更改。
    • 这看起来很有希望,但我们似乎无法使用 Beacon 请求设置请求标头? sendBeacon 的 MDN 页面没有提及它们,但我发现 this article 指出只有 Content-Type 是可能的。我对此是否错了,或者这可能需要更新接收 API 以允许绕过身份验证(或通过请求有效负载而不是标头处理它们)之类的东西?
    【解决方案2】:

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

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

    如果您确实需要此功能,请考虑使用“ping”样式的解决方案。每分钟发送一个请求,基本上告诉服务器“我还在这里”。然后,如果服务器超过两分钟没有收到这样的请求(你必须考虑延迟等),你认为客户端离线。


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

    【讨论】:

    • 好吧,如果他正在注销,那么我已经看到卸载打开一个弹出窗口然后将用户注销的工作示例(这是否解决了非保证问题?)。
    • 没有。我敢打赌,那里的弹出窗口阻止程序不允许在这些事件中弹出窗口。此外,当用户关闭选项卡/窗口时打开弹出窗口是很糟糕的用户体验。
    • 我认为问题是请求需要时间,并且浏览器可能在完成时完成卸载(所以它可能永远不会完成)?
    • 我实际上正在做“我还在这里”的事情。但是有点慢,降低了用户体验。同样减少“我还在这里”的间隔可以以高用户率杀死服务器。所以我试图用别的东西代替它。当您说请求不可靠时,您的意思是多么不可靠?我可以使用它并将它加倍,以更高的间隔发送消息,成功率超过 50%。我认为这可以提高双方(服务器和客户端)的性能。
    • 我认为这将是您最好的选择(尽管如果您想 100% 确定,您仍然需要进行 ping 操作)。顺便说一句,执行此操作的方法是在您的 beforeunload 处理程序中使用 return "a string";(执行 confirm() 将不起作用)。
    【解决方案3】:

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

    2) 您也可以使用异步 AJAX 请求,并在服务器上使用 ignore_user_abort 函数(如果您使用的是 PHP)。然而 ignore_user_abort 很大程度上取决于服务器配置。确保您对其进行了良好的测试。

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

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

    似乎还有一个polyfill for sendBeacon。如果方法本身不可用,它会发送同步 AJAX。

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

    三种方式的代码实现可以参考我的blog article

    【讨论】:

    • 2018年,这是最好的回应。使用信标 API。阅读答案中提到的博客文章 (usefulangle.com/post/62/…) 了解更多详情。
    • 我知道它存在,但不记得它的名字。这是正确的答案 IMO "navigator.sendBeacon"
    【解决方案4】:

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

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

    onbeforeunload = function() {
    
        //Your code goes here.
    
        return "";
    } 
    

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

    【讨论】:

    • 如果 beforeunload 函数中不仅有 retrun string 语句,当前的 Chrome 似乎完全忽略了这一点。所以你可以返回“你想关闭吗?”,但你不能做任何其他事情......
    【解决方案5】:

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

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

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

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

    【讨论】:

      【解决方案6】:

      选择的答案是正确的,您不能保证浏览器会发送 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请求,但是没有body。

      为了后代,这是我用作 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

      【讨论】:

      • for 循环所花费的时间会因 CPU、JS 引擎和其他因素而异。一个稍微不那么糟糕的想法是阻止事件循环 100 毫秒或任何其他在实验后被认为足够长的值
      【解决方案7】:

      试试这个。我在 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
         });
      }
      

      【讨论】:

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

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

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

      2.将上次接收请求的时间保存在变量中

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

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

      【讨论】:

        【解决方案9】:

        用途:

        <body onUnload="javascript:">
        

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

        【讨论】:

        • hmm (-1),我很想知道这种方法有什么问题。我自己从未使用过它,但多次遇到它作为您确切问题的解决方案。
        • 我已经发布了一个答案,我在其中解释了 -1。
        猜你喜欢
        • 2019-06-15
        • 1970-01-01
        • 2015-04-27
        • 2021-05-22
        • 2014-10-14
        • 1970-01-01
        • 2010-10-22
        • 2012-05-28
        相关资源
        最近更新 更多