【问题标题】:Change URL hash on scroll and keep back button working在滚动时更改 URL 哈希并保持返回按钮正常工作
【发布时间】:2026-02-05 00:00:02
【问题描述】:

在带有固定顶部菜单和锚导航的单页布局上,我有一个“scrollspy”,它可以更改滚动上的片段标识符,根据滚动位置为菜单链接提供一个活动类,并使用动画滚动到锚点速度.js。​​

不幸的是,当点击浏览器后退按钮时,它会带我完成滚动方式的所有步骤,这意味着我加载网站并向下和向上滚动一点点,然后经常点击后退按钮,浏览器会也可以向下和向上滚动,但实际上不会转到上次访问的 id 或返回浏览器历史记录。

Here is the jsfiddle.

// jQuery on DOM ready

// In-Page Scroll Animation with VelocityJS
// ------------------------------------------------ //
// https://john-dugan.com/fixed-headers-with-hash-links/
$('.menu-a').on('click', function(e) {
    var hash  = this.hash,
        $hash = $(hash)

        addHash = function() {
            window.location.hash = hash;
        };      

      $hash.velocity("scroll", { duration: 700, easing: [ .4, .21, .35, 1 ], complete: addHash });

    e.preventDefault();
});

// ScrollSpy for Menu items and Fragment Identifier
// ------------------------------------------------ //
// https://jsfiddle.net/mekwall/up4nu/
$menuLink           = $('.menu-a')

var lastId,
    // Anchors corresponding to menu items
    scrollItems = $menuLink.map(function(){
    var item = $($(this).attr("href"));
        if (item.length) { return item; }
    });


$(window).scroll(function(){
    // Get container scroll position
    var fromTop = $(this).scrollTop()+ 30; // or the value for the #navigation height

    // Get id of current scroll item
    var cur = scrollItems.map(function(){
        if ($(this).offset().top < fromTop)
        return this;
    });

    // Get the id of the current element
    cur = cur[cur.length-1];
    var id = cur && cur.length ? cur[0].id : "";
    if (lastId !== id) {
        lastId = id;

        // Set/remove active class
        $menuLink
        .parent().removeClass("active")
        .end().filter("[href='#"+id+"']").parent().addClass("active");
    }

    // If supported by the browser we can also update the URL
    // http://codepen.io/grayghostvisuals/pen/EtdwL
    if (window.history && window.history.pushState) {
        history.pushState("", document.title,'#'+id);
    }   
});

使用上面的代码可以正常工作:

  • 使用 VelocityJS 的滚动动画单击菜单链接时,哈希或片段标识符更新正常。

  • 活动类在滚动时被赋予相应的菜单链接。

  • 当滚动而不是单击菜单链接时,片段标识符也能正常更新。

问题
第 1 部分:当您在小提琴上滚动一点点然后点击后退按钮时,您会看到滚动条“移动”的正是以同样的方式,记住已完成的滚动。

我需要后退按钮才能正常工作。 a)返回浏览器历史记录并返回您所在的页面/站点,然后 b) 当点击一个锚链接 (i) 然后是锚链接 (ii) 和后退按钮时,页面应该返回到锚链接 (i)。

第 2 部分: 由于 IE8 不支持 history.pushState,因此我正在寻找一种使用 window.location.hash = $(this).attr('id'); 的方法。无论我在代码末尾尝试了什么,我都无法让window.location.hash = $(this).attr('id'); 工作。我真的不想为此使用 HistoryJS 或其他东西,但有兴趣学习并自己编写它。

除了后退按钮损坏的行为之外,我想要的所有其他行为都已经存在,现在我只需要修复后退按钮的行为。

编辑 我想我可能已经找到了解决方案here,如果我能解决这个问题,我会进行测试并详细回复。

相关:
smooth scrolling and get back button with popState on Firefox - need to click twice
jQuery in page back button scrolling
Modifying document.location.hash without page scrolling

How to Detect Browser Back Button event - Cross Browser

