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

154

Alors maintenant que HTML5 introduit history.pushStatepour modifier l'historique des navigateurs, les sites Web commencent à l'utiliser 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 n'indique 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 window.historyje l'ai remplacée par mon propre objet JavaScript, mais cela n'a eu aucun effet.

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

Mettre à jour:

Voici un jsfiddle (utilisez Firefox 4 ou Chrome 8) qui montre qu'il onpopstatene se déclenche pas lors de l' pushStateappel (ou est-ce que je fais quelque chose de mal? N'hésitez pas à l'améliorer!).

Mise à jour 2:

Un autre problème (secondaire) est qu'il window.locationn'est pas mis à jour lors de l'utilisation pushState(mais j'ai déjà lu à ce sujet ici sur donc je pense).

Félix Kling
la source

Réponses:

194

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 déclenché lorsque vous utilisez pushState. Mais un événement tel que cela pushstateserait utile. Comme il historys'agit d'un objet hôte, vous devez être prudent avec lui, mais Firefox semble être bien 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.replaceStatede la même manière.

Remarque: bien sûr, vous pouvez ajouter onpushstatesimplement à l'objet global, et vous pouvez même le faire gérer plus d'événements viaadd/removeListener

gblazex
la source
Vous avez dit que vous avez remplacé tout l' historyobjet. Cela peut être inutile dans ce cas.
gblazex
@galambalazs: Oui probablement. Peut-être (je ne sais pas) window.historyest en lecture seule, mais les propriétés de l'objet historique ne sont pas ... Merci beaucoup pour cette solution :)
Felix Kling
Selon la spécification, il y a un événement "pagetransition", il semble qu'il ne soit pas encore implémenté.
Mohamed Mansour
2
@ user280109 - Je vous dirais si je savais. :) Je pense qu'il n'y a aucun moyen de faire cela dans Opera ATM.
gblazex
1
@cprcrack C'est pour garder la portée globale propre. Vous devez enregistrer la pushStateméthode native pour une utilisation ultérieure. Donc, au lieu d'une variable globale, j'ai choisi d'encapsuler tout le code dans un IIFE: en.wikipedia.org/wiki/Immediately-invoked_function_expression
gblazex
4

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

C'est généralement exagéré cependant. Et cela peut 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 comme ça. Je m'en fichais.)

Rudie
la source
4

Enfin trouvé la manière «correcte» de faire cela! Cela nécessite d'ajouter un privilège à votre extension et d'utiliser la page d'arrière-plan (pas seulement un script de contenu), mais cela fonctionne.

L'événement que vous souhaitez est browser.webNavigation.onHistoryStateUpdated, qui est déclenché lorsqu'une page utilise l' historyAPI pour modifier l'URL. Il se déclenche uniquement pour les sites auxquels vous êtes autorisé à accéder, et vous pouvez également utiliser un filtre d'URL pour réduire davantage le spam si vous en avez besoin. Il nécessite l' webNavigationautorisation (et bien sûr l'autorisation d'hôte pour le (s) domaine (s) concerné (s)).

