防止浏览器在 HTML5的历史记录状态上滚动

当 popstate 事件发生时,是否可以防止文档的默认滚动行为?

我们的站点使用 jQuery 动画滚动和 History.js,状态更改应该通过 pushstate 或 popstate 将用户滚动到页面的不同区域。问题是当 popstate 事件发生时,浏览器会自动恢复前一个状态的滚动位置。

我尝试使用一个设置为文档100% 宽度和高度的容器元素,并滚动该容器中的内容。我发现这样做的问题是,它似乎不像滚动文档那样流畅; 特别是如果使用大量的 css3,比如框阴影和渐变。

我还尝试在用户启动滚动时存储文档的滚动位置,并在浏览器滚动页面后(在 popstate 上)恢复它。这在 Firefox12中可以很好地工作,但是在 Chrome19中,由于页面被滚动和恢复,会出现一个闪烁。我假设这是因为在滚动和触发滚动事件之间存在延迟(滚动位置在这里被恢复)。

Firefox 在 popstate 触发之前滚动页面(并触发滚动事件) ,Chrome 首先触发 popstate,然后滚动文档。

我见过的所有使用历史 API 的站点,要么使用类似于上面那些的解决方案,要么在用户返回/转发时忽略滚动位置的变化(例如 GitHub)。

是否有可能防止文档在 popstate 事件上滚动?

30564 次浏览

Might want to try this?

window.onpopstate = function(event) {
return false;
};

You're going to have to use some kind of horrible browser sniffing here. For Firefox, I would go with your solution of storing the scroll position and restoring it.

I thought I had a good Webkit solution based on your description, but I just tried in Chrome 21, and it seems that Chrome scrolls first, then fires the popstate event, then fires the scroll event. But for reference, here's what I came up with:

function noScrollOnce(event) {
event.preventDefault();
document.removeEventListener('scroll', noScrollOnce);
}
window.onpopstate = function () {
document.addEventListener('scroll', noScrollOnce);
};​

Black magic such as pretending the page is scrolling by moving an absolute positioned element is ruled out by the screen repainting speed too.

So I'm 99% sure that the answer is that you can't, and you're going to have to use one of the compromises you've mentioned in the question. Both browsers scroll before JavaScript knows anything about it, so JavaScript can only react after the event. The only difference is that Firefox doesn't paint the screen until after the Javascript has fired, which is why there's a workable solution in Firefox but not in WebKit.

This has been a reported issue with the mozilla developer core for more than a year now. Unfortunately, the ticket did not really progress. I think Chrome is the same: There is no reliable way to tackle the scroll position onpopstate via js, since it's native browser behaviour.

There is hope for the future though, if you look at the HTML5 history spec, which explicitly wishes for the scroll position to be represented on the state object:

History objects represent their browsing context's session history as a flat list of session history entries. Each session history entry consists of a URL and optionally a state object, and may in addition have a title, a Document object, form data, a scroll position, and other information associated with it.

This, and if you read the comments on the mozilla ticket mentioned above, gives some indication that it is possible that in the near future scroll position will not be restored anymore onpopstate, at least for people using pushState.

Unfortunately, until then, the scroll position gets stored when pushState is used, and replaceState does not replace the scroll position. Otherwise, it would be fairly easy, and you could use replaceState to set the current Scroll position everytime the user has scrolled the page (with some cautious onscroll handler).

Also unfortunately, the HTML5 spec does not specify when exactly the popstate event has to be fired, it just says: «is fired in certain cases when navigating to a session history entry», which does not clearly say if it's before or after; if it was always before, a solution with handling the scroll event occuring after the popstate would be possible.

Cancel the scroll event?

Furthermore, it would also be easy, if the scroll event where cancelable, which it isn't. If it was, you could just cancel the first scroll event of a series (user scroll events are like lemmings, they come in dozens, whereas the scroll event fired by the history repositioning is a single one), and you would be fine.

There's no solution for now

