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


154

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

可悲的是,这意味着这些呼叫无法再由来检测onhashchange

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

进一步的说明:我正在开发一个Firefox插件,需要检测这些更改并采取相应措施。
我知道几天前有一个类似的问题,询问是否可以有效地侦听某些DOM事件,但我宁愿不依赖于此,因为可以出于许多不同的原因来生成这些事件。

更新:

这是一个jsfiddle(使用Firefox 4或Chrome 8),显示onpopstatepushState调用时不会触发(或者我做错了吗?随时进行改进!)。

更新2:

另一个(侧面)问题是window.location使用时未更新pushState(但我认为我已经在此处阅读过此信息)。

Answers:


194

5.5.9.1事件定义

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

据此,当您使用时,没有理由触发popstate pushState。但是这样的事件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 变为

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

您可以window.history.replaceState用相同的方式进行猴子补丁。

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


您说您替换了整个history对象。在这种情况下,这可能是不必要的。
gblazex 2011年

@galambalazs:可能是的。也许(不知道)window.history是只读的,但是历史对象的属性却不是...感谢这一解决方案:)
Felix Kling

根据规范,有一个“ pagetransition”事件,看来它尚未实现。
Mohamed Mansour

2
@ user280109-我会告诉你我是否知道。:)我认为Opera Atm无法做到这一点。
gblazex 2011年

1
@cprcrack用于保持全局作用域干净。您必须保存本机pushState方法供以后使用。因此,我选择将整个代码封装到IIFE中,而不是使用全局变量:en.wikipedia.org/wiki/Immediately-invoked_function_expression
gblazex

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,所以您可能要包装它或其他东西。我不在乎。)


4

终于找到了做到这一点的“正确”方法!它需要为您的扩展程序添加特权并使用后台页面(不仅是内容脚本),但它确实可以工作。

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

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


2

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

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

从文档:

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

每当活动历史记录条目更改时,都会将popstate事件调度到窗口。如果要激活的历史记录条目是通过调用history.pushState()创建的,或者受调用history.replaceState()的影响,则popstate事件的state属性包含历史记录条目的状态对象的副本。


6
我已经尝试过了,并且仅当用户返回历史记录(或任何history.go.back)函数时才触发该事件。但不是pushState。这是我尝试进行测试的方法,也许我做错了:jsfiddle.net/fkling/vV9vd似乎仅以这种方式相关:如果历史记录被更改pushState,则在其他方法时会将相应的状态对象传递给事件处理程序被称为。
菲利克斯·克林

1
啊。在那种情况下,我唯一想到的就是注册一个超时以查看历史堆栈的长度,并在堆栈大小已更改的情况下触发事件。
Stef 2011年

好的,这是一个有趣的想法。唯一的问题是超时必须经常触发,以使用户不会注意到任何(长)延迟(我必须加载并显示新URL的数据)。我总是尽量避免超时和轮询,但是直到现在,这似乎是唯一的解决方案。我仍将等待其他建议。但是,非常感谢您!
菲利克斯·克林

检查每次单击的历史记录棒的长度甚至足够。
菲利克斯·克林

1
是的-可以。我一直在玩这个游戏-一个问题是replaceState调用,该调用不会引发事件,因为堆栈的大小不会改变。
Stef 2011年

1

由于您正在询问Firefox插件,因此这是我必须使用的代码。使用unsafeWindow不再推荐,并出现了错误的时候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对我不起作用。
Lindsay-Needs-Sleep

0

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

有轮询的替代方法吗?
SuperUberDuper 2015年

@SuperUberDuper:查看其他答案。
Flimm

@Flimm Polling不好,但是很丑。但是,您没有选择的余地。
Yairopro

这比猴子修补好得多,猴子修补可以被其他脚本撤消,并且您不会收到任何通知。
伊万·卡斯泰拉诺斯

0

我认为该主题需要更现代的解决方案。

我确定那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。但是有一条评论警告它“ ALSO触发pushState”。因此,我们需要在此处进行更多过滤,以确保它只是pushstate的东西。

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

并且:resource://gre/modules/WebNavigationContent.js


除非我弄错了,否则这个答案与评论无关。WebProgressListener是否用于FF扩展?这个问题是关于HTML5的历史
Kloar

@Kloar请删除这些评论
Noitidart

0

好吧,我看到了很多替换的pushState属性的示例,history但是我不确定这是一个好主意,我更愿意创建一个与历史类似的基于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。
Yairopro

0

我宁愿不覆盖本机历史记录方法,因此此简单实现将创建我自己的名为eventedPush状态的函数,该函数仅调度一个事件并返回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

基于@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}`);
  }

0

由于我只想要新的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);

0

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


-5

作为标准状态:

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

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

因此:

history.pushState(...)

做:

history.pushState(...)
history.pushState(...)
history.back()
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.