【问题标题】:How to get notified about changes of the history via history.pushState?如何通过 history.pushState 获得有关历史更改的通知?
【发布时间】:2011-06-01 23:44:42
【问题描述】:

既然 HTML5 引入了 history.pushState 来更改浏览器历史记录,网站开始将其与 Ajax 结合使用,而不是更改 URL 的片段标识符。

遗憾的是,这意味着onhashchange 无法再检测到这些呼叫。

我的问题是:是否有可靠的方法(黑客?;))来检测网站何时使用history.pushState?该规范没有说明引发的任何事件(至少我找不到任何东西)。
我尝试创建一个外观并用我自己的 JavaScript 对象替换 window.history,但它根本没有任何效果。

进一步说明:我正在开发一个 Firefox 插件,它需要检测这些变化并采取相应措施。
我知道几天前有一个类似的问题,询问听一些DOM events 是否有效,但我宁愿不依赖它,因为这些事件可能因很多不同的原因而产生。

更新:

Here is a jsfiddle(使用 Firefox 4 或 Chrome 8)显示在调用 pushState 时不会触发 onpopstate(或者我做错了什么?随时改进它!)。

更新 2:

另一个(侧面)问题是使用pushStatewindow.location 没有更新(但我认为我已经在此处阅读过这个)。

【问题讨论】:

    标签: javascript firefox-addon browser-history pushstate


    【解决方案1】:

    5.5.9.1 事件定义

    popstate 事件在某些情况下会在导航到会话历史记录条目时触发。

    据此,当您使用pushState 时,没有理由触发 popstate。但是像pushstate 这样的事件会派上用场。因为history 是一个主机对象,所以你应该小心它,但在这种情况下,Firefox 似乎很好。这段代码工作得很好:

    (function(history){
        var pushState = history.pushState;
        history.pushState = function(state) {
            if (typeof history.onpushstate == "function") {
                history.onpushstate({state: state});
            }
            // ... whatever else you want to do
            // maybe call onhashchange e.handler
            return pushState.apply(history, arguments);
        };
    })(window.history);
    

    你的 jsfiddle becomes

    window.onpopstate = history.onpushstate = function(e) { ... }
    

    你可以用同样的方法给window.history.replaceState打补丁。

    注意:当然你可以将onpushstate简单地添加到全局对象中,你甚至可以通过add/removeListener使其处理更多事件

    【讨论】:

    • 你说你替换了整个 history 对象。在这种情况下,这可能是不必要的。
    • @galambalazs:可能是的。也许(不知道)window.history 是只读的,但历史对象的属性不是......非常感谢这个解决方案:)
    • 根据规范有一个“pagetransition”事件,它似乎还没有实现。
    • @user280109 - 如果我知道,我会告诉你。 :) 我认为在 Opera atm 中没有办法做到这一点。
    • @cprcrack 用于保持全局范围清洁。您必须保存本机 pushState 方法以供以后使用。因此,我选择将整个代码封装到 IIFE 中,而不是全局变量:en.wikipedia.org/wiki/Immediately-invoked_function_expression
    【解决方案2】:

    终于找到了“正确”的方法!它需要为您的扩展程序添加权限并使用后台页面(不仅仅是内容脚本),但它确实有效。

    您想要的事件是browser.webNavigation.onHistoryStateUpdated,当页面使用history API 更改URL 时触发。它只针对您有权访问的网站触发,如果需要,您还可以使用 URL 过滤器进一步减少垃圾邮件。它需要webNavigation 权限(当然还有相关域的主机权限)。

    事件回调获取选项卡 ID、“导航”到的 URL 以及其他此类详细信息。如果您需要在事件触发时对该页面上的内容脚本执行操作,请直接从后台页面注入相关脚本,或者让内容脚本在加载时打开一个port 到后台页面,让后台页面将该端口保存在按标签 ID 索引的集合中,并在事件触发时通过相关端口(从后台脚本到内容脚本)发送消息。

    【讨论】:

    • 该 API 仅适用于 Web 扩展和附加组件,不是普通的 DOM API,很遗憾
    • 正确,但这就是 OP 根据问题试图构建的内容。对于纯网络的东西,我认为你别无选择,只能包装函数和/或事件。
    【解决方案3】:

    我使用简单的代理来做到这一点。这是原型的替代方法

    window.history.pushState = new Proxy(window.history.pushState, {
      apply: (target, thisArg, argArray) => {
        // trigger here what you need
        return target.apply(thisArg, argArray);
      },
    });
    

    【讨论】:

    • 这对我有用,只需将我的触发器添加到window.onpopstate 以覆盖用户按下后退按钮。
    • 这对我来说非常有效。 (我不需要支持 IE。)但是,请注意:这是 TypeScript。要转换为纯 JavaScript,您需要删除每个参数上的 : any。我还要注意 : any 在 TypeScript 中也是多余的。
    【解决方案4】:

    我以前用这个:

    var _wr = function(type) {
        var orig = history[type];
        return function() {
            var rv = orig.apply(this, arguments);
            var e = new Event(type);
            e.arguments = arguments;
            window.dispatchEvent(e);
            return rv;
        };
    };
    history.pushState = _wr('pushState'), history.replaceState = _wr('replaceState');
    
    window.addEventListener('replaceState', function(e) {
        console.warn('THEY DID IT AGAIN!');
    });
    

    这和galambalazs几乎一样。

    不过,这通常是矫枉过正。它可能不适用于所有浏览器。 (我只关心我的浏览器版本。)

    (它留下了一个 var _wr,所以你可能想要包装它什么的。我不在乎。)

    【讨论】:

    【解决方案5】:

    我宁愿不覆盖本机历史方法,所以这个简单的实现创建了我自己的名为 eventedPush state 的函数,它只是调度一个事件并返回 history.pushState()。无论哪种方式都可以正常工作,但我发现此实现更简洁,因为本机方法将继续按照未来开发人员的预期执行。

    function eventedPushState(state, title, url) {
        var pushChangeEvent = new CustomEvent("onpushstate", {
            detail: {
                state,
                title,
                url
            }
        });
        document.dispatchEvent(pushChangeEvent);
        return history.pushState(state, title, url);
    }
    
    document.addEventListener(
        "onpushstate",
        function(event) {
            console.log(event.detail);
        },
        false
    );
    
    eventedPushState({}, "", "new-slug"); 
    

    【讨论】:

      【解决方案6】:

      除了其他答案。我们可以使用 History 接口,而不是存储原始函数。

      history.pushState = function()
      {
          // ...
      
          History.prototype.pushState.apply(history, arguments);
      }
      

      【讨论】:

        【解决方案7】:

        您可以绑定到window.onpopstate 事件吗?

        https://developer.mozilla.org/en/DOM%3awindow.onpopstate

        来自文档:

        popstate 的事件处理程序 窗口上的事件。

        一个 popstate 事件被调度到 每次活动历史记录的窗口 条目变化。如果历史条目 被激活是由调用创建的 到 history.pushState() 或受到影响 通过调用 history.replaceState(), popstate 事件的 state 属性 包含历史条目的副本 状态对象。

        【讨论】:

        • 我已经尝试过了,只有当用户返回历史记录(或任何history.go.back)函数被调用时才会触发该事件。但不在pushState 上。这是我尝试对其进行测试的尝试,也许我做错了什么:jsfiddle.net/fkling/vV9vd 它似乎只与 if 历史被pushState 更改相关,相应的状态对象被传递给调用其他方法时的事件处理程序。
        • 啊。在这种情况下,我唯一能想到的就是注册一个超时来查看历史堆栈的长度,并在堆栈大小发生变化时触发一个事件。
        • 好吧,这是一个有趣的想法。唯一的问题是超时必须经常触发,以便用户不会注意到任何(长时间)延迟(我必须加载并显示新 URL 的数据)。我总是尽量避免超时和轮询,但直到现在这似乎是唯一的解决方案。我仍然会等待其他建议。但是现在非常感谢您!
        • 每次点击时检查历史记录棒的长度可能就足够了。
        • 是的 - 可以。我一直在玩这个 - 一个问题是 replaceState 调用不会引发事件,因为堆栈的大小不会改变。
        【解决方案8】:

        既然您询问的是 Firefox 插件,这里是我开始工作的代码。使用unsafeWindowno longer recommended,修改后从客户端脚本调用pushState时报错:

        访问属性history.pushState的权限被拒绝

        相反,有一个名为 exportFunction 的 API,它允许将函数注入到 window.history 中,如下所示:

        var pushState = history.pushState;
        
        function pushStateHack (state) {
            if (typeof history.onpushstate == "function") {
                history.onpushstate({state: state});
            }
        
            return pushState.apply(history, arguments);
        }
        
        history.onpushstate = function(state) {
            // callback here
        }
        
        exportFunction(pushStateHack, unsafeWindow.history, {defineAs: 'pushState', allowCallbacks: true});
        

        【讨论】:

        • 在最后一行,您应该将unsafeWindow.history, 更改为window.history。 unsafeWindow 对我不起作用。
        【解决方案9】:

        我认为即使可以修改本机函数也不是一个好主意,并且您应该始终保持应用程序范围,因此一个好的方法是不使用全局 pushState 函数,而是使用您自己的一个:

        function historyEventHandler(state){ 
            // your stuff here
        } 
        
        window.onpopstate = history.onpushstate = historyEventHandler
        
        function pushHistory(...args){
            history.pushState(...args)
            historyEventHandler(...args)
        }
        
        <button onclick="pushHistory(...)">Go to happy place</button>
        

        请注意,如果任何其他代码使用本机 pushState 函数,您将不会获得事件触发器(但如果发生这种情况,您应该检查您的代码)

        【讨论】:

          【解决方案10】:

          感谢@KalanjDjordjeDjordje his answer。我试图让他的想法成为一个完整的解决方案:

          const onChangeState = (state, title, url, isReplace) => { 
              // define your listener here ...
          }
          
          // set onChangeState() listener:
          ['pushState', 'replaceState'].forEach((changeState) => {
              // store original values under underscored keys (`window.history._pushState()` and `window.history._replaceState()`):
              window.history['_' + changeState] = window.history[changeState]
              
              window.history[changeState] = new Proxy(window.history[changeState], {
                  apply (target, thisArg, argList) {
                      const [state, title, url] = argList
                      onChangeState(state, title, url, changeState === 'replaceState')
                      
                      return target.apply(thisArg, argList)
                  },
              })
          })
          

          【讨论】:

            【解决方案11】:

            galambalazs's answer 猴子补丁 window.history.pushStatewindow.history.replaceState,但由于某种原因,它对我停止了工作。这是一个不太好的替代方案,因为它使用轮询:

            (function() {
                var previousState = window.history.state;
                setInterval(function() {
                    if (previousState !== window.history.state) {
                        previousState = window.history.state;
                        myCallback();
                    }
                }, 100);
            })();
            

            【讨论】:

            • 还有其他的投票方式吗?
            • @SuperUberDuper:查看其他答案。
            • @Flimm Polling 不好,但丑陋。但好吧,你没有选择你说的。
            • 这比猴子补丁要好得多,猴子补丁可以被其他脚本撤消,你不会收到任何通知。
            • 我不知道为什么这被否决了。这是一个非常可行的解决方案,并且效果很好。有时最简单的答案是最好的。
            【解决方案12】:

            好吧,我看到了很多替换 historypushState 属性的示例,但我不确定这是一个好主意,我更愿意使用与历史类似的 API 创建一个服务事件,这样你不仅可以控制推送状态,还可以控制替换状态,它为许多其他不依赖全局历史 API 的实现打开了大门。请检查以下示例:

            function HistoryAPI(history) {
                EventEmitter.call(this);
                this.history = history;
            }
            
            HistoryAPI.prototype = utils.inherits(EventEmitter.prototype);
            
            const prototype = {
                pushState: function(state, title, pathname){
                    this.emit('pushstate', state, title, pathname);
                    this.history.pushState(state, title, pathname);
                },
            
                replaceState: function(state, title, pathname){
                    this.emit('replacestate', state, title, pathname);
                    this.history.replaceState(state, title, pathname);
                }
            };
            
            Object.keys(prototype).forEach(key => {
                HistoryAPI.prototype = prototype[key];
            });
            

            如果您需要EventEmitter 定义,上面的代码基于NodeJS 事件发射器:https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/events.jsutils.inherits 的实现可以在这里找到:https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970

            【讨论】:

            • 我刚刚用要求的信息编辑了我的答案,请检查!
            • @VictorQueiroz 封装函数的好主意。但是如果某些第三方库调用 pushState,您的 HistoryApi 将不会收到通知。
            【解决方案13】:

            基于@gblazex 给出的解决方案,如果您想遵循相同的方法,但使用箭头函数,请在您的 javascript 逻辑中遵循以下示例:

            private _currentPath:string;    
            ((history) => {
                      //tracks "forward" navigation event
                      var pushState = history.pushState;
                      history.pushState =(state, key, path) => {
                          this._notifyNewUrl(path);
                          return pushState.apply(history,[state,key,path]); 
                      };
                    })(window.history);
            
            //tracks "back" navigation event
            window.addEventListener('popstate', (e)=> {
              this._onUrlChange();
            });
            

            然后,实现另一个函数_notifyUrl(url),它会在当前页面 url 更新时触发您可能需要的任何操作(即使页面根本没有加载)

              private _notifyNewUrl (key:string = window.location.pathname): void {
                this._path=key;
                // trigger whatever you need to do on url changes
                console.debug(`current query: ${this._path}`);
              }
            

            【讨论】:

              【解决方案14】:

              因为我只想要新的 URL,我已经修改了 @gblazex 和 @Alberto S. 的代码来获得这个:

              (function(history){
              
                var pushState = history.pushState;
                  history.pushState = function(state, key, path) {
                  if (typeof history.onpushstate == "function") {
                    history.onpushstate({state: state, path: path})
                  }
                  pushState.apply(history, arguments)
                }
                
                window.onpopstate = history.onpushstate = function(e) {
                  console.log(e.path)
                }
              
              })(window.history);
              

              【讨论】:

                【解决方案15】:

                我认为这个话题需要一个更现代的解决方案。

                我确定 nsIWebProgressListener 就在附近,但我很惊讶没有人提到它。

                来自框架脚本(为了 e10s 兼容性):

                let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
                webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW | Ci.nsIWebProgress.NOTIFY_LOCATION);
                

                然后在onLoacationChange收听

                onLocationChange: function onLocationChange(webProgress, request, locationURI, flags) {
                       if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
                

                这显然会捕获所有 pushState。但是有一条评论警告说它“也触发了 pushState”。所以我们需要在这里做更多的过滤,以确保它只是 pushstate 的东西。

                基于:https://github.com/jgraham/gecko/blob/55d8d9aa7311386ee2dabfccb481684c8920a527/toolkit/modules/addons/WebNavigation.jsm#L18

                还有:resource://gre/modules/WebNavigationContent.js

                【讨论】:

                • 这个答案与评论无关,除非我弄错了。 WebProgressListeners 用于 FF 扩展?这个问题是关于 HTML5 历史的
                • @Kloar 请删除这些 cmets
                【解决方案16】:

                按照标准规定:

                请注意,仅调用 history.pushState() 或 history.replaceState() 不会触发 popstate 事件。 popstate 事件仅通过执行浏览器操作触发,例如单击后退按钮(或在 JavaScript 中调用 history.back())

                我们需要调用 history.back() 来触发 WindowEventHandlers.onpopstate

                如此安装:

                history.pushState(...)
                

                做:

                history.pushState(...)
                history.pushState(...)
                history.back()
                

                【讨论】:

                  猜你喜欢
                  • 2015-12-10
                  • 1970-01-01
                  • 2021-05-03
                  • 2012-06-21
                  • 2019-07-10
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多