As far as I see, the only thing I'd recommend for now is to wait for the HTML5 Spec to be fully implemented and to roll with the browser behaviour in this case, that means: animate the scrolling when the browser lets you do it, and let the browser reposition the page when there's a history event. The only thing you can influence position-wise is that you use pushState when the page is positioned in a good way to go back to. Any other solution is either bound to have bugs, or to be too browser-specific, or both.

Create a 'span' element somewhere at the top of the page and set focus to this on load. The browser will scroll to the focussed element. I understand that this is a workaround and focus on 'span' doesn't work in all browsers ( uhmmm.. Safari ). Hope this helps.

Here is what I have implemented on a site that wanted the scroll position to focus to a specific element when the poststate is fired (back button):

$(document).ready(function () {


if (window.history.pushState) {
//if push supported - push current page onto stack:
window.history.pushState(null, document.title, document.location.href);
}


//add handler:
$(window).on('popstate', PopStateHandler);
}


//fires when the back button is pressed:
function PopStateHandler(e) {
emnt= $('#elementID');
window.scrollTo(0, emnt.position().top);
alert('scrolling to position');
}

Tested and works in firefox.

Chrome will scroll to position but then repositions back to original place.

The solution is to use position: fixed and specify top equal to scroll position of page.

Here is an example:

$(window).on('popstate', function()
{
$('.yourWrapAroundAllContent').css({
position: 'fixed',
top: -window.scrollY
});


requestAnimationFrame(function()
{
$('.yourWrapAroundAllContent').css({
position: 'static',
top: 0
});
});
});

Yes, you instead receive flickering scrollbar, but it is less evil.

Like others said, there is no real way to do it, only ugly hacky way. Removing the scroll event listener didn't work for me, so here's my ugly way to do it:

/// GLOBAL VARS ////////////////////////
var saveScrollPos = false;
var scrollPosSaved = window.pageYOffset;
////////////////////////////////////////


jQuery(document).ready(function($){


//// Go back with keyboard shortcuts ////
$(window).keydown(function(e){
var key = e.keyCode || e.charCode;


if((e.altKey && key == 37) || (e.altKey && key == 39) || key == 8)
saveScrollPos = false;
});
/////////////////////////////////////////


//// Go back with back button ////
$("html").bind("mouseout",  function(){ saveScrollPos = false; });
//////////////////////////////////


$("html").bind("mousemove", function(){ saveScrollPos = true; });


$(window).scroll(function(){
if(saveScrollPos)
scrollPosSaved = window.pageYOffset;
else
window.scrollTo(0, scrollPosSaved);
});


});

It works in Chrome, FF and IE (it flashes the first time you go back in IE). Any improvement suggestions are welcome! Hope this helps.

if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}

(Announced by Google on September 2, 2015)

Browser support:

Chrome: supported (since 46)

Firefox: supported (since 46)

IE: not supported

Edge: supported (since 79)

Opera: supported (since 33)

Safari: supported

For more info, see Browser compatibility on MDN.

The following fix should work in all browsers.

You can set scroll position to 0 on the unload event. You can read about this event here: https://developer.mozilla.org/en-US/docs/Web/Events/unload. Essentially, the unload event fires right before you leave the page.

By setting scrollPosition to 0 on unload means when you leave the page with a set pushState it sets scrollPosition to 0. When you return to this page by refreshing or pressing back it will not autoscroll.

//Listen for unload event. This is triggered when leaving the page.
//Reference: https://developer.mozilla.org/en-US/docs/Web/Events/unload
window.addEventListener('unload', function(e) {
//set scroll position to the top of the page.
window.scrollTo(0, 0);
});

Now you can do

history.scrollRestoration = 'manual';

and this should prevent browser scroll. This only works right now in Chrome 46 and above, but it seems that Firefox is planning to support it too

Setting scrollRestoration to manual not worked for me, here is my solution.

window.addEventListener('popstate', function(e) {
var scrollTop = document.body.scrollTop;
window.addEventListener('scroll', function(e) {
document.body.scrollTop = scrollTop;
});
});