89

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

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

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

我还尝试在用户启动滚动期间存储文档的滚动位置,并在浏览器滚动页面后恢复它(在 popstate 上)。这在 Firefox 12 中运行良好,但在 Chrome 19 中,由于页面正在滚动和恢复,会出现闪烁。我认为这与滚动和被触发的滚动事件(滚动位置恢复)之间的延迟有关。

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

我见过的所有使用历史 API 的网站要么使用与上述类似的解决方案,要么在用户返回/前进时忽略滚动位置的变化(例如 GitHub)。

是否可以在 popstate 事件中完全防止文档滚动?

4

11 回答 11

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

谷歌于 2015 年 9 月 2 日宣布)

浏览器支持:

Chrome:支持(从 46 开始)

Firefox:支持(从 46 开始)

IE:不支持

边缘:支持(从 79 开始)

Opera:支持(33 起)

Safari:支持

有关详细信息,请参阅MDN 上的浏览​​器兼容性

于 2015-10-08T01:01:40.273 回答
32

一年多来,这个问题一直是 Mozilla 开发者核心的一个报告问题。不幸的是,票并没有真正进展。我认为 Chrome 是一样的:没有可靠的方法来onpopstate通过 js 处理滚动位置,因为它是本机浏览器行为。

但是,如果您查看HTML5 历史规范,那么未来还是有希望的,它明确希望滚动位置在状态对象上表示:

历史对象将其浏览上下文的会话历史表示为会话历史条目的平面列表。每个会话历史条目由一个 URL 和一个可选的状态对象组成,并且可能还具有标题、文档对象、表单数据、滚动位置以及与其相关的其他信息。

这一点,如果您阅读了上面提到的 Mozilla 票证上的评论,则表明在不久的将来滚动位置可能不会再恢复onpopstate,至少对于使用pushState.

不幸的是,在此之前,滚动位置在使用时被存储pushState,并且replaceState不会替换滚动位置。否则,这将相当容易,并且您可以replaceState在每次用户滚动页面时使用设置当前滚动位置(使用一些谨慎的 onscroll 处理程序)。

同样不幸的是,HTML5 规范没有具体说明何时popstate必须触发该事件,它只是说:“在导航到会话历史记录条目时在某些情况下触发”,它没有明确说明它是之前还是之后;如果它总是在之前,则可以解决处理 popstate 之后发生的滚动事件的解决方案。

取消滚动事件?

此外,如果滚动事件可以取消,这也很容易,但事实并非如此。如果是这样,您可以取消系列的第一个滚动事件(用户滚动事件就像旅鼠一样,它们有几十个,而历史重新定位触发的滚动事件是一个),您会没事的。

暂时没有解决办法

据我所知,我现在唯一推荐的是等待 HTML5 规范完全实现并在这种情况下随浏览器行为滚动,这意味着:当浏览器允许时为滚动设置动画,并让浏览器在有历史事件时重新定位页面。唯一可以影响位置的事情是pushState当页面以一种好的方式定位时使用。任何其他解决方案要么必然有错误,要么过于特定于浏览器,或两者兼而有之。

于 2012-08-20T21:05:14.717 回答
4

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.

于 2012-08-20T11:16:08.333 回答
3

现在你可以做

history.scrollRestoration = 'manual';

这应该可以防止浏览器滚动。这目前仅适用于 Chrome 46 及更高版本,但似乎 Firefox 也计划支持它

于 2015-12-25T16:18:34.877 回答
2

解决方案是使用position: fixed并指定top等于页面的滚动位置。

这是一个例子:

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

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

是的,您会收到闪烁的滚动条,但它不那么邪恶。

于 2014-06-06T13:01:35.517 回答
1

将 scrollRestoration 设置为手动对我不起作用,这是我的解决方案。

window.addEventListener('popstate', function(e) {
    var scrollTop = document.body.scrollTop;
    window.addEventListener('scroll', function(e) {
        document.body.scrollTop = scrollTop;
    });
});
于 2016-06-21T13:48:49.007 回答
1

以下修复应该适用于所有浏览器。

您可以在卸载事件上将滚动位置设置为 0。您可以在此处阅读有关此事件的信息: https ://developer.mozilla.org/en-US/docs/Web/Events/unload 。本质上,卸载事件会在您离开页面之前触发。

通过在卸载时将 scrollPosition 设置为 0 意味着当您以设置的 pushState 离开页面时,它将 scrollPosition 设置为 0。当您通过刷新或按下返回到此页面时,它不会自动滚动。

//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);
});
于 2015-10-20T15:41:17.413 回答
0

就像其他人说的那样,没有真正的方法可以做到,只有丑陋的hacky方式。删除滚动事件侦听器对我不起作用,所以这是我的丑陋方法:

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

});

它适用于 Chrome、FF 和 IE(第一次返回 IE 时它会闪烁)。欢迎任何改进建议!希望这可以帮助。

于 2014-08-01T19:41:15.537 回答
0

在页面顶部的某处创建一个“跨度”元素,并在加载时将焦点设置为该元素。浏览器将滚动到焦点元素。我知道这是一种解决方法,并且专注于“跨度”不适用于所有浏览器(uhmmm.. Safari)。希望这可以帮助。

于 2013-01-05T17:42:01.533 回答
0

这是我在一个网站上实现的,该网站希望在触发 poststate 时将滚动位置聚焦到特定元素(后退按钮):

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

经过测试并在 Firefox 中工作。

Chrome 将滚动到某个位置,然后重新定位回原来的位置。

于 2013-02-04T13:04:13.620 回答
-5

Might want to try this?

window.onpopstate = function(event) {
  return false;
};
于 2012-08-15T22:00:31.090 回答