如何通过history.pushState获得有关历史记录更改的通知?

How to get notified about changes of the history via history.pushState?

提问人:Felix Kling 提问时间:12/31/2010 最后编辑:Zsolt MeszarosFelix Kling 更新时间:2/3/2022 访问量:150731

问:

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

可悲的是,这意味着这些调用无法再被 检测到。onhashchange

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

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

更新:

这是一个 jsfiddle(使用 Firefox 4 或 Chrome 8),它显示调用时没有触发(或者我做错了什么?随意改进它!onpopstatepushState

更新2:

另一个(侧面)问题是使用时没有更新(但我认为我已经在这里读到了这一点)。window.locationpushState

javascript firefox-addon browser-history pushstate

评论

0赞 SuperStormer 3/11/2022
相关新闻: 如何检测 YouTube 上的页面导航并无缝修改其外观?

答:

-1赞 stef 1/3/2011 #1

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

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

从文档中:

popstate 的事件处理程序 事件。

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

评论

10赞 Felix Kling 1/3/2011
我已经尝试过了,只有当用户返回历史记录(或调用任何 , )函数时才会触发该事件。但不是.这是我测试它的尝试,也许我做错了什么: jsfiddle.net/fkling/vV9vd 似乎只有这样才能与此相关,如果历史记录被更改,则在调用其他方法时,相应的状态对象将传递给事件处理程序。history.go.backpushStatepushState
2赞 stef 1/3/2011
啊。在这种情况下,我唯一能想到的就是注册一个超时来查看历史堆栈的长度,并在堆栈大小发生变化时触发事件。
1赞 Felix Kling 1/3/2011
好的,这是一个有趣的想法。唯一的问题是超时必须足够频繁地触发,以便用户不会注意到任何(长时间)延迟(我必须加载并显示新 URL 的数据)。我总是尽可能避免超时和轮询,但直到现在,这似乎是唯一的解决方案。我仍然会等待其他提案。但现在非常感谢你!
0赞 Felix Kling 1/3/2011
在每次点击时检查历史棒的长度可能就足够了。
1赞 stef 1/3/2011
是的 - 那会起作用。我一直在玩这个 - 一个问题是 replaceState 调用,它不会抛出事件,因为堆栈的大小不会改变。
247赞 gblazex 1/3/2011 #2

5.5.9.1 事件定义

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

据此,当您使用 .但是这样的活动会派上用场。因为是一个主机对象,你应该小心它,但在这种情况下,Firefox似乎很好。这段代码工作得很好:pushStatepushstatehistory

(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 变成

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

你可以用同样的方式进行猴子修补。window.history.replaceState

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

评论

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

我曾经用过这个:

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

评论

1赞 ggorlen 11/30/2020
这个答案几乎相同
1赞 Flimm 6/3/2015 #4

Galambalazs 的答案是猴子补丁和,但由于某种原因它不再为我工作。这是一个不太好的替代方案,因为它使用轮询:window.history.pushStatewindow.history.replaceState

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

评论

0赞 SuperUberDuper 7/12/2015
有没有投票的替代方案?
0赞 Flimm 7/13/2015
@SuperUberDuper:查看其他答案。
1赞 Yairopro 7/15/2018
@Flimm 投票不是很好,而是丑陋。但是,好吧,你说你别无选择。
0赞 Ivan Castellanos 11/26/2018
这比猴子修补要好得多,猴子修补可以被其他脚本撤消,并且您不会收到任何通知。
0赞 Mark Shust at M.academy 7/16/2021
我不知道为什么这被否决了。这是一个非常可行的解决方案,并且效果很好。有时最简单的答案是最好的。
1赞 nathancahill 8/28/2015 #5

既然你问的是Firefox插件,这里是我开始工作的代码。不再建议使用 ,并且在修改后从客户端脚本调用 pushState 时出错:unsafeWindow

访问属性 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});

评论

0赞 Lindsay-Needs-Sleep 4/19/2020
在最后一行,应更改为 .unsafeWindow对我不起作用。unsafeWindow.history,window.history
-1赞 Noitidart 10/15/2015 #6

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

