有没有一种方法可以检测浏览器窗口当前是否处于非活动状态?

727

我有一段定期执行的JavaScript代码。当用户不在浏览网站时(即窗口或选项卡没有焦点),最好不要运行它。

是否有一种使用JavaScript实现此功能的方法?

我的参考对象是 Gmail 聊天,如果你正在使用的窗口不活跃,它会播放声音。


12
如果您对以下答案不满意,请查看 requestAnimationFrame API,或使用现代特性,即当窗口不可见时 setTimeout/setInterval 的频率会降低(例如在 Chrome 中约为1秒)。请注意,翻译尽可能保持原意,简化表达,并避免添加解释。 - Rob W
4
document.body.onblur=function(e){console.log('lama');} 可以用于非聚焦元素。 - WhyMe
3
请参考这个答案,它使用W3C页面可见性API来实现跨浏览器兼容,并在不支持该API的浏览器中回退到blur/focus - Mathias Bynens
8
以下80%的回答“不是对这个问题的回答”。该问题询问“目前未激活的”,而下面大量回答是关于“不可见”的,这不是对这个问题的回答。它们应该被标记为“不是答案”。 - gman
1
大多数人谈论“非活动”时,实际上是指“非活动且不可见”。只是“非活动”很容易 - 只需处理窗口的blur / focus事件...尽管如此,这将具有有限的用途,因为窗口可以是非活动的但完全或部分可见(某些任务栏中也有“预览”图标,人们希望继续更新)。 - rustyx
1
Twitter 会在页面顶部刷新页面,如果用户一段时间未打开该页面,并且有新的消息时,则会自动跳转到该标签页或窗口,这样用户就可以轻松找到他们需要的内容。 - gseattle
25个回答

818

自本回答最初发布以来,得益于W3C,一项新的标准已经达到了推荐状态。 页面可见性API(在MDN上)现在允许我们更准确地检测页面何时对用户隐藏。

document.addEventListener("visibilitychange", onchange);

当前浏览器支持情况:

  • Chrome 13+
  • Internet Explorer 10+
  • Firefox 10+
  • Opera 12.10+[详见注释]

在不兼容的浏览器中,以下代码会回退到不太可靠的blur/focus方法:

(function() {
  var hidden = "hidden";

  // Standards:
  if (hidden in document)
    document.addEventListener("visibilitychange", onchange);
  else if ((hidden = "mozHidden") in document)
    document.addEventListener("mozvisibilitychange", onchange);
  else if ((hidden = "webkitHidden") in document)
    document.addEventListener("webkitvisibilitychange", onchange);
  else if ((hidden = "msHidden") in document)
    document.addEventListener("msvisibilitychange", onchange);
  // IE 9 and lower:
  else if ("onfocusin" in document)
    document.onfocusin = document.onfocusout = onchange;
  // All others:
  else
    window.onpageshow = window.onpagehide
    = window.onfocus = window.onblur = onchange;

  function onchange (evt) {
    var v = "visible", h = "hidden",
        evtMap = {
          focus:v, focusin:v, pageshow:v, blur:h, focusout:h, pagehide:h
        };

    evt = evt || window.event;
    if (evt.type in evtMap)
      document.body.className = evtMap[evt.type];
    else
      document.body.className = this[hidden] ? "hidden" : "visible";
  }

  // set the initial state (but only if browser supports the Page Visibility API)
  if( document[hidden] !== undefined )
    onchange({type: document[hidden] ? "blur" : "focus"});
})();

onfocusinonfocusout在IE 9及以下版本中必需,而其他所有浏览器都使用onfocusonblur,iOS使用onpageshowonpagehide


