Come ricevere notifiche sui cambiamenti della cronologia tramite history.pushState?


154

Quindi ora che HTML5 introduce history.pushStateper cambiare la cronologia dei browser, i siti Web iniziano a usarlo in combinazione con Ajax invece di cambiare l'identificatore di frammento dell'URL.

Purtroppo ciò significa che tali chiamate non possono più essere rilevate da onhashchange.

La mia domanda è: esiste un modo affidabile (hack?;)) Per rilevare quando viene utilizzato un sito Web history.pushState? Le specifiche non indicano nulla sugli eventi generati (almeno non sono riuscito a trovare nulla).
Ho provato a creare una facciata e sostituito window.historycon il mio oggetto JavaScript, ma non ha avuto alcun effetto.

Ulteriore spiegazione: sto sviluppando un componente aggiuntivo per Firefox che deve rilevare queste modifiche e agire di conseguenza.
So che qualche giorno fa c'era una domanda simile che chiedeva se ascoltare alcuni eventi DOM sarebbe stato efficace, ma preferirei non fare affidamento su questo perché questi eventi possono essere generati per molte ragioni diverse.

Aggiornare:

Ecco un jsfiddle (usa Firefox 4 o Chrome 8) che mostra che onpopstatenon viene attivato quando pushStateviene chiamato (o sto facendo qualcosa di sbagliato? Sentiti libero di migliorarlo!).

Aggiornamento 2:

Un altro problema (laterale) è che window.locationnon viene aggiornato quando si utilizza pushState(ma ho letto di questo già qui su SO penso).

Risposte:


194

5.5.9.1 Definizioni degli eventi

L' evento popstate viene generato in alcuni casi quando si accede a una voce della cronologia della sessione.

In base a ciò, non vi è alcun motivo per il popstate da licenziare quando si utilizza pushState. Ma un evento come pushstatesarebbe utile. Perché historyè un oggetto host, dovresti stare attento con esso, ma Firefox sembra essere bello in questo caso. Questo codice funziona perfettamente:

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

Il tuo jsfiddle diventa :

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

Puoi rattoppare le scimmie window.history.replaceStateallo stesso modo.

Nota: ovviamente puoi onpushstatesemplicemente aggiungere all'oggetto globale e puoi anche farlo gestire più eventi tramiteadd/removeListener


Hai detto di aver sostituito l'intero historyoggetto. Ciò potrebbe non essere necessario in questo caso.
gblazex,

@galambalazs: Sì probabilmente. Forse (non lo so) window.historyè di sola lettura ma le proprietà dell'oggetto storia non sono ... Grazie mille per questa soluzione :)
Felix Kling

Secondo le specifiche c'è un evento di "pagetransition", sembra che non sia ancora implementato.
Mohamed Mansour,

2
@ user280109 - Te lo direi se lo sapessi. :) Penso che non ci sia modo di farlo in Opera atm.
gblazex,

1
@cprcrack Serve a mantenere pulito l'ambito globale. Devi salvare il pushStatemetodo nativo per un uso successivo. Quindi, invece di una variabile globale, ho scelto di incapsulare l'intero codice in un IIFE: en.wikipedia.org/wiki/Immediately-invoked_function_expression
gblazex

4

Prima usavo questo:

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

È quasi lo stesso dei galambalaz .

Di solito è eccessivo però. E potrebbe non funzionare in tutti i browser. (Mi interessa solo la mia versione del mio browser.)

(E lascia un var _wr, quindi potresti volerlo avvolgere o qualcosa del genere. Non me ne importava.)


4

Finalmente ho trovato il modo "corretto" per farlo! Richiede l'aggiunta di un privilegio all'estensione e l'utilizzo della pagina di sfondo (non solo uno script di contenuto), ma funziona.

L'evento desiderato è browser.webNavigation.onHistoryStateUpdated, che viene generato quando una pagina utilizza l' historyAPI per modificare l'URL. Viene attivato solo per i siti a cui hai l'autorizzazione ad accedere e puoi anche utilizzare un filtro URL per ridurre ulteriormente lo spam se necessario. Richiede l' webNavigationautorizzazione (e ovviamente l'autorizzazione dell'host per i domini pertinenti).

Il callback dell'evento ottiene l'ID della scheda, l'URL a cui si sta "navigando" e altri dettagli simili. Se è necessario eseguire un'azione nello script del contenuto in quella pagina quando l'evento si attiva, iniettare lo script pertinente direttamente dalla pagina di sfondo o fare in modo che lo script del contenuto apra porta alla pagina di sfondo quando viene caricato, per salvare la pagina di sfondo quella porta in una raccolta indicizzata da ID scheda e invia un messaggio attraverso la porta pertinente (dallo script in background allo script del contenuto) quando l'evento viene generato.


3

Si potrebbe legare al window.onpopstate all'evento?

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

Dai documenti:

Un gestore eventi per l'evento popstate nella finestra.

Un evento popstate viene inviato alla finestra ogni volta che cambia la voce della cronologia attiva. Se la voce della cronologia attivata è stata creata da una chiamata a history.pushState () o è stata interessata da una chiamata a history.replaceState (), la proprietà state dell'evento popstate contiene una copia dell'oggetto stato della voce history.


6
Ho già provato questo e l'evento viene attivato solo quando vengono richiamate le funzioni della cronologia (o di una delle history.go, .back). Ma non acceso pushState. Ecco il mio tentativo di testarlo, forse faccio qualcosa di sbagliato: jsfiddle.net/fkling/vV9vd Sembra collegato solo in questo modo che se la cronologia è stata modificata pushState, l'oggetto stato corrispondente viene passato al gestore eventi quando gli altri metodi sono chiamati.
Felix Kling

1
Ah. In tal caso, l'unica cosa a cui riesco a pensare è di registrare un timeout per esaminare la lunghezza dello stack della cronologia e generare un evento se le dimensioni dello stack sono cambiate.
stef

Ok questa è un'idea interessante. L'unica cosa è che il timeout dovrebbe essere attivato abbastanza spesso in modo che l'utente non si accorga di alcun (lungo) ritardo (devo caricare e mostrare i dati per il nuovo URL). Cerco sempre di evitare timeout e polling ove possibile, ma fino ad ora questa sembra essere l'unica soluzione. Aspetterò ancora altre proposte. Ma grazie mille per ora!
Felix Kling,

Potrebbe essere anche sufficiente controllare la lunghezza dello stick della cronologia ad ogni clic.
Felix Kling

1
Sì, avrebbe funzionato. Ci sto giocando - un problema è la chiamata di sostituzione dello stato che non genererebbe un evento perché le dimensioni dello stack non sarebbero cambiate.
stef

1

Dato che mi stai chiedendo di un componente aggiuntivo per Firefox, ecco il codice con cui devo lavorare. L'uso nonunsafeWindow è più raccomandato ed errori fuori quando viene chiamato pushState da uno script client dopo essere stato modificato:

Autorizzazione negata per accedere alla proprietà history.pushState

Invece, c'è un'API chiamata exportFunction che consente di iniettare la funzione in window.historyquesto modo:

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

Nell'ultima riga, dovresti passare unsafeWindow.history,a window.history. Non funzionava per me con Windows non sicuro.
Lindsay-Needs-Sleep

0

la risposta di galambalazs patch di scimmia window.history.pushStatee window.history.replaceState, per qualche ragione, ha smesso di funzionare per me. Ecco un'alternativa che non è così piacevole perché utilizza il polling:

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

esiste un'alternativa al polling?
SuperUberDuper,

@SuperUberDuper: vedi le altre risposte.
Flimm,

@Flimm Il polling non è bello, ma brutto. Ma bene, non avevi scelta che hai detto.
Yairopro,

Questo è molto meglio del patch di scimmia, il patch di scimmia può essere annullato da altri script e non si ricevono notifiche al riguardo.
Ivan Castellanos,

0

Penso che questo argomento abbia bisogno di una soluzione più moderna.

Sono sicuro che nsIWebProgressListenerfosse in giro, quindi sono sorpreso che nessuno l'abbia menzionato.

Da un framescript (per compatibilità con e10s):

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

Quindi ascoltando in onLoacationChange

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

Ciò apparentemente catturerà tutti i pushState. Ma c'è un commento che avverte che "attiva anche pushState". Quindi dobbiamo fare un po 'più di filtraggio qui per assicurarci che sia solo roba pushstate.

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

E: risorsa: //gre/modules/WebNavigationContent.js


Questa risposta non ha nulla a che fare con il commento, a meno che non mi sbagli. WebProgressListeners sono per estensioni FF? Questa domanda riguarda la storia di HTML5
Kloar il

@Kloar consente di eliminare questi commenti per favore
Noitidart,

0

Bene, vedo molti esempi di sostituzione della pushStateproprietà di historyma non sono sicuro che sia una buona idea, preferirei creare un evento di servizio basato su un'API simile alla cronologia in questo modo puoi controllare non solo lo stato push ma sostituire lo stato inoltre apre le porte a molte altre implementazioni che non si basano sull'API di cronologia globale. Si prega di controllare il seguente esempio:

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

Se è necessaria la EventEmitterdefinizione, il codice sopra riportato si basa sull'emettitore di eventi NodeJS: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/events.js . utils.inheritsl'implementazione si trova qui: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970


Ho appena modificato la mia risposta con le informazioni richieste, per favore controlla!
Victor Queiroz,

@VictorQueiroz Che idea carina e pulita per incapsulare le funzioni. Ma se una libreria di terze parti richiede pushState, HistoryApi non verrà avvisato.
Yairopro,

0

Preferirei non sovrascrivere il metodo della cronologia nativa, quindi questa semplice implementazione crea la mia funzione chiamata stato eventedPush che invia semplicemente un evento e restituisce history.pushState (). Ad ogni modo funziona bene, ma trovo questa implementazione un po 'più pulita poiché i metodi nativi continueranno a funzionare come previsto dai futuri sviluppatori.

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

0

Sulla base della soluzione fornita da @gblazex , nel caso in cui si desideri seguire lo stesso approccio, ma utilizzando le funzioni freccia, seguire l'esempio seguente nella logica 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();
});

Quindi, implementa un'altra funzione _notifyUrl(url) che attiva qualsiasi azione richiesta di cui potresti aver bisogno quando l'aggiornamento dell'URL della pagina corrente (anche se la pagina non è stata caricata)

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

0

Dal momento che volevo solo il nuovo URL, ho adattato i codici di @gblazex e @Alberto S. per ottenere questo:

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

0

Non penso che sia una buona idea modificare le funzioni native anche se puoi, e dovresti sempre mantenere l'ambito di applicazione, quindi un buon approccio non sta usando la funzione pushState globale, invece, usa una delle tue:

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>

Nota che se qualsiasi altro codice utilizza la funzione pushState nativa, non otterrai un trigger di evento (ma in tal caso, dovresti controllare il tuo codice)


-5

Come standard afferma:

Nota che solo chiamare history.pushState () o history.replaceState () non attiverà un evento popstate. L'evento popstate viene attivato solo eseguendo un'azione del browser, ad esempio facendo clic sul pulsante Indietro (o chiamando history.back () in JavaScript)

dobbiamo chiamare history.back () per trigeer WindowEventHandlers.onpopstate

Quindi ho istinto di:

history.pushState(...)

fare:

history.pushState(...)
history.pushState(...)
history.back()
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.