我敢肯定当时就在附近,我很惊讶没有人提到它。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”。因此,我们需要在这里进行更多的过滤,以确保它只是推送状态的东西。

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

以及:resource://gre/modules/WebNavigationContent.js

评论

0赞 Kloar 3/26/2016
这个答案与评论无关,除非我弄错了。WebProgressListeners 是用于 FF 扩展的吗?这个问题是关于HTML5历史的
0赞 Noitidart 3/26/2016
@Kloar让我们删除这些评论
-7赞 user2360102 2/10/2017 #7

正如标准所述:

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

我们需要调用 history.back() 到 trigeer WindowEventHandlers.onpopstate

因此,而不是:

history.pushState(...)

做:

history.pushState(...)
history.pushState(...)
history.back()
1赞 Victor Queiroz 11/4/2017 #8

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

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

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

评论

0赞 Victor Queiroz 11/10/2017
我刚刚用要求的信息编辑了我的答案,请检查!
0赞 Yairopro 7/15/2018
@VictorQueiroz 封装函数是一个漂亮而干净的主意。但是,如果某些第三方库调用 pushState,则不会通知 HistoryApi。
17赞 CBHacking 1/16/2019 #9

终于找到了“正确”(没有猴子补丁,没有破坏其他代码的风险)的方法!它需要为您的扩展添加权限(是的,在评论中帮助指出这一点的人,这是针对扩展 API 的),这是所要求的)并使用后台页面(不仅仅是内容脚本),但它确实有效。

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

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

评论

24赞 ccnokes 1/6/2021
不幸的是,该 API 仅适用于 Web 扩展和附加组件,而不是普通的 DOM API
5赞 CBHacking 1/6/2021
没错,但这就是 OP 试图根据问题构建的内容。对于纯粹的 Web 内容,我认为您别无选择,只能包装函数和/或事件。
3赞 jopfre 1/28/2020 #10

我宁愿不覆盖本机历史记录方法,因此这个简单的实现创建了我自己的名为 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"); 
0赞 Alberto S. 3/27/2020 #11

根据 @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();
});

然后,实现另一个函数,该函数在更新当前页面 url 时触发您可能需要的任何所需操作(即使页面根本没有加载)_notifyUrl(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}`);
  }
0赞 Andre Lopes 6/22/2020 #12

由于我只是想要新的 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);
1赞 Maxwell s.c 6/30/2020 #13

我认为即使可以修改本机函数也不是一个好主意,并且您应该始终保留您的应用程序范围,因此一个好的方法是不使用全局 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 函数,您将不会获得事件触发器(但如果发生这种情况,您应该检查您的代码)

6赞 Doliman100 8/22/2020 #14

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

history.pushState = function()
{
    // ...

    History.prototype.pushState.apply(history, arguments);
}
41赞 Kalanj Djordje Djordje 11/20/2020 #15

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

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

评论

0赞 Avindra Goolcharan 8/2/2021
这对我有用,只需要添加我的触发器来覆盖用户按下后退按钮。window.onpopstate
4赞 trlkly 12/19/2021
这对我来说非常有效。(我不需要支持 IE。但是,请注意:这是 TypeScript。要转换为纯 JavaScript,您需要删除每个参数。我还要指出,这在 TypeScript 中也是多余的。: any: any
4赞 trlkly 3/19/2022
从那以后,我发现这是一个细微的变化。如上所述,“触发”代码在更改页面历史记录之前触发。这使得测试新 URL 变得困难。从那以后,我更改了 apply 中的代码:let output = target.apply(thisArg, argArray); /* trigger code */ return output;
7赞 Mir-Ismaili 7/17/2021 #16

谢谢@KalanjDjordjeDjordje的回答。我试着把他的想法变成一个完整的解决方案:

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

评论

0赞 Mattwmaster58 5/29/2022
这个+对我的用户脚本很有效。谢谢!@run-at document-start