2
@bellpeace:IE 应该将 focusinfocusout 从 iframe 传递到上层窗口。对于较新的浏览器,您只需要在每个 iframe 的 window 对象上处理 focusblur 事件即可。您应该使用我刚添加的更新代码,它至少可以覆盖新浏览器中的这些情况。 - Andy E
4
@JulienKronegg:这就是为什么我的回答特别提到了页面可见性API,它在我最初撰写答案后进入了工作草案状态。对于旧版浏览器,focus/blur方法提供的功能有限。像你的回答中绑定其他事件并不能涵盖更多内容,而且更容易出现行为差异(比如IE在光标下方弹出窗口时不会触发mouseout)。我建议更合适的做法是显示一条消息或图标,提示用户由于页面不活动,更新可能会更少。 - Andy E
17
@AndyE,我在Chromium上尝试了这个解决方案。如果我切换选项卡,它可以正常工作,但是如果我改变窗口(ALT+Tab),它就不能工作了。它应该能够工作吗?这是一个演示 - http://jsfiddle.net/8a9N6/17/ - Tony Lâmpada
4
@Heliodor: 我希望现在答案中的代码尽可能简洁。这并不意味着它是一个可以复制粘贴的完整解决方案,因为实施者可能希望避免在body上设置类,并采取完全不同的操作(如停止和启动定时器)。 - Andy E
21
@AndyE 您的解决方案似乎只适用于用户更改选项卡或最小化/最大化窗口的情况。但是,如果用户将另一个程序从任务栏最大化到覆盖当前选项卡,那么 onchange 事件将不会被触发。是否有针对这种情况的解决方案?谢谢! - user1491636
显示剩余40条评论

143

我会使用 jQuery,因为你只需要这样做:

$(window).blur(function(){
  //your code here
});
$(window).focus(function(){
  //your code
});

或者至少对我来说有效。


1
这个调用在 iframe 中对我来说执行了两次。 - msangel
2
在Firefox中,如果您单击firebug控制台内部(在同一页上),则“窗口”将失去焦点,这是正确的,但根据您的意图可能不是您所需的。 - Majid Fouladpour
30
此方法已经不适用于现代浏览器的最新版本,请查看已批准的答案(页面可见性 API)。 - Jon z
此解决方案在iPad上不起作用,请使用“pageshow”事件。 - ElizaS
页面加载时,BLUR和FOCUS事件都会触发。当我从页面打开一个新窗口时,什么也不会发生,但是一旦新窗口关闭,两个事件都会触发 :/(使用IE8)。 - SearchForKnowledge
显示剩余2条评论

66

有三种常用方法用于确定用户是否能看到HTML页面,但它们都不完美:

  • W3C页面可见性API被认为可以实现这一点(支持自Firefox 10、MSIE 10和Chrome 13)。然而,该API仅在浏览器选项卡完全覆盖时引发事件(例如,当用户从一个选项卡切换到另一个选项卡时)。该API在无法以100%的准确度确定可见性时(例如,使用Alt+Tab切换到另一个应用程序)不会引发事件。

  • 使用基于焦点/失焦的方法会给出很多误报。例如,如果用户在浏览器窗口上方显示了一个较小的窗口,则浏览器窗口将丢失焦点(onblur被触发),但用户仍然能够看到它(因此仍需要刷新)。请参阅http://javascript.info/tutorial/focus

  • 依赖用户活动(鼠标移动、点击、键入)也会给出很多误报。想象一下与上述情况相同的情况,或者用户观看视频。

为了改善上述不完美的行为,我使用三种方法的组合:W3C可见性API,然后是焦点/失焦和用户活动方法,以减少误报率。这允许管理以下事件:

  • 将浏览器选项卡更改为另一个选项卡(100%准确性,多亏了W3C页面可见性API)
  • 页面可能被其他窗口隐藏,例如由于Alt+Tab(概率性=不是100%准确)
  • 用户注意力可能没有集中在HTML页面上(概率性=不是100%准确)

