【问题标题】:How to detect in iOS webapp when switching back to Safari from background?从后台切换回 Safari 时如何在 iOS webapp 中检测?
【发布时间】:2011-06-07 01:59:42
【问题描述】:

我如何构建一个能够监控页面何时获得焦点的网页,尤其是当 Safari 在后台并且用户将 Safari 切换回前台时。

以下代码在 iPhone 上切换到 Safari 时不会触发事件

<html>
  <head>
    <script type="text/javascript">
      window.onfocus = function() { alert("onfocus"); };
    </script>
  </head>
  <body>

    Main text

  </body>
</html>

根据http://www.quirksmode.org/dom/events/index.html:当窗口获得焦点时,Safari iPhone 不会触发事件。

所以我的问题仍然是:如何通过在 iPhone 版 Safari 中的网页上使用 Javascript 来检测窗口是否获得焦点?

【问题讨论】:

  • 你能解释一下为什么你需要知道它吗?也许还有另一种方法?例如使用计时器。
  • 很多人将 webapp 移到后台。当他们再次打开它时(例如几天后),它必须将屏幕更新到最新状态。

标签: javascript ios iphone mobile-safari dom-events


【解决方案1】:

我相信当应用程序进入后台时计时器 (setInterval()) 会暂停。你可以这样做:

var lastFired = new Date().getTime();
setInterval(function() {
    now = new Date().getTime();
    if(now - lastFired > 5000) {//if it's been more than 5 seconds
        alert("onfocus");
    }
    lastFired = now;
}, 500);

您可能需要调整这些时间间隔以满足您的需求。

但是,如果时间长到需要刷新(几天),Safari 很可能会因为内存不足而重新加载页面。

【讨论】:

  • 如果我们在前台没有任何活动的情况下离开 safari,它甚至会刷新。因此,如果有人正在阅读新闻,则页面会重新加载。我们想要的是,只有当我们从后台返回到前台时,它才应该重新加载。
  • 如果另一个前台应用程序需要更多资源,Safari 中的网页会首先被卸载。如果另一个应用程序需要更多内存并且 Safari 选择卸载您的网页,您将不会收到任何卸载通知,并且当您返回时网页将重新加载。
  • 这种方法存在问题。当用户滚动页面时,setInterval 也会暂停。因此,如果用户滚动页面超过 5 秒,警报也会被触发 - 这可能是不希望的。
  • 例如,如果您离开 web 应用程序全屏并按下“锁定”按钮,然后解锁屏幕,则此方法有效。但是,如果您通过按主页按钮切换应用程序,则它不起作用,因为这会导致整个 Web 应用程序从头开始重新加载。我想在这种情况下这并不重要,因为无论如何你都会有东西在 onload 上触发。但是正如@EricFalsken 提到的那样,您无法收到任何卸载通知。
【解决方案2】:

根据您需要支持的内容,您需要各种不同的技术来检测页面何时变得可见。由于浏览器供应商、浏览器版本、操作系统、在 WebView/UIWebView/WKWebView 中运行等原因,会出现差异。

您可以使用this page 查看正在发生的事件。我发现要检测页面何时“唤醒”所有组合,我需要注册以下所有事件:

  • 窗口可见性更改事件
  • 窗口焦点事件
  • 窗口页面显示事件
  • 启动计时器并查看计时器所用的时间是否比应有的时间长得多(计时器在休眠时被 iOS 置于睡眠状态)。即使在 iOS9 上,使用 UIWebView 的应用程序也不会触发 visibilityChange 事件(WKWebView 可以)。

我曾经也使用过 webkitRequestAnimationFrame,但我删除了它,因为它可能会导致卡顿(AFAIK 渲染引擎会为此对主线程进行阻塞调用)。

要尝试的事情:

  • 转到另一个标签
  • 锁屏,等待,解锁
  • 让另一个应用成为焦点
  • 最小化浏览器

您可以看到正在发生的事件:

  • 实时查看控制台日志(附加调试器)。
  • 通过使用http://output.jsbin.com/rinece#http://localhost:80/ 在设备上实时查看日志被请求为 Ajax 调用(使用代理,或在 # 之后的地址上运行小型服务器并将正文记录到控制台)。
  • 查看屏幕日志,并密切注意每个条目的记录时间,以查看是否记录了该条目,例如visibilitychange 隐藏事件可能不会在页面实际隐藏时发生(如果应用程序处于休眠状态),而是排队并在页面重新显示时发生!!!

