190

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

可悲的是,这意味着这些呼叫无法再被onhashchange.

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

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

更新:

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

更新 2:

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

4

16 回答 16

229

5.5.9.1 事件定义

在导航到会话历史条目时,在某些情况下会触发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

于 2011-01-03T13:59:56.747 回答
12

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

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

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

于 2019-01-16T01:34:10.363 回答
10

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

window.history.pushState = new Proxy(window.history.pushState, {
  apply: (target, thisArg, argArray) => {
    // trigger here what you need
    return target.apply(thisArg, argArray);
  },
});
于 2020-11-20T10:17:56.950 回答
5

我以前用这个:

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

于 2014-09-04T19:54:18.890 回答
2

我宁愿不覆盖本机历史方法,所以这个简单的实现创建了我自己的名为 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"); 
于 2020-01-28T10:13:24.203 回答
2

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

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

    History.prototype.pushState.apply(history, arguments);
}
于 2020-08-22T06:55:31.080 回答
2

感谢@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)
        },
    })
})
于 2021-07-17T07:54:15.400 回答
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});
于 2015-08-27T21:34:05.720 回答
1

好吧,我看到了很多替换的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

于 2017-11-04T13:14:58.487 回答
1

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

于 2020-06-30T13:46:04.617 回答
0

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

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

从文档:

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

每次活动历史条目更改时,都会向窗口发送一个 popstate 事件。如果被激活的历史条目是由调用 history.pushState() 创建的或受到调用 history.replaceState() 的影响,则 popstate 事件的 state 属性包含历史条目的 state 对象的副本。

于 2011-01-02T23:18:10.397 回答
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);
})();
于 2015-06-03T13:34:34.993 回答
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}`);
  }
于 2020-03-26T16:10:53.653 回答
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);
于 2020-06-22T01:36:38.600 回答
-1

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

我确定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

于 2015-10-15T01:06:57.170 回答
-6

正如标准所述:

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

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

所以安装:

history.pushState(...)

做:

history.pushState(...)
history.pushState(...)
history.back()
于 2017-02-10T10:42:02.143 回答