它的工作原理如下:当文档失去焦点时,监视文档上的用户活动(例如鼠标移动),以确定窗口是否可见。页面可见性概率与页面上最后一次用户活动的时间成反比:如果用户长时间未对文档进行任何活动,则该页面很可能不可见。下面的代码模仿W3C页面可见性API:它的行为方式相同,但具有较小的误报率。它的优点是跨浏览器(已在Firefox 5、Firefox 10、MSIE 9、MSIE 7、Safari 5、Chrome 9上测试)。


    <div id="x"></div>
     
    <script>
    /**
    Registers the handler to the event for the given object.
    @param obj the object which will raise the event
    @param evType the event type: click, keypress, mouseover, ...
    @param fn the event handler function
    @param isCapturing set the event mode (true = capturing event, false = bubbling event)
    @return true if the event handler has been attached correctly
    */
    function addEvent(obj, evType, fn, isCapturing){
      if (isCapturing==null) isCapturing=false; 
      if (obj.addEventListener){
        // Firefox
        obj.addEventListener(evType, fn, isCapturing);
        return true;
      } else if (obj.attachEvent){
        // MSIE
        var r = obj.attachEvent('on'+evType, fn);
        return r;
      } else {
        return false;
      }
    }
     
    // register to the potential page visibility change
    addEvent(document, "potentialvisilitychange", function(event) {
      document.getElementById("x").innerHTML+="potentialVisilityChange: potentialHidden="+document.potentialHidden+", document.potentiallyHiddenSince="+document.potentiallyHiddenSince+" s<br>";
    });
     
    // register to the W3C Page Visibility API
    var hidden=null;
    var visibilityChange=null;
    if (typeof document.mozHidden !== "undefined") {
      hidden="mozHidden";
      visibilityChange="mozvisibilitychange";
    } else if (typeof document.msHidden !== "undefined") {
      hidden="msHidden";
      visibilityChange="msvisibilitychange";
    } else if (typeof document.webkitHidden!=="undefined") {
      hidden="webkitHidden";
      visibilityChange="webkitvisibilitychange";
    } else if (typeof document.hidden !=="hidden") {
      hidden="hidden";
      visibilityChange="visibilitychange";
    }
    if (hidden!=null && visibilityChange!=null) {
      addEvent(document, visibilityChange, function(event) {
        document.getElementById("x").innerHTML+=visibilityChange+": "+hidden+"="+document[hidden]+"<br>";
      });
    }
     
     
    var potentialPageVisibility = {
      pageVisibilityChangeThreshold:3*3600, // in seconds
      init:function() {
        function setAsNotHidden() {
          var dispatchEventRequired=document.potentialHidden;
          document.potentialHidden=false;
          document.potentiallyHiddenSince=0;
          if (dispatchEventRequired) dispatchPageVisibilityChangeEvent();
        }
     
        function initPotentiallyHiddenDetection() {
          if (!hasFocusLocal) {
            // the window does not has the focus => check for  user activity in the window
            lastActionDate=new Date();
            if (timeoutHandler!=null) {
              clearTimeout(timeoutHandler);
            }
            timeoutHandler = setTimeout(checkPageVisibility, potentialPageVisibility.pageVisibilityChangeThreshold*1000+100); // +100 ms to avoid rounding issues under Firefox
          }
        }
     
        function dispatchPageVisibilityChangeEvent() {
          unifiedVisilityChangeEventDispatchAllowed=false;
          var evt = document.createEvent("Event");
          evt.initEvent("potentialvisilitychange", true, true);
          document.dispatchEvent(evt);
        }
     
        function checkPageVisibility() {
          var potentialHiddenDuration=(hasFocusLocal || lastActionDate==null?0:Math.floor((new Date().getTime()-lastActionDate.getTime())/1000));
                                        document.potentiallyHiddenSince=potentialHiddenDuration;
          if (potentialHiddenDuration>=potentialPageVisibility.pageVisibilityChangeThreshold && !document.potentialHidden) {
            // page visibility change threshold raiched => raise the even
            document.potentialHidden=true;
            dispatchPageVisibilityChangeEvent();
          }
        }
                            
        var lastActionDate=null;
        var hasFocusLocal=true;
        var hasMouseOver=true;
        document.potentialHidden=false;
        document.potentiallyHiddenSince=0;
        var timeoutHandler = null;
     
        addEvent(document, "pageshow", function(event) {
          document.getElementById("x").innerHTML+="pageshow/doc:<br>";
        });
        addEvent(document, "pagehide", function(event) {
          document.getElementById("x").innerHTML+="pagehide/doc:<br>";
        });
        addEvent(window, "pageshow", function(event) {
          document.getElementById("x").innerHTML+="pageshow/win:<br>"; // raised when the page first shows
        });
        addEvent(window, "pagehide", function(event) {
          document.getElementById("x").innerHTML+="pagehide/win:<br>"; // not raised
        });
        addEvent(document, "mousemove", function(event) {
          lastActionDate=new Date();
        });
        addEvent(document, "mouseover", function(event) {
          hasMouseOver=true;
          setAsNotHidden();
        });
        addEvent(document, "mouseout", function(event) {
          hasMouseOver=false;
          initPotentiallyHiddenDetection();
        });
        addEvent(window, "blur", function(event) {
          hasFocusLocal=false;
          initPotentiallyHiddenDetection();
        });
        addEvent(window, "focus", function(event) {
          hasFocusLocal=true;
          setAsNotHidden();
        });
        setAsNotHidden();
      }
    }
     
    potentialPageVisibility.pageVisibilityChangeThreshold=4; // 4 seconds for testing
    potentialPageVisibility.init();
    </script>

由于目前没有可靠的跨浏览器解决方案且会有误报,因此您最好再三考虑是否禁用网站上的定期活动。


在上面的代码中,使用严格比较运算符来比较字符串“undefined”而不是未定义的关键字,会导致假阳性吗? - Jonathon
@kiran:实际上,使用Alt+Tab是可以工作的。当你使用Alt+Tab切换窗口时,你无法确定页面是否被隐藏,因为你可能会切换到一个较小的窗口,所以无法保证你的页面完全被隐藏。这就是为什么我使用“潜在隐藏”的概念(在本例中,阈值设置为4秒,因此你需要使用Alt+Tab切换到另一个窗口至少4秒钟)。然而,你的评论表明答案并不是那么清晰,所以我重新措辞了一下。 - Julien Kronegg
@JulienKronegg 我认为这是目前最好的解决方案。然而,上面的代码极其需要一些重构和抽象性。为什么不将它上传到GitHub并让社区进行重构呢? - Jacob
2
@Jacob,很高兴你喜欢我的解决方案。请随意将其推广为GitHub项目。我提供的代码使用知识共享署名4.0国际许可证(Creative Commons BY)授权。 - Julien Kronegg
例如,如果用户在浏览器窗口上方显示一个较小的窗口,则浏览器窗口将失去焦点。你是在谈论iframe吗?如果是,那是正确的。 - Caleb Taylor
1
@Caleb 不是的,我指的是另一个应用程序在网页前面(例如计算器)。在这种情况下,网页失去了焦点,但仍然能够接收一些事件(例如鼠标悬停事件)。 - Julien Kronegg

38

问题并不是页面可见性,而是非活动/活动状态。 - gman
我认为OP并不是在谈论IDE的功能。 - l2aelba
2
我不是在谈论IDE。我是在谈论使用alt-tab/cmd-tab切换到另一个应用程序时,页面突然变得不活跃了。页面可见性API无法帮助我知道页面是否不活跃,它只能帮助我知道页面可能不可见。 - gman

30

起初,我使用了社区维基的答案,但是发现它无法检测到Chrome中的alt-tab事件。这是因为它使用了第一个可用的事件源,在这种情况下,它是页面可见性API,在Chrome中似乎无法跟踪alt-tab。

我决定稍微修改一下脚本,以跟踪所有可能的页面焦点变化事件。这里是一个可以插入的函数:

function onVisibilityChange(callback) {
    var visible = true;

    if (!callback) {
        throw new Error('no callback given');
    }

    function focused() {
        if (!visible) {
            callback(visible = true);
        }
    }

    function unfocused() {
        if (visible) {
            callback(visible = false);
        }
    }

    // Standards:
    if ('hidden' in document) {
        visible = !document.hidden;
        document.addEventListener('visibilitychange',
            function() {(document.hidden ? unfocused : focused)()});
    }
    if ('mozHidden' in document) {
        visible = !document.mozHidden;
        document.addEventListener('mozvisibilitychange',
            function() {(document.mozHidden ? unfocused : focused)()});
    }
    if ('webkitHidden' in document) {
        visible = !document.webkitHidden;
        document.addEventListener('webkitvisibilitychange',
            function() {(document.webkitHidden ? unfocused : focused)()});
    }
    if ('msHidden' in document) {
        visible = !document.msHidden;
        document.addEventListener('msvisibilitychange',
            function() {(document.msHidden ? unfocused : focused)()});
    }
    // IE 9 and lower:
    if ('onfocusin' in document) {
        document.onfocusin = focused;
        document.onfocusout = unfocused;
    }
    // All others:
    window.onpageshow = window.onfocus = focused;
    window.onpagehide = window.onblur = unfocused;
};

像这样使用:

onVisibilityChange(function(visible) {
    console.log('the page is now', visible ? 'focused' : 'unfocused');
});

这个版本监听所有不同的可见性事件,如果它们中的任何一个发生变化,则触发回调。 focusedunfocused 处理程序确保如果多个API捕获相同的可见性变化,则回调不会被多次调用。


1
例如,Chrome浏览器同时具有document.hiddendocument.webkitHidden。如果在if语句中没有else,我们会得到2个回调调用,对吧? - Christiaan Westerbeek
@ChristiaanWesterbeek 非常好的观点,我没有想到!如果您可以编辑此帖,请继续,我会接受 :) - Daniel Buckmaster
1
经过今天的测试,它至少在Chrome 78 + macOS上无法检测到alt+tab。 - Hugo Gresse
1
@HugoGresse 这段代码在Chrome + MacOS上完美运行。 - Cory Robinson
1
@DanielBuckmaster 这仍然存在(不受控制的)iframe问题,此外还存在一个问题,即在用户执行某种操作之前它不起作用。代码运行并且事件被连接,但是在alt+tab组合键之前,它们不会触发任何活动事件(切换元素,单击页面等)。这意味着如果您加载页面而没有进行任何页面操作,当您切换时您将不会获得未聚焦事件,也不会在回来时获得聚焦事件。 - AntonOfTheWoods
显示剩余2条评论

28

在GitHub上有一个很棒的库:

https://github.com/serkanyersen/ifvisible.js

示例:

// If page is visible right now
if( ifvisible.now() ){
  // Display pop-up
  openPopUp();
}

我已在所有我拥有的浏览器上测试了1.0.1版本,并确认它可以与以下浏览器配合使用:

  • IE9,IE10
  • FF 26.0
  • Chrome 34.0

......可能也适用于所有更新版本的浏览器。

但是与以下浏览器配合使用时存在问题:

  • IE8-始终指示选项卡/窗口当前处于活动状态(对我而言.now()始终返回true

1
被接受的答案在IE9中出现了问题。这个库很好用。 - Tom Teman
2
这个库已经完全被抛弃了。虽然它看起来有一个 TypeScript 版本,但它在 VSCode 中不再工作,即使复制/粘贴源代码也有很多不再被认为是 TypeScript 的良好实践的东西。 - AntonOfTheWoods

23

我为我的应用程序创建了Comet Chat,当我从另一个用户那里收到消息时,我使用:

if(new_message){
    if(!document.hasFocus()){
        audio.play();
        document.title="Have new messages";
    }
    else{
        audio.stop();
        document.title="Application Name";
    } 
}

3
最干净的解决方案,支持返回到IE6。 - Paul Cooper
8
document.hasFocus() 是最简洁的方法。所有其他使用可见性API或事件驱动或寻找各种用户活动/不活动级别的方式都变得过于复杂,充满了边缘情况和漏洞。将其放在简单的时间间隔上,并在结果更改时引发自定义事件。示例:https://jsfiddle.net/59utucz6/1/ - danatcofo
1
高效,并且与其他解决方案不同的是,当您切换到另一个浏览器选项卡或窗口,甚至不同的应用程序时,它会提供正确的反馈。 - ow3n
毫无疑问,这是最干净的方法,但在火狐浏览器中不起作用。 - hardik chugh
1
如果我打开Chrome Dev工具,那么document.hasFocus()等于false。即使您单击浏览器的顶部面板,情况也是如此。我不确定这个解决方案是否适用于暂停视频、动画等。 - tylik
@tylik,在您使用开发工具时,文档将不会获得焦点。将此代码粘贴到JS控制台中,然后单击文档,输出应从“false”更改为“true”。代码:setInterval(() => { console.log(document.hasFocus()) }, 1000); - Walter Stabosz

10

这适用于所有现代浏览器:

  • 切换标签页时
  • 切换窗口(Alt+Tab)时
  • 从任务栏最大化另一个程序时
var eventName;
var visible = true;
var propName = "hidden";
if (propName in document) eventName = "visibilitychange";
else if ((propName = "msHidden") in document) eventName = "msvisibilitychange";
else if ((propName = "mozHidden") in document) eventName = "mozvisibilitychange";
else if ((propName = "webkitHidden") in document) eventName = "webkitvisibilitychange";
if (eventName) document.addEventListener(eventName, handleChange);

if ("onfocusin" in document) document.onfocusin = document.onfocusout = handleChange; //IE 9
window.onpageshow = window.onpagehide = window.onfocus = window.onblur = handleChange;// Changing tab with alt+tab

// Initialize state if Page Visibility API is supported
if (document[propName] !== undefined) handleChange({ type: document[propName] ? "blur" : "focus" });

function handleChange(evt) {
  evt = evt || window.event;
  if (visible && (["blur", "focusout", "pagehide"].includes(evt.type) || (this && this[propName]))){
    visible = false;
    console.log("Out...")
  }
  else if (!visible && (["focus", "focusin", "pageshow"].includes(evt.type) || (this && !this[propName]))){
    visible = true;
    console.log("In...")
  }
}

2
这个解决方案非常好,即使打开开发者工具也能触发它,非常棒! - Haim
2
不错的技巧!这应该更高。 - undefined

9
这真的很棘手。考虑到以下要求,似乎没有解决方案。
- 页面包含您无法控制的 iframe。 - 您想要跟踪可见性状态的更改,而不管是由 TAB 更改 (ctrl+tab) 还是窗口更改 (alt+tab) 触发的。
这是因为:
- 页面的可见性 API 可以可靠地告诉您选项卡的更改(即使有 iframe),但它不能告诉您用户何时更改窗口。 - 监听窗口的 blur/focus 事件可以检测到 alt+tab 和 ctrl+tab,只要 iframe 没有焦点。
在这些限制下,可以实现一种解决方案,结合了:
- 页面可见性 API - 窗口 blur/focus - document.activeElement
能够实现以下功能:
- 当父页面具有焦点时进行 ctrl+tab:是 - 当 iframe 具有焦点时进行 ctrl+tab:是 - 当父页面具有焦点时进行 alt+tab:是 - 当 iframe 具有焦点时进行 alt+tab:否 - 很遗憾
当 iframe 获得焦点时,你的模糊/聚焦事件根本不会被调用,页面可见性 API 也不会在 alt+tab 上触发。
我基于 @AndyE 的解决方案并实现了这个(几乎好的)解决方案:https://dl.dropboxusercontent.com/u/2683925/estante-components/visibility_test1.html(抱歉,我在 JSFiddle 上遇到了一些问题)。
这也可以在 Github 上找到:https://github.com/qmagico/estante-components 这适用于 chrome/chromium。 它在 firefox 上运行得很好,但是它不会加载 iframe 内容(有什么想法吗?)
无论如何,要解决最后一个问题(4),您唯一能做的就是监听 iframe 上的模糊/聚焦事件。 如果您对 iframe 有一定的控制权,则可以使用 postMessage API 来实现。

https://dl.dropboxusercontent.com/u/2683925/estante-components/visibility_test2.html

我还没有在足够多的浏览器上测试过这个。 如果你能找到更多关于它不能工作的信息,请在下面的评论中让我知道。


在我的测试中,它也能在IE9、IE10以及Android上的Chrome上运行。 - Tony Lâmpada
1
似乎IPAD需要完全不同的解决方案 - https://dev59.com/oW445IYBdhLWcg3wTIVf - Tony Lâmpada
4
所有这些链接都是404错误页面 :( - Daniel Buckmaster

8
var visibilityChange = (function (window) {
    var inView = false;
    return function (fn) {
        window.onfocus = window.onblur = window.onpageshow = window.onpagehide = function (e) {
            if ({focus:1, pageshow:1}[e.type]) {
                if (inView) return;
                fn("visible");
                inView = true;
            } else if (inView) {
                fn("hidden");
                inView = false;
            }
        };
    };
}(this));

visibilityChange(function (state) {
    console.log(state);
});

http://jsfiddle.net/ARTsinn/JTxQY/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接