iOS:如果使用计时器来检测 iOS UIWebView 是否进入睡眠状态,请注意,您需要使用 new Date.getNow() 而不是 performance.now() 来测量差异。那是因为performance.now() 在页面进入睡眠状态时停止计算时间,iOS 实现 performance.now() 的速度也很慢...(另外:您可以通过检测来测量页面睡眠的时间量new Date.getNow() 和 performance.now() 的差异差异。查找 test page 上的 !=。

如果您使用的是 UIWebView,那么有两种方法可以使用(如果您支持 iOS7 应用程序,则必须使用 UIWebView)。 WKWebView 具有 visibilitychange 事件,因此不需要变通方法。

==技术1。

当应用发生 applicationWillEnterForeground 事件时,调用 UIWebView stringByEvaluatingJavaScriptFromString 调用你的 JavaScript pageAwakened()。

优点:干净、准确。

缺点:需要 Objective-C 代码。调用的函数需要可从全局范围访问。

==技术 2。

使用 webkitRequestAnimationFrame 并检测时间延迟。

优点:仅限 JavaScript。适用于 iOS7 上的移动 Safari。

缺点:卡顿的丑陋风险和使用 webkitRequestAnimationFrame 是一种严重的黑客攻击。

// iOS specific workaround to detect if Mobile App comes back to focus. UIWebView and old iOS don't fire any of: window.onvisibilitychange, window.onfocus, window.onpageshow
function iosWakeDetect() {
    function requestAnimationFrameCallback() {
        webkitRequestAnimationFrame(function() {
            // Can't use timestamp from webkitRequestAnimationFrame callback, because timestamp is not incremented while app is in background. Instead use UTC time. Also can't use performance.now() for same reason.
            var thisTime = (new Date).getTime();
            if (lastTime && (thisTime - lastTime) > 60000) {    // one minute
                // Very important not to hold up browser within webkitRequestAnimationFrame() or reference any DOM - zero timeout so shoved into event queue
                setTimeout(pageAwakened, 0);
            }
            lastTime = thisTime;
            requestAnimationFrameCallback();
        });
    }
    var lastTime;
    if (/^iPhone|^iPad|^iPod/.test(navigator.platform) && !window.indexedDB && window.webkitRequestAnimationFrame) {    // indexedDB sniff: it is missing in UIWebView
        requestAnimationFrameCallback();
    }
}

function pageAwakened() {
    // add code here to remove duplicate events. Check !document.hidden if supported
};

window.addEventListener('focus', pageAwakened);
window.addEventListener('pageshow', pageAwakened);
window.addEventListener('visibilitychange', function() {
    !document.hidden && pageAwakened();
});

【讨论】:

    【解决方案3】:

    Page Visibility API 可能会为这个问题提供解决方案。我猜这个 API 还没有在 Mobile Safari 中实现,至少我还没有找到任何 iOS 实现的文档。但是,Webkit Trunk 已经提交了一个实现,因此未来版本的 Mobile Safari 可能会支持它。

    【讨论】:

    • 这是一个不错的想法,但它现在已在 iOS7 (caniuse.com/#feat=pagevisibility) 中实现,但它本身并不能解决问题。如果您在 Safari 中切换选项卡,它可以工作,但如果您切换应用程序(通过单按或双击主页按钮),它不会。
    • 您找到解决此问题的方法了吗?似乎 PageVisibility API 仍然无法使用主页按钮...
    • 页面可见性 API 在 safari 和 chrome 中适用于我。查看developer.mozilla.org/en-US/docs/Web/Guide/User_experience/…
    • 对于 iOS 上的应用程序,我的testing 表明,无论您使用什么 iOS 版本,UIWebView 中都不会发生 visibilityChange 事件。它确实发生在 WKWebView 中。
    • 目前正在移动 Safari iOS 14.2 上运行。当我点击主页按钮以使应用程序后台运行时,当我点击应用程序图标以恢复时,以及在应用程序之间切换时,我都会收到事件。
    【解决方案4】:

    我写了一个小测试页面来看看什么事件被发送到 iOS 上的窗口。

    该页面“支持 Apple 网络应用”,因此您可以将其保存到主屏幕并在独立模式下进行测试。

    这是页面:Test of Window Events

    代码:

    // determine if this is a touch-capable device
    const isTouchDevice = ('ontouchstart' in window) ||
      (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0);
    console.log(`isTouchDevice: ${isTouchDevice ? 'TRUE' : 'FALSE'} `);
    
    const button = document.getElementById('btnClear');
    const divEvents = document.getElementById('divEvents');
    const olEvents = document.getElementById('olEvents');
    const divBottom = document.getElementById('divBottom');
    
    // handle "clear history" button click
    button.addEventListener('click', function() {
      if (isTouchDevice) {
        // simulate click on button using `focus` and `blur`
        button.focus();
        setTimeout(() => button.blur(), 500);
      }
      olEvents.innerHTML = '';
    });
    
    const eventNames = [
      'load',
      'focus',
      'blur',
      'change',
      'close',
      'error',
      'haschange',
      'message',
      'offline',
      'online',
      'pagehide',
      'pageshow',
      'popstate',
      'resize',
      'submit',
      'unload',
      'beforeunload'
    ];
    eventNames.forEach(function(eventName) {
      window.addEventListener(eventName, function(evt) {
        const now = new Date();
        const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
          now.getMinutes().toString().padStart(2, '0') + ':' +
          now.getSeconds().toString().padStart(2, '0') + '.' +
          now.getMilliseconds();
        let li = document.createElement('li');
        li.innerHTML = timeStr + ' - ' + `<code>${evt.type}</code>`;
        olEvents.appendChild(li);
    
        // scroll to bottom
        // window.scrollTo(0, divBottom.offsetTop);
        const bottomOffset = divBottom.offsetTop;
        divEvents.scrollTop = bottomOffset - 10;
      });
    });
    #divEvents {
      border: 1px solid rgba(0, 0, 0, 0.5);
      height: 400px;
      max-width: 60rem;
      padding: 1rem 0;
      overflow-y: auto;
    }
    
    #olEvents {
      font-size: 87.5%;
    }
    
    #divBottom {
      height: 0px;
    }
    
    code {
      font-size: 100%;
    }
    
    
    /* handle the sticky hover problem on touch devices */
    
    @media (hover:none) {
      /* set button hover style to match non-hover */
      .btn-outline-primary:hover {
        color: #007bff;
        background-color: transparent;
        background-image: none;
        border-color: #007bff;
      }
      /* set button focus style to match hover */
      .btn-outline-primary:focus {
        color: #fff;
        background-color: #007bff;
        border-color: #007bff;
      }
    }
    <head>
      <!-- Required meta tags -->
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, maximum-scale=1, minimum-scale=1, shrink-to-fit=no, user-scalable=no">
    
      <!-- apple web app meta tags -->
      <meta name="apple-mobile-web-app-title" content="WinEvents">
      <meta name="apple-mobile-web-app-capable" content="yes">
      <meta name="apple-mobile-web-app-status-bar-style" content="black">
    
      <title>Test of Window Events</title>
    
      <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.css">
      <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootsdark@latest/dist/bootsdark.min.css">
    
    </head>
    
    <body class="d-flex flex-column h-100">
      <header>
        <!-- Fixed navbar -->
        <nav class="navbar navbar-expand-md navbar-dark bg-dark">
          <a class="navbar-brand" href="https://terrymorse.com">Terry Morse
          Software</a>
        </nav>
      </header>
    
      <main role="main" class="flex-shrink-0 m-4">
        <h1>Test of Window Events</h1>
    
        <p>Displays all of the events (except for
          <code>scroll</code>) sent to <code>document.window</code>.</p>
    
        <p>
          <button id="btnClear" class="btn btn-sm btn-outline-primary"
          >Clear History</button>
        </p>
    
        <h4>Events Caught:</h4>
        <div id="divEvents">
          <ol id="olEvents" class="text-monospace"></ol>
          <div id="divBottom"></div>
        </div>
      </main>
    
    </body>

    【讨论】:

    • 在 i(Pad)OS 15.1 上,当浏览器回到前台时,似乎有一个“调整大小”事件(实际上是 3 个)。任何人都在猜测 iPadOS 15.2 或 16 会是什么……
    【解决方案5】:

    窗口上的焦点和模糊事件可以很好地检测浏览器是从后台返回还是返回。这在 iOS8 Safari 上对我有用:

    window.addEventListener("focus", function(evt){
        console.log('show');
    }, false);
    window.addEventListener("blur", function(evt){
        console.log('hide');
    }, false);
    

    【讨论】:

    • 我在 iPhone XS 和 iPad Pro、iOS 12.1 上测试了“焦点”事件检测。虽然每次激活 Safari 时都会检测到两个焦点事件,但它正在工作。
    【解决方案6】:

    由于问题出在移动 safari 并且它支持 popstate 事件,您可以使用此事件来检测用户何时回来

    【讨论】:

      【解决方案7】:

      使用pageshow 和pagehide 事件。

      <script type="text/javascript">
              window.addEventListener("pageshow", function(evt){
                  alert('show');
              }, false);
              window.addEventListener("pagehide", function(evt){
                  alert('hide');
              }, false);
      </script>
      

      https://developer.apple.com/library/mac/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html

      【讨论】:

      • 当 safari 处于后台/返回前台时不会触发页面隐藏/页面显示
      猜你喜欢
      • 2014-03-29
      • 2013-09-23
      • 2020-08-05
      • 2015-12-11
      • 2017-07-05
      • 1970-01-01
      • 2017-08-16
      • 2015-08-16
      • 2012-01-18
      相关资源
      最近更新 更多