Le rappel d'événement obtient l'ID de l'onglet, l'URL vers laquelle «navigue» et d'autres détails similaires. Si vous devez effectuer une action dans le script de contenu sur cette page lorsque l'événement se déclenche, soit injectez le script approprié directement à partir de la page d'arrière-plan, soit ouvrez le script de contenu portsur la page d'arrière-plan lors du chargement, enregistrez la page d'arrière-plan ce port dans une collection indexée par ID d'onglet et envoyer un message sur le port approprié (du script d'arrière-plan au script de contenu) lorsque l'événement se déclenche.

CBHacking
la source
3

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

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

À partir de la documentation:

Un gestionnaire d'événements pour l'événement popstate dans la fenêtre.

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

stef
la source
6
J'ai déjà essayé cela et l'événement n'est déclenché que lorsque les utilisateurs reviennent dans l'historique (ou l'une des fonctions history.go, .back) sont appelées. Mais pas sur pushState. Voici ma tentative de le tester, peut-être que je fais quelque chose de mal: jsfiddle.net/fkling/vV9vd Il semble seulement lié de cette manière que si l'historique a été modifié par pushState, l'objet d'état correspondant est passé au gestionnaire d'événements lorsque les autres méthodes sont appelés.
Felix Kling
1
Ah. Dans ce cas, la seule chose à laquelle je peux penser est d'enregistrer un timeout pour regarder la longueur de la pile d'historique et déclencher un événement si la taille de la pile a changé.
stef
Ok c'est une idée intéressante. La seule chose est que le timeout devrait se déclencher assez souvent pour que l'utilisateur ne remarque aucun (long) retard (je dois charger et afficher les données pour la nouvelle URL). J'essaie toujours d'éviter les délais d'attente et les sondages dans la mesure du possible, mais jusqu'à présent, cela semble être la seule solution. J'attendrai encore d'autres propositions. Mais merci beaucoup pour l'instant!
Felix Kling
Il peut même suffire de vérifier la longueur du bâton d'historique à chaque clic.
Felix Kling
1
Oui, cela fonctionnerait. J'ai joué avec cela - un problème est l'appel replaceState qui ne lancerait pas d'événement car la taille de la pile n'aurait pas changé.
stef
1

Puisque vous posez des questions sur un addon Firefox, voici le code que je dois travailler. L' utilisation unsafeWindowest pas recommandé plus , et les erreurs quand pushState est appelé à partir d' un script client après avoir été modifié:

Autorisation refusée d'accéder à l'historique de la propriété.

Au lieu de cela, il existe une API appelée exportFunction qui permet à la fonction d'être injectée window.historycomme 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});
nathancahill
la source
Sur la dernière ligne, vous devez changer unsafeWindow.history,pour window.history. Cela ne fonctionnait pas pour moi avec unsafeWindow.
Lindsay-Needs-Sleep le
0

patches de singe de réponse de galambalazswindow.history.pushState et window.history.replaceState, mais pour une raison quelconque, cela a cessé de fonctionner pour moi. Voici une alternative qui n'est pas aussi agréable car elle utilise l'interrogation:

(function() {
    var previousState = window.history.state;
    setInterval(function() {
        if (previousState !== window.history.state) {
            previousState = window.history.state;
            myCallback();
        }
    }, 100);
})();
Flimm
la source
existe-t-il une alternative au sondage?
SuperUberDuper
@SuperUberDuper: voir les autres réponses.
Flimm
@Flimm Polling n'est pas gentil, mais moche. Mais bon, tu n'avais pas le choix, tu as dit.
Yairopro
C'est beaucoup mieux que le patching de singe, le patching de singe peut être annulé par un autre script et vous n'obtenez AUCUNE notification à ce sujet.
Ivan Castellanos
0

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

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

À 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 écoutant dans le onLoacationChange

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

Cela attrapera apparemment tous les pushState. Mais il y a un commentaire avertissant qu'il "déclenche AUSSI pour pushState". Nous devons donc faire un peu plus de filtrage ici pour nous assurer qu'il ne s'agit que de choses pushstate.

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

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

Noitidart
la source
Cette réponse n'a rien à voir avec le commentaire, sauf erreur de ma part. Les WebProgressListeners sont pour les extensions FF? Cette question concerne l'histoire de HTML5
Kloar
@Kloar permet de supprimer ces commentaires s'il vous plaît
Noitidart
0

Eh bien, je vois de nombreux exemples de remplacement de la pushStatepropriété de historymais 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 cette façon, vous pouvez contrôler non seulement l'état de poussée, mais également remplacer l'état. ainsi et cela ouvre des portes à de nombreuses autres implémentations ne reposant pas sur l'API d'historique global. 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 EventEmitterdéfinition, le code ci-dessus est basé sur l'émetteur d'événements NodeJS: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/events.js . utils.inheritsLa mise en œuvre peut être trouvée ici: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970

Victor Queiroz
la source
Je viens de modifier ma réponse avec les informations demandées, veuillez vérifier!
Victor Queiroz
@VictorQueiroz C'est une idée sympa et propre pour encapsuler les fonctions. Mais si une bibliothèque tierce appelle pushState, votre HistoryApi ne sera pas notifié.
Yairopro
0

Je préfère ne pas écraser la méthode d'historique native afin que cette implémentation simple crée ma propre fonction appelée eventedPush state qui distribue simplement un événement et renvoie history.pushState (). Dans les deux cas, cela fonctionne bien, mais je trouve cette implémentation un peu plus propre, car les méthodes natives continueront à fonctionner comme les futurs développeurs l'attendent.

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"); 
jopfre
la source
0

Sur la base de la solution donnée par @gblazex , au cas où vous voudriez suivre la même approche, mais en utilisant les fonctions fléchées, suivez l'exemple ci-dessous dans votre logique 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();
});

Ensuite, implémentez une autre fonction _notifyUrl(url)qui déclenche toute action requise dont vous pourriez avoir besoin lorsque l'url de la page actuelle est mise à jour (même si la page n'a pas été chargée du tout)

  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}`);
  }
Alberto S.
la source
0

Puisque je voulais juste la nouvelle URL, j'ai adapté les codes de @gblazex et @Alberto S.pour obtenir ceci:

(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);
André Lopes
la source
0

Je ne pense pas que ce soit une bonne idée de modifier les fonctions natives même si vous le pouvez, et vous devez toujours garder la portée de votre application, donc une bonne approche consiste à ne pas utiliser la fonction globale pushState, à la place, utilisez l'une des vôtres:

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>

Notez que si un autre code utilise la fonction native pushState, vous n'obtiendrez pas de déclencheur d'événement (mais si cela se produit, vous devriez vérifier votre code)

Maxwell sc
la source
-5

Comme l'indique la norme:

Notez que le simple fait d'appeler history.pushState () ou history.replaceState () ne déclenchera pas d'événement popstate. L'événement popstate est uniquement déclenché en effectuant une action du navigateur telle que cliquer sur le bouton Précédent (ou appeler history.back () en JavaScript)

nous devons appeler history.back () pour triger WindowEventHandlers.onpopstate

Donc insted de:

history.pushState(...)

faire:

history.pushState(...)
history.pushState(...)
history.back()
utilisateur2360102
la source