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

拟墨画扇 提交于 2019-12-27 10:19:52

问题


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

Sadly that means that those calls cannot be detect anymore by 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.history with my own JavaScript object, but it didn't have any effect at all.

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 events would be efficient but I would rather not rely on that because these events can be generated for a lot of different reasons.

Update:

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

Update 2:

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


回答1:


5.5.9.1 Event definitions

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

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

(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:

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

You can monkey-patch window.history.replaceState in the same way.

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




回答2:


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 galambalazs did.

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.)




回答3:


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 history API 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 webNavigation permission (and of course host permission for the relevant domain(s)).

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 port to 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.




回答4:


You could bind to the window.onpopstate event?

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.




回答5:


galambalazs's answer monkey patches window.history.pushState and 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:

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



回答6:


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

Permission denied to access property history.pushState

Instead, there's an API called exportFunction which allows the function to be injected into window.history like this:

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



回答7:


I think this topic needs a more modern solution.

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

From a framescript (for e10s compatability):

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

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.

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

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




回答8:


Well, I see many examples of replacing the pushState property of history but 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:

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 EventEmitter definition, the code above is based on the NodeJS event emitter: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/events.js. utils.inherits implementation can be found here: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970




回答9:


As standard states:

Note that just calling history.pushState() or history.replaceState() won't trigger a popstate event. The popstate event is only triggered by doing a browser action such as clicking on the back button (or calling history.back() in JavaScript)

we need to call history.back() to trigeer WindowEventHandlers.onpopstate

So insted of:

history.pushState(...)

do:

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


来源:https://stackoverflow.com/questions/4570093/how-to-get-notified-about-changes-of-the-history-via-history-pushstate

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!