防止HTML5历史记录popstate事件触发浏览器滚动

91
当“popstate”事件发生时,是否可能防止文档默认滚动行为?我们的网站使用jQuery动画滚动和History.js,状态更改应该通过pushstate或popstate将用户滚动到页面的不同区域。问题是浏览器在popstate事件发生时自动恢复先前状态的滚动位置。已尝试使用设置为100%宽度和高度的容器元素,并在容器内滚动内容。我发现这个方法的问题在于滚动文档似乎并不像滚动容器那样平滑,特别是如果使用了很多css3,如box-shadows和gradients。还尝试了在用户发起滚动期间存储文档的滚动位置,并在浏览器滚动页面(在popstate时)后恢复它。这在Firefox 12中运行良好,但在Chrome 19中存在闪烁,因为页面被滚动和恢复。我认为这与滚动和滚动事件被触发之间的延迟有关(其中恢复滚动位置)。Firefox在触发popstate之前滚动页面(并触发滚动事件),而Chrome首先触发popstate然后再滚动文档。我看过的所有使用历史API的网站都使用类似上述方法的解决方案或者在用户返回/前进时忽略滚动位置更改(例如GitHub)。是否可能在popstate事件上完全防止文档滚动?

你可能会在设置滚动位置后读取元素尺寸(以强制页面回流)方面取得一些成功。例如:document.body.clientHeight,但我没有看到它在所有浏览器中都能正常工作。 - Sean Hogan
如果您操纵了文档的滚动功能,那至少是我会开始的地方。这里有一个解决办法:https://dev59.com/jmw05IYBdhLWcg3wxkqr - Jeff Wooden
11个回答

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

(这一功能由Google于2015年9月2日宣布)

浏览器支持情况:

Chrome:支持(自版本46起)

Firefox:支持(自版本46起)

IE:不支持

Edge:支持(自版本79起)

Opera:支持(自版本33起)

Safari:支持

有关更多信息,请参见MDN上的浏览器兼容性.


1
缩短版: history.scrollRestoration && history.scrollRestoration = 'manual'; 或者甚至是 var sr = 'scrollRestoration'; history[sr] && history[sr] = 'manual'; - Bharata

33

这是mozilla开发者核心一个已经超过一年的问题报告。不幸的是,这个问题没有得到很好的解决。我认为Chrome也是相同的情况: 没有可靠的方法通过js处理onpopstate的滚动位置,因为它是本地浏览器行为。

但是,如果您查看HTML5历史规范,就会看到未来有希望,该规范明确希望在状态对象中表示滚动位置:

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

这些内容,以及上述mozilla票证上的评论,表明在不久的将来,至少对于使用pushState的人来说,可能无法再通过onpopstate恢复滚动位置。

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

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

取消滚动事件?

此外,如果滚动事件是可取消的,那就会更容易,但事实上它是不可取消的。如果可以取消第一个滚动事件(用户的滚动事件像旅鼠一样成群结队,而由历史重新定位引发的滚动事件只有一个),那么一切都好办了。

目前没有解决方案

据我所知,现在唯一建议的方法是等待HTML5规范完全实施,并且跟随浏览器的行为,也就是说:当浏览器可以滚动时,通过动画滚动;当有历史事件时,让浏览器重新定位页面。在位置方面,您唯一能影响的是在页面定位良好时使用pushState以便能回退至该位置。其他任何解决方案要么存在缺陷,要么太具体于某个浏览器,或者两者都有。


历史规范的措辞现在略有不同。此外,如果您向下滚动并查看pushState的文档,则没有提到添加滚动位置。规范建议会话历史记录条目可能包含保存的滚动位置,但我不确定这是否意味着Web开发人员将能够设置它。 - Walex

4

在这里,您需要使用某种可怕的浏览器嗅探技术。对于Firefox,我会采用您存储滚动位置并恢复它的解决方案。

根据您的描述,我认为我有一个基于Webkit的良好解决方案,但是我刚在Chrome 21中尝试了一下,似乎Chrome先滚动,然后触发popstate事件,然后再触发滚动事件。但是供参考,以下是我的解决方案:

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

黑魔法,如通过移动absolute定位元素来模拟页面滚动,由于屏幕重绘速度的限制而被排除。

因此,我99%确定答案是您无法实现,并且您将不得不使用问题中提到的其中一种妥协方案。两种浏览器在JavaScript知道有关它之前都会滚动,因此JavaScript只能在事件发生后做出反应。唯一的区别是Firefox直到Javascript触发后才绘制屏幕,这就是为什么Firefox有可行的解决方案,而WebKit没有的原因。


3
现在您可以做到:
history.scrollRestoration = 'manual';

这应该可以防止浏览器滚动。目前只有Chrome 46及以上版本支持,但似乎Firefox也计划支持。


2
解决方案是使用 position: fixed 并将 top 指定为页面滚动位置。以下是一个示例:
$(window).on('popstate', function()
{
   $('.yourWrapAroundAllContent').css({
      position: 'fixed',
      top: -window.scrollY
   });

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

是的,您会看到闪烁的滚动条,但这比其他问题要好一些。


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

1

将scrollRestoration设置为手动对我没有用,这是我的解决方案。

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

有传言说 document.body.scrollTop 已经被弃用了。这个可以用来代替: document.documentElement.scrollTop - Adam Jagosz

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会滚动到指定位置,但随后会重新定位回原始位置。


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中第一次返回时会闪烁)。欢迎提出任何改进建议!希望这有所帮助。


0
在页面顶部创建一个“span”元素,并在加载时将焦点设置为此元素。浏览器将滚动到聚焦的元素。我知道这是一种解决方法,而且在所有浏览器中都无法聚焦于“span”(嗯... Safari)。希望这可以帮助到您。

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