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

时间:2010-12-31 12:31:43

标签: javascript firefox-addon browser-history pushstate

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

可悲的是,这意味着onhashchange无法再检测到这些来电。

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

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

更新

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

更新2:

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

16 个答案:

答案 0 :(得分:157)

  

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

使其处理更多事件

答案 1 :(得分:3)

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

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

来自文档:

  

popstate的事件处理程序   窗户上的活动。

     

将popstate事件分派给   窗口每次活动历史记录   进入变化。如果是历史记录   被激活是由一个电话创建的   到history.pushState()或受到影响   通过调用history.replaceState(),   popstate事件的州财产   包含历史记录条目的副本   国家对象。

答案 2 :(得分: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,所以你可能想要把它包裹起来。我不关心它。)

答案 3 :(得分:1)

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

我确信nsIWebProgressListener已经回来了,然后我很惊讶没有人提到它。

从framecript(对于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

并且:资源://gre/modules/WebNavigationContent.js

答案 4 :(得分:1)

除了其他答案。我们可以使用“历史记录”界面来代替存储原始功能。

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

    History.prototype.pushState.apply(history, arguments);
}

答案 5 :(得分:1)

感谢@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)
        },
    })
})

答案 6 :(得分:0)

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

答案 7 :(得分:0)

既然你在询问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});

答案 8 :(得分: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.js。可以在此处找到utils.inherits实施:https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970

答案 9 :(得分:0)

最后找到了执行此操作的“正确”方法!它需要向您的扩展程序添加特权并使用后台页面(不仅是内容脚本),但确实可以。

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

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

答案 10 :(得分: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"); 

答案 11 :(得分: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}`);
  }

答案 12 :(得分: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);

答案 13 :(得分: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函数,则不会获得事件触发器(但是,如果发生这种情况,则应检查代码)

答案 14 :(得分:0)

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

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

答案 15 :(得分:-3)

作为标准状态:

  

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

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

如此绝对:

history.pushState(...)

做的:

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