web-dev-qa-db-fra.com

Comment être averti des changements de l'historique via history.pushState?

Alors maintenant que HTML5 introduit history.pushState pour changer l'historique des navigateurs, les sites Web commencent à utiliser ceci en combinaison avec Ajax au lieu de changer l'identifiant de fragment de l'URL.

Malheureusement, cela signifie que ces appels ne peuvent plus être détectés par onhashchange.

Ma question est la suivante: Existe-t-il un moyen fiable (piratage?;)) De détecter quand un site Web utilise history.pushState? La spécification ne dit rien sur les événements déclenchés (au moins, je n'ai rien trouvé).
J'ai essayé de créer une façade et j'ai remplacé window.history par mon propre objet JavaScript, mais cela n'a eu aucun effet.

Explication complémentaire: Je développe un module complémentaire Firefox qui doit détecter ces modifications et agir en conséquence.
Je sais qu’il y avait quelques jours, une question similaire demandait si l’écoute de certains événements DOM serait efficace, mais je préférerais ne pas compter sur cela, car ces événements peuvent être générés pour différentes raisons.

Mettre à jour:

Voici un jsfiddle (utilisez Firefox 4 ou Chrome 8) qui indique que onpopstate n'est pas déclenché lorsque pushState est appelé (ou est-ce que je fais quelque chose de mal? N'hésitez pas à l'améliorer!).

Mise à jour 2:

Un autre problème (latéral) est que window.location n’est pas mis à jour lors de l’utilisation de pushState (mais j’ai déjà lu à ce sujet ici SO je pense).

121
Felix Kling

5.5.9.1 Définitions d'événements

L'événement popstate est déclenché dans certains cas lors de la navigation vers une entrée d'historique de session.

Selon cela, il n'y a aucune raison pour que Popstate soit renvoyé lorsque vous utilisez pushState. Mais un événement tel que pushstate serait utile. Étant donné que history est un objet hôte, vous devez être prudent, mais Firefox semble être agréable dans ce cas. Ce code fonctionne très bien:

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

Votre jsfiddle devient:

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

Vous pouvez monkey-patch window.history.replaceState de la même manière.

Remarque: vous pouvez bien sûr ajouter onpushstate simplement à l'objet global, et vous pouvez même le faire gérer davantage d'événements via add/removeListener

147
gblazex

Vous pouvez vous lier à l'événement window.onpopstate?

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

De la docs:

Un gestionnaire d'événement pour le popstate événement sur la fenêtre.

Un événement popstate est envoyé au fenêtre à chaque fois que l'historique actif changements d'entrée. Si l'entrée dans l'historique en cours d'activation a été créé par un appel à history.pushState () ou a été affecté par un appel à history.replaceState (), la propriété state de l'événement popstate contient une copie de l'entrée de l'historique objet d'état.

3
stef

J'avais l'habitude d'utiliser ceci:

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

C'est presque la même chose que galambalazs l'a fait.

Cependant, c'est généralement excessif. Et cela pourrait ne pas fonctionner dans tous les navigateurs. (Je ne me soucie que de ma version de mon navigateur.)

(Et cela laisse un var _wr, alors vous voudrez peut-être l'envelopper ou quelque chose du genre. Je m'en fichais.)

2
Rudie

Je pense que ce sujet nécessite une solution plus moderne.

Je suis sûr que nsIWebProgressListener était à l'époque, et je suis surpris que personne ne l'ait mentionné.

A partir d'un framescript (pour la compatibilité e10s):

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

Puis écoute dans la onLoacationChange

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

Cela va apparemment attraper tous les pushState. Mais il y a un commentaire qui avertit qu'il "ALLE se déclenche AUSSI pour pushState". Nous devons donc faire un peu plus de filtrage ici pour nous assurer que ce ne sont que des éléments poussés.

Basé sur: https://github.com/jgraham/gecko/blob/55d8d9aa7311386ee2dabfccb481684c8920a527/toolkit/modules/addons/WebNavigation.jsm#L18

Et: ressource: //gre/modules/WebNavigationContent.js

1
Noitidart

Eh bien, je vois de nombreux exemples de remplacement de la propriété pushState de history mais je ne suis pas sûr que ce soit une bonne idée. Je préférerais créer un événement de service basé sur une API similaire à l'historique de manière à pouvoir contrôler non seulement l'état Push, mais aussi remplace également l'état et ouvre la porte à de nombreuses autres implémentations ne reposant pas sur une API d'historique globale. Veuillez vérifier l'exemple suivant:

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

Si vous avez besoin de la définition EventEmitter, le code ci-dessus est basé sur l'émetteur de l'événement NodeJS: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/events.js . utils.inherits implémentation peut être trouvée ici: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970

0
Victor Queiroz

Enfin trouvé la "bonne" façon de faire ça! Cela nécessite l'ajout d'un privilège à votre extension et l'utilisation de la page d'arrière-plan (pas seulement un script de contenu), mais cela fonctionne.

L'événement souhaité est browser.webNavigation.onHistoryStateUpdated , qui est déclenché lorsqu'une page utilise l'API history pour modifier l'URL. Il ne se déclenche que pour les sites auxquels vous êtes autorisé à accéder. Vous pouvez également utiliser un filtre d'URL pour réduire davantage le courrier indésirable, le cas échéant. Il nécessite l’autorisation webNavigation (et bien sûr l’autorisation de l’hôte pour le ou les domaines pertinents).

Le rappel d'événement obtient l'ID de l'onglet, l'URL en cours de "navigation" et d'autres détails de ce type. Si vous devez effectuer une action dans le script de contenu de cette page lorsque l'événement est déclenché, injectez le script approprié directement à partir de la page d'arrière-plan ou demandez au script de contenu d'ouvrir un port à la page d'arrière-plan lors de son chargement. enregistrez ce port dans une collection indexée par un ID d'onglet et envoyez un message sur le port approprié (du script d'arrière-plan au script de contenu) lors du déclenchement de l'événement.

0
CBHacking

Puisque vous parlez d'un addon pour Firefox, voici le code sur lequel je dois travailler. Utiliser unsafeWindow est n'est plus recommandé , et des erreurs se produisent lorsque pushState est appelé à partir d'un script client après avoir été modifié:

Permission refusée d'accéder à la propriété history.pushState

À la place, il existe une API appelée exportFunction qui permet d’injecter la fonction dans window.history comme ceci:

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

Réponse de galambalazs monkey patches window.history.pushState et window.history.replaceState, mais pour une raison quelconque, il a cessé de fonctionner pour moi. Voici une alternative qui n’est pas aussi agréable parce qu’elle utilise polling:

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