【问题讨论】:

    标签: javascript jquery html url back


    【解决方案1】:

    回答你问题的第一部分,如果你不想污染浏览器的历史,你可以使用history.replaceState()而不是history.pushState()pushState 更改 URL 并在浏览器历史记录中添加新条目,replaceState 在修改当前历史记录条目而不是添加新条目时更改 URL。

    还有一篇很好的文章,包括pushStatereplaceStateon MDN之间的区别。

    【讨论】:

    • ..并且因为 history.pushStatewindow.scroll 函数中,它将通过滚动事件触发的每个新滚动位置推送到浏览器历史记录。我设法用另一种方法解决了这个问题,但现在我知道为什么会发生这种情况。现在有道理了。仍在研究,但完成后会提出我的解决方案。
    【解决方案2】:

    对于较旧的浏览器,我决定包含https://github.com/devote/HTML5-History-API,并且有了它,我得到了所需的行为(或多或少)。

    这个答案有:
    - 菜单项和集合的滚动间谍以及滚动上的活动类
    - 滚动间谍也适用于 URL 哈希,根据当前滚动到的部分设置正确的哈希
    - 滚动停止函数,检查用户何时停止滚动,然后从当前活动的菜单项中获取值并将其设置为当前 URL 哈希。这样做的目的是为了在滚动时不捕捉部分的锚点,而只捕捉用户滚动到的部分的锚点。
    - 单击菜单链接以及使用后退和前进按钮时使用 Velocity.js 平滑滚动
    - 对加载和重新加载页面做出反应的函数,这意味着如果您输入带有特定 URL 散列的页面的某个部分,它将动画滚动到该部分,如果重新加载页面,它将动画滚动到当前的顶部部分

    代码是一个粗略的草图,可能会进行一些调整,这仅用于演示目的。我想我还是个初学者,指出明显的错误,以便我从中吸取教训。还包括所有指向代码 sn-ps 来源的链接。

    // In-Page Scroll Animation to Menu Link on click
    // ------------------------------------------------ //
    // https://john-dugan.com/fixed-headers-with-hash-links/
    // https://*.com/questions/8355673/jquery-how-to-scroll-an-anchor-to-the-top-of-the-page-when-clicked
    // http://*.com/a/8579673/1010918
    // $('a[href^="#"]').on('click', function(e) {
    $('.menu-a').on('click', function(e) {
    
        // default = make hash appear right after click
        // not default = make hash appear after scrolling animation is finished
        e.preventDefault();
    
        var hash  = this.hash,
            $hash = $(hash)
    
        $hash.velocity("scroll", { duration: 700, easing: [ .4, .21, .35, 1 ], queue: false });
    });
    
    
    
    // In-Page Scroll Animation to Hash on Load and Reload
    // ----------------------------------------------------------- //
    // https://*.com/questions/680785/on-window-location-hash-change
    // hashchange triggers popstate !
    $(window).on('load', function(e) {
    
        var hash  = window.location.hash;
        console.log('hash on window load '+hash);
        $hash = $(hash)
    
        $hash.velocity("scroll", { duration: 500, easing: [ .4, .21, .35, 1 ], queue: false });
    
        // if not URL hash is present = root, go to top of page on reload
        if (!window.location.hash){
            $('body').velocity("scroll", { duration: 500, easing: [ .4, .21, .35, 1 ], queue: false });
        }   
    });
    
    
    
    // In-Page Scroll Animation to Hash on Back or Forward Button
    // ---------------------------------------------------------- //
    $('.menu-a').on('click', function(e) {  
        e.preventDefault(); 
        // keep the link in the browser history
        // set this separately from scrolling animation so it works in IE8
        history.pushState(null, null, this.href);
        return false
    }); 
    $(window).on('popstate', function(e) {
        // alert('popstate fired');
        $('body').append('<div class="console1">popstate fired</div>');
        $('.console1').delay(1000).fadeOut('slow');
    
        if (!window.location.hash){
            $('body').append('<div class="console2">no window location hash present</div>');
    
            $('body').velocity("scroll", { duration: 700, easing: [ .4, .21, .35, 1 ], queue: false });
    
            $('.console2').delay(1000).fadeOut('slow');
        }
    
        console.log('window.location.hash = '+window.location.hash);
        var hash  = window.location.hash;
        $hash = $(hash)
    
        $hash.velocity("scroll", { duration: 700, easing: [ .4, .21, .35, 1 ], queue: false });
    });
    
    
    
    // ScrollSpy for Menu items only - gives selected Menu items the active class
    // ------------------------------------------------------------------------ //
    // Does not update fragment identifier in URL https://en.wikipedia.org/wiki/Fragment_identifier
    // https://jsfiddle.net/mekwall/up4nu/
        var lastId,
    
        // Anchors corresponding to menu items
        scrollItems = $menuLink.map(function(){
            var item = $($(this).attr("href"));
            // console.table(item);
            if (item.length) { return item; }
        });
    
        // Give menu item the active class on load of corresponding item
        function scrollSpy () {
    
            // Get container scroll position
            var fromTop = $(this).scrollTop()+ $menuButtonHeight;
    
            // Get id of current scroll item
            var cur = scrollItems.map(function(){
                if ($(this).offset().top < fromTop)
                return this;
            });
    
            // Get the id of the current element
            cur = cur[cur.length - 1];
            var id = cur && cur.length ? cur[0].id : "";
    
            if (lastId !== id) {
                lastId = id;
                // Set/remove active class
                $menuLink
                .parent().removeClass("active").end()
                .filter("[href='#"+id+"']").parent().addClass("active");
            }
    
            // Active Menu Link href Attribute
            activeMenuLinkHref = $('.menu-li.active > .menu-a').attr('href');
            // console.log('activeMenuLinkHref '+activeMenuLinkHref);   
        }
        scrollSpy()
    
        $(window).scroll(function(e){
            scrollSpy()
        });
    
    
    
    // On Stop of Scrolling get Active Menu Link Href and set window.location.hash
    // --------------------------------------------------------------------------- //
    
    // Scroll Stop Function
    //---------------------//
    // https://*.com/questions/8931605/fire-event-after-scrollling-scrollbars-or-mousewheel-with-javascript
    // http://*.com/a/8931685/1010918
    // http://jsfiddle.net/fbSbT/1/
    // http://jsfiddle.net/fbSbT/87/
    
    (function(){ 
        var special = jQuery.event.special,
            uid1 = 'D' + (+new Date()),
            uid2 = 'D' + (+new Date() + 1); 
        special.scrollstart = {
            setup: function() { 
                var timer,
                    handler =  function(evt) { 
                        var _self = this,
                            _args = arguments; 
                        if (timer) {
                            clearTimeout(timer);
                        } else {
                            evt.type = 'scrollstart';
                            // throws "TypeError: jQuery.event.handle is undefined"
                            // jQuery.event.handle.apply(_self, _args);
                            // solution
                            // http://*.com/a/20809936/1010918
                            // replace jQuery.event.handle.apply with jQuery.event.dispatch.apply
                            jQuery.event.dispatch.apply(_self, _args);
                        } 
                        timer = setTimeout( function(){
                            timer = null;
                        }, special.scrollstop.latency); 
                    }; 
                jQuery(this).bind('scroll', handler).data(uid1, handler); 
            },
            teardown: function(){
                jQuery(this).unbind( 'scroll', jQuery(this).data(uid1) );
            }
        }; 
        special.scrollstop = {
            latency: 250,
            setup: function() { 
                var timer,
                        handler = function(evt) { 
                        var _self = this,
                            _args = arguments; 
                        if (timer) {
                            clearTimeout(timer);
                        }
                         timer = setTimeout( function(){ 
                            timer = null;
                            evt.type = 'scrollstop';                        
                            // throws "TypeError: jQuery.event.handle is undefined"
                            // jQuery.event.handle.apply(_self, _args);
                            // solution
                            // http://*.com/a/20809936/1010918
                            // replace jQuery.event.handle.apply with jQuery.event.dispatch.apply
                            jQuery.event.dispatch.apply(_self, _args); 
                        }, special.scrollstop.latency); 
                    }; 
                jQuery(this).bind('scroll', handler).data(uid2, handler); 
            },
            teardown: function() {
                jQuery(this).unbind( 'scroll', jQuery(this).data(uid2) );
            }
        };
    
    })();
    
    
    
    // Scroll Stop Function Called
    //----------------------------//
    
    $(window).on("scrollstop", function() {
    
        // window.history.pushState(null, null, activeMenuLinkHref);
        // window.history.replaceState(null, null, activeMenuLinkHref);
    
        // http://*.com/a/1489802/1010918 //
        // works best really
        hash = activeMenuLinkHref.replace( /^#/, '' );
        console.log('hash '+hash);
        var node = $( '#' + hash );
        if ( node.length ) {
          node.attr( 'id', '' );
          // console.log('node.attr id'+node.attr( 'id', '' ));
        }
        document.location.hash = hash;
        if ( node.length ) {
          node.attr( 'id', hash );
        }
    });
    

    CSS

    .console1{
        position: fixed;
        z-index: 9999;
        top:0;
        right:0;    
        background-color: #fff;
        border: 2px solid red;
    }
    
    .console2{
        position: fixed;
        z-index: 9999;
        bottom:0;
        right:0;    
        background-color: #fff;
        border: 2px solid red;
    }
    

    我也会在适当的时候提供一个 jsfiddle。 ;)

    【讨论】: