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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/4570093/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 13:00:38  来源:igfitidea点击:

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

javascriptfirefox-addonbrowser-historypushstate

提问by Felix Kling

So now that HTML5 introduces history.pushStateto change the browsers history, websites start using this in combination with Ajax instead of changing the fragment identifier of the URL.

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

Sadly that means that those calls cannot be detect anymore by onhashchange.

可悲的是,这意味着onhashchange.

My question is:Is there a reliable way (hack? ;)) to detect when a website uses history.pushState? The specification does not state anything about events that are raised (at least I couldn't find anything).
I tried to create a facade and replaced window.historywith my own JavaScript object, but it didn't have any effect at all.

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

Further explanation:I'm developing a Firefox add-on that needs to detect these changes and act accordingly.
I know there was a similar question a few days ago that asked whether listening to some DOM eventswould be efficient but I would rather not rely on that because these events can be generated for a lot of different reasons.

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

Update:

更新:

Here is a jsfiddle(use Firefox 4 or Chrome 8) that shows that onpopstateis not triggered when pushStateis called (or am I doing something wrong? Feel free to improve it!).

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

Update 2:

更新 2:

Another (side) problem is that window.locationis not updated when using pushState(but I read about this already here on SO I think).

另一个(侧面)问题是window.location使用时没有更新pushState(但我已经在此处阅读了相关内容,所以我认为)。

回答by gblazex

5.5.9.1 Event definitions

The popstateevent is fired in certain cases when navigating to a session history entry.

5.5.9.1 事件定义

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

According to this, there is no reason for popstate to be fired when you use pushState. But an event such as pushstatewould come in handy. Because historyis a host object, you should be careful with it, but Firefox seems to be nice in this case. This code works just fine:

据此,当您使用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);

Your jsfiddle becomes:

你的 jsfiddle变成了

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

You can monkey-patch window.history.replaceStatein the same way.

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

Note: of course you can add onpushstatesimply to the global object, and you can even make it handle more events via add/removeListener

注意:当然你可以onpushstate简单地添加到全局对象中,你甚至可以通过add/removeListener

回答by Rudie

I used to use this:

我曾经使用过这个:

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!');
});

It's almost the same as galambalazsdid.

这几乎和galambalazs做的一样。

It's usually overkill though. And it might not work in all browsers. (I only care about my version of my browser.)

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

(And it leaves a var _wr, so you might want to wrap it or something. I didn't care about that.)

(它留下一个 var _wr,所以你可能想要包装它或其他东西。我不在乎。)

回答by CBHacking

Finally found the "correct" way to do this! It requires adding a privilege to your extension and using the background page (not just a content script), but it does work.

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

The event you want is browser.webNavigation.onHistoryStateUpdated, which is fired when a page uses the historyAPI to change the URL. It only fires for sites that you have permission to access, and you can also use a URL filter to further cut down on the spam if you need to. It requires the webNavigationpermission (and of course host permission for the relevant domain(s)).

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

The event callback gets the tab ID, the URL that is being "navigated" to, and other such details. If you need to take an action in the content script on that page when the event fires, either inject the relevant script directly from the background page, or have the content script open a portto the background page when it loads, have the background page save that port in a collection indexed by tab ID, and send a message across the relevant port (from the background script to the content script) when the event fires.

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

回答by stef

You could bind to the window.onpopstateevent?

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

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

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

From the docs:

从文档:

An event handler for the popstate event on the window.

A popstate event is dispatched to the window every time the active history entry changes. If the history entry being activated was created by a call to history.pushState() or was affected by a call to history.replaceState(), the popstate event's state property contains a copy of the history entry's state object.

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

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

回答by nathancahill

Since you're asking about a Firefox addon, here's the code that I got to work. Using unsafeWindowis no longer recommended, and errors out when pushState is called from a client script after being modified:

既然你问的是 Firefox 插件,这里是我开始工作的代码。使用unsafeWindow不再推荐,并出现了错误的时候pushState的是被修改后,客户端脚本调用:

Permission denied to access property history.pushState

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

Instead, there's an API called exportFunctionwhich allows the function to be injected into window.historylike this:

相反,有一个名为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});

回答by Noitidart

I think this topic needs a more modern solution.

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

I'm sure nsIWebProgressListenerwas around back then I'm surprised no one mentioned it.

我肯定nsIWebProgressListener当时就在附近,我很惊讶没有人提到它。

From a framescript (for e10s compatability):

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

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

Then listening in the onLoacationChange

然后在听 onLoacationChange

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

That will apparently catch all pushState's. But there is a comment warning that it "ALSO triggers for pushState". So we need to do some more filtering here to ensure it's just pushstate stuff.

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

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

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

And: resource://gre/modules/WebNavigationContent.js

和:resource://gre/modules/WebNavigationContent.js

回答by jopfre

I'd rather not overwrite the native history method so this simple implementation creates my own function called eventedPush state which just dispatches an event and returns history.pushState(). Either way works fine but I find this implementation a bit cleaner as native methods will continue to perform as future developers expect.

我宁愿不覆盖本机历史记录方法,因此这个简单的实现创建了我自己的称为 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"); 

回答by Alberto S.

Based on the solution given by @gblazex, in case you want to follow the same approach, but using arrow functions, follow up the below example in your javascript logic:

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

Then, implement another function _notifyUrl(url)that triggers any required action you may need when the current page url is updated ( even if the page has not been loaded at all )

然后,实现另一个函数_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}`);
  }

回答by Flimm

galambalazs's answermonkey patches window.history.pushStateand window.history.replaceState, but for some reason it stopped working for me. Here's an alternative that's not as nice because it uses polling:

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

回答by Victor Queiroz

Well, I see many examples of replacing the pushStateproperty of historybut I'm not sure that's a good idea, I'd prefer to create a service event based with a similar API to history that way you can control not only push state but replace state as well and it open doors for many other implementations not relying on global history API. Please check the following example:

嗯,我看到了很多替换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];
});

If you need the EventEmitterdefinition, the code above is based on the NodeJS event emitter: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/events.js. utils.inheritsimplementation can be found here: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970

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