Chrome浏览器中页面加载时的Popstate

95

我正在为我的Web应用使用History API,但是有一个问题。我通过Ajax调用更新页面上的一些结果,并使用history.pushState()来更新浏览器的位置栏而不重新加载页面。然后,当单击后退按钮时,我使用window.popstate以便恢复先前的状态。

这个问题是众所周知的——Chrome 和Firefox对popstate事件处理不同。虽然Firefox在第一次加载时不会触发它,但Chrome会。 我想要像Firefox一样,不要在加载时触发该事件,因为它只是在加载时更新了完全相同的结果。除了使用History.js之外,是否有其他解决方法?我不想使用它的原因是——它本身需要太多的JS库,而且由于我需要将其实现在已经有太多JS的CMS中,因此我希望将放入其中的JS数量最小化。

所以,我想知道是否有办法使Chrome在加载时不触发popstate,或者是否有人尝试过将所有库混合在一起成为一个文件使用History.js。


3
另外,Firefox的行为是规范和正确的行为。自Chrome 34以来,现在已经修复了!感谢Opera开发人员! - Henry
15个回答

49

在Google Chrome版本19中,@spliter提供的解决方案已经失效。正如@johnnymire所指出的那样,在Chrome 19中存在history.state,但它是null。

我的解决方法是在检查window.history中是否存在state时添加 window.history.state!== null

var popped = ('state' in window.history && window.history.state !== null), initialURL = location.href;

我在所有主流浏览器和 Chrome 版本 19 和 18 中进行了测试。看起来它可以工作。


5
谢谢提醒;我认为你可以用!= null来简化innull的检查,因为它也会处理undefined - pimvdb
5
只有当您在 history.pushState() 方法中始终使用 null,例如 history.pushState(null,null,'http//example.com/) 时,才能使用此功能。尽管这可能是大多数用法(如jquery.pjax.js和其他大多数演示中设置的那样),但如果浏览器实现了 window.history.state(例如FF和Chrome 19+),则在刷新或浏览器重新启动后,window.history.state 可能不为 null。 - unexplainedBacn
19
在Chrome 21中,这对我不再起作用。我最终只是在页面加载时将“popped”设置为false,然后在任何推送或弹出时将其设置为true。这会使Chrome在首次加载时无法弹出。这里有更好的解释:https://github.com/defunkt/jquery-pjax/issues/143#issuecomment-6194330 - Chad von Nau
在@ChadvonNau发布的相同链接中,Josh回答说只需要使用条件:(event.state && event.state.container)。这对我起作用了。 - Fractalf
感谢 Chad von Nau 提供的解决方案!我花了一天时间解决这个问题。但我最大的问题是意识到 AJAX 和常规 URL 不能完全相同,而 Chrome 似乎处理不好这种情况。(https://dev59.com/SG015IYBdhLWcg3w7wQW#6836213) - Flavio Wuensche
显示剩余4条评论

30

如果您不想为添加到onpopstate的每个处理程序采取特殊措施,那么我的解决方案可能会对您有所帮助。这种解决方案的一个重要优点是,在页面加载完成之前就可以处理onpopstate事件。

在添加任何onpopstate处理程序之前只需要运行此代码一次,然后一切都应该按预期工作(就像在Mozilla中一样)

(function() {
    // There's nothing to do for older browsers ;)
    if (!window.addEventListener)
        return;
    var blockPopstateEvent = document.readyState!="complete";
    window.addEventListener("load", function() {
        // The timeout ensures that popstate-events will be unblocked right
        // after the load event occured, but not in the same event-loop cycle.
        setTimeout(function(){ blockPopstateEvent = false; }, 0);
    }, false);
    window.addEventListener("popstate", function(evt) {
        if (blockPopstateEvent && document.readyState=="complete") {
            evt.preventDefault();
            evt.stopImmediatePropagation();
        }
    }, false);
})();

它是如何工作的:

Chrome、Safari以及可能其他基于Webkit内核的浏览器会在文档加载完成后触发onpopstate事件。这并不是预期的行为,因此我们阻止popstate事件直到文档加载完成后第一个事件循环周期。这是通过preventDefault和stopImmediatePropagation调用实现的(与stopPropagation不同,stopImmediatePropagation可以立即停止所有事件处理程序的调用)。

然而,由于Chrome在文档加载完成时已经处于“complete”状态,会错误地触发onpopstate事件,因此允许在文档加载完成之前已被触发的opopstate事件,在文档加载完成之前允许onpopstate的调用。

更新2014-04-23: 修复了一个bug,该bug导致如果脚本在页面加载后执行,则popstate事件将被阻止。


4
目前对我来说是最佳解决方案。我多年来一直使用setTimeout方法,但当页面加载缓慢或用户快速触发pushstate时,它会导致错误行为。如果已设置状态,则重新加载时window.history.state会失败。在使用pushState之前设置变量会使代码混乱。您的解决方案优雅且可以放置在其他脚本之上,而不会影响代码库的其余部分。谢谢。 - kik
4
这就是解决方案!如果我几天前在尝试解决这个问题时能往下读这么远,那该多好啊。这是唯一一个完美地发挥作用的方法。谢谢! - Ben
1
优秀的解释,也是唯一值得尝试的解决方案。 - Sunny R Gupta
纯金!适当处理,不依赖于计时器! - Mikael Gidmark
1
哇!如果Safari让你心烦意乱,这些就是你要找的代码! - DanO
显示剩余2条评论

28

仅使用setTimeout并不是一个正确的解决方案,因为你不知道加载内容需要多长时间,所以在超时之后可能会发出popstate事件。

这是我的解决方案:https://gist.github.com/3551566

/*
* Necessary hack because WebKit fires a popstate event on document load
* https://code.google.com/p/chromium/issues/detail?id=63040
* https://bugs.webkit.org/process_bug.cgi
*/
window.addEventListener('load', function() {
  setTimeout(function() {
    window.addEventListener('popstate', function() {
      ...
    });
  }, 0);
});

3
太好了!比最受赞同和/或被采纳的答案还要好用! - ProblemsOfSumit
有人有这个解决方案的有效链接吗?我无法访问给定的链接https://gist.github.com/3551566 - Harbir
1
你为什么需要要点? - Oleg Vaskevich
完美的答案。谢谢。 - sideroxylon

25
解决方案已经在jquery.pjax.js的195-225行找到:

jquery.pjax.js

// Used to detect initial (useless) popstate.
// If history.state exists, assume browser isn't going to fire initial popstate.
var popped = ('state' in window.history), initialURL = location.href


// popstate handler takes care of the back and forward buttons
//
// You probably shouldn't use pjax on pages with other pushState
// stuff yet.
$(window).bind('popstate', function(event){
  // Ignore inital popstate that some browsers fire on page load
  var initialPop = !popped && location.href == initialURL
  popped = true
  if ( initialPop ) return

  var state = event.state

  if ( state && state.pjax ) {
    var container = state.pjax
    if ( $(container+'').length )
      $.pjax({
        url: state.url || location.href,
        fragment: state.fragment,
        container: container,
        push: false,
        timeout: state.timeout
      })
    else
      window.location = location.href
  }
})

8
这个解决方案在我使用的Windows上失效了(Chrome 19),但在Mac上仍然有效(Chrome 18)。看起来在Chrome 19中存在history.state,但在Chrome 18中不存在。 - johnnymire
1
@johnnymire 我发布了适用于Chrome 19以下版本的解决方案:https://dev59.com/Qmw15IYBdhLWcg3wv-VM#10651028 - Pavel Linkesch
有人应该编辑这个答案以反映Chrome 19之后的行为。 - Jorge Suárez de Lis
5
对于任何寻找解决方案的人,截至今天,Torben的回答在当前版本的Chrome和Firefox上非常有效。 - Jorge Suárez de Lis
1
现在是2015年4月,我使用了这个解决方案来解决Safari的一个问题。我必须使用onpopstate事件才能处理在使用Cordova的混合iOS/Android应用程序上按下后退按钮。Safari在重新加载后会触发其onpopstate事件,我通过编程方式调用了它。由于我的代码设置方式,调用reload()后进入了无限循环。这个解决方法修复了它。我只需要事件声明后的前3行(以及顶部的赋值)。 - Martavis P.

14

比重新实现pjax更直接的解决方案是,在pushState时设置一个变量,并在popState时检查该变量,以便初始popState不会在加载时不一致地触发(这不是特定于jQuery的解决方案,只是在事件上使用它):

$(window).bind('popstate', function (ev){
  if (!window.history.ready && !ev.originalEvent.state)
    return; // workaround for popstate on load
});

// ... later ...

function doNavigation(nextPageId) {
  window.history.ready = true;

  history.pushState(state, null, 'content.php?id='+ nextPageId); 
  // ajax in content instead of loading server-side
}

那个if语句不是总是会返回false吗?我的意思是,window.history.ready总是为true。 - Andriy Drozdyuk
将示例更新为更清晰一些。基本上,您只想在给出所有清除后处理popstate事件。您可以在加载后使用setTimeout 500ms实现相同的效果,但说实话,这有点便宜。 - Tom McKenzie
3
我认为这应该是答案。我尝试了Pavels的解决方案,但在Firefox浏览器中无法正常工作。 - Porco
这对我来说是一个简单的解决方案,解决了这个烦人的问题。非常感谢! - nirazul
当我刷新页面时,“!window.history.ready && !ev.originalEvent.state”这两个东西从来没有被定义过,因此没有任何代码会执行。 - chovy
在页面加载期间,window.history.ready 没有定义,只有在处理链接点击之后并在调用 pushState 之前才会定义。如果需要的话,您还应该在 ready 处理程序中加载适当的内容。在此示例中,progress.php 将使用 url 参数(服务器端)进行加载。否则,我们将通过 ajax 加载 url 参数。 - Tom McKenzie

4

Webkit的初始onpopstate事件没有分配状态,因此您可以使用它来检查不需要的行为:

window.onpopstate = function(e){
    if(e.state)
        //do something
};

一种全面的解决方案,允许返回原始页面的导航,将建立在这个想法之上:

<body onload="init()">
    <a href="page1" onclick="doClick(this); return false;">page 1</a>
    <a href="page2" onclick="doClick(this); return false;">page 2</a>
    <div id="content"></div>
</body>

<script>
function init(){
   openURL(window.location.href);
}
function doClick(e){
    if(window.history.pushState)
        openURL(e.getAttribute('href'), true);
    else
        window.open(e.getAttribute('href'), '_self');
}
function openURL(href, push){
    document.getElementById('content').innerHTML = href + ': ' + (push ? 'user' : 'browser'); 
    if(window.history.pushState){
        if(push)
            window.history.pushState({href: href}, 'your page title', href);
        else
            window.history.replaceState({href: href}, 'your page title', href);
    }
}
window.onpopstate = function(e){
    if(e.state)
        openURL(e.state.href);
};
</script>

虽然这个操作可能会触发两次(通过一些巧妙的导航),但只需检查前一个href,就可以简单地处理它。


1
谢谢。pjax解决方案在iOS 5.0上停止工作,因为window.history.state在iOS中也为null。只需检查e.state属性是否为null即可。 - Husky
这个解决方案有没有已知的问题?我在测试的任何浏览器上都能完美运行。 - Jacob Ewing

3
这是我的解决方法。
window.setTimeout(function() {
  window.addEventListener('popstate', function() {
    // ...
  });
}, 1000);

不适用于我的Ubuntu上的Chromium 28。有时,事件仍然会被触发。 - Jorge Suárez de Lis
我自己尝试过,但并不总是有效。有时候,根据初始页面加载情况,popstate事件会比超时更晚触发。 - Andrew Burgess

1

这是我的解决方案:

var _firstload = true;
$(function(){
    window.onpopstate = function(event){
        var state = event.state;

        if(_firstload && !state){ 
            _firstload = false; 
        }
        else if(state){
            _firstload = false;
            // you should pass state.some_data to another function here
            alert('state was changed! back/forward button was pressed!');
        }
        else{
            _firstload = false;
            // you should inform some function that the original state returned
            alert('you returned back to the original state (the home state)');
        }
    }
})   

0

2
这个问题已经在Chrome 34中得到了修复。 - matys84pl

0

如果使用event.state !== null在非移动浏览器中返回到最初加载的页面将无法工作。 我使用sessionStorage来标记ajax导航真正开始的时间。

history.pushState(url, null, url);
sessionStorage.ajNavStarted = true;

window.addEventListener('popstate', function(e) {
    if (sessionStorage.ajNavStarted) {
        location.href = (e.state === null) ? location.href : e.state;
    }
}, false);


window.onload = function() { if (history.state === null) { history.replaceState(location.pathname + location.search + location.hash, null, location.pathname + location.search + location.hash); } };window.addEventListener('popstate', function(e) { if (e.state !== null) { location.href = e.state; } }, false); - ilusionist

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