页面加载中的 Popstate

我使用历史应用程序接口为我的网络应用程序,有一个问题。 我使用 Ajax 调用来更新页面上的一些结果,并使用 history.pushState()来更新浏览器的位置栏,而无需重新加载页面。然后,当然,我使用 window.popstate以便在单击后退按钮时恢复以前的状态。

这个问题是众所周知的ーー Chrome 和 Firefox 对待 popstate事件的方式不同。火狐在第一次加载时没有启动,但 Chrome 启动了。我希望拥有火狐风格,而不是在加载时启动事件,因为它只是在加载时用完全相同的结果更新。除了使用 V 史,还有其他解决办法吗?我不喜欢使用它的原因是ーー它本身需要太多的 JS 库,而且,由于我需要在一个已经有太多 JS 的 CMS 中实现它,所以我想尽量减少我放入的 JS。

所以,想知道是否有一种方法可以让 Chrome 不会在加载时启动 popstate,或者,也许,有人试图使用 History.js 将所有的库混合到一个文件中。

58690 次浏览

You can create an event and fire it after your onload handler.

var evt = document.createEvent("PopStateEvent");
evt.initPopStateEvent("popstate", false, false, { .. state object  ..});
window.dispatchEvent(evt);

Note, this is slightly broke in Chrome/Safari, but I have submitted the patch in to WebKit and it should be available soon, but it is the "most correct" way.

The solution has been found in jquery.pjax.js lines 195-225:

// 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
}
})

A more direct solution than reimplementing pjax is set a variable on pushState, and check for the variable on popState, so the initial popState doesn't inconsistently fire on load (not a jquery-specific solution, just using it for events):

$(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
}

This is my workaround.

window.setTimeout(function() {
window.addEventListener('popstate', function() {
// ...
});
}, 1000);

Webkit's initial onpopstate event has no state assigned, so you can use this to check for the unwanted behaviour:

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

A comprehensive solution, allowing for navigation back to the original page, would build on this idea:

<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>

While this could still fire twice (with some nifty navigation), it can be handled simply with a check against the previous href.

In Google Chrome in version 19 the solution from @spliter stopped working. As @johnnymire pointed out, history.state in Chrome 19 exists, but it's null.

My workaround is to add window.history.state !== null into checking if state exists in window.history:

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

I tested it in all major browsers and in Chrome versions 19 and 18. It looks like it works.

Using setTimeout only isn't a correct solution because you have no idea how long it will take for the content to be loaded so it's possible the popstate event is emitted after the timeout.

Here is my solution: 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);
});

Here's my solution:

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)');
}
}
})

The presented solutions have a problem on page reload. The following seems to work better, but I have only tested Firefox and Chrome. It uses the actuality, that there seems to be a difference between e.event.state and window.history.state.

window.addEvent('popstate', function(e) {
if(e.event.state) {
window.location.reload(); // Event code
}
});

In case you do not want to take special measures for each handler you add to onpopstate, my solution might be interesting for you. A big plus of this solution is also that onpopstate events can be handled before the page loading has been finished.

Just run this code once before you add any onpopstate handlers and everything should work as expected (aka like in 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);
})();

How it works:

Chrome, Safari and probably other webkit browsers fire the onpopstate event when the document has been loaded. This is not intended, so we block popstate events until the the first event loop cicle after document has been loaded. This is done by the preventDefault and stopImmediatePropagation calls (unlike stopPropagation stopImmediatePropagation stops all event handler calls instantly).

However, since the document's readyState is already on "complete" when Chrome fires onpopstate erroneously, we allow opopstate events, which have been fired before document loading has been finished to allow onpopstate calls before the document has been loaded.

Update 2014-04-23: Fixed a bug where popstate events have been blocked if the script is executed after the page has been loaded.

The best way to get Chrome to not fire popstate on a page load is to up-vote https://code.google.com/p/chromium/issues/detail?id=63040. They've known Chrome isn't in compliance with the HTML5 spec for two full years now and still haven't fixed it!

I know you asked against it, but you should really just use History.js as it clears up a million browser incompatibilities. I went the manual fix route only to later find there were more and more problems that you'll only find out way down the road. It really isn't that hard nowadays:

<script src="//cdnjs.cloudflare.com/ajax/libs/history.js/1.8/native.history.min.js" type="text/javascript"></script>

And read the api at https://github.com/browserstate/history.js

This worked for me in Firefox and Chrome

window.onpopstate = function(event) { //back button click
console.log("onpopstate");
if (event.state) {
window.location.reload();
}
};

This solved the problem for me. All I did was set a timeout function which delays the execution of the function long enough to miss the popstate event that is fired on pageload

if (history && history.pushState) {
setTimeout(function(){
$(window).bind("popstate", function() {
$.getScript(location.href);
});
},3000);
}

In case of use event.state !== null returning back in history to first loaded page won't work in non mobile browsers. I use sessionStorage to mark when ajax navigation really starts.

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);