Wie werde ich über history.pushState über Änderungen des Verlaufs informiert?

153

Nachdem HTML5 nun eingeführt wurde history.pushState, um den Browserverlauf zu ändern, verwenden Websites dies in Kombination mit Ajax, anstatt die Fragmentkennung der URL zu ändern.

Leider bedeutet dies, dass diese Anrufe von nicht mehr erkannt werden können onhashchange.

Meine Frage ist: Gibt es eine zuverlässige Möglichkeit (Hack ?;)) Zu erkennen, wann eine Website verwendet wird history.pushState? Die Spezifikation gibt nichts über Ereignisse an, die ausgelöst werden (zumindest konnte ich nichts finden).
Ich habe versucht, eine Fassade zu erstellen und durch window.historymein eigenes JavaScript-Objekt ersetzt, aber es hatte überhaupt keine Auswirkungen.

Weitere Erklärung: Ich entwickle ein Firefox-Add-On, das diese Änderungen erkennen und entsprechend handeln muss.
Ich weiß, dass es vor einigen Tagen eine ähnliche Frage gab, die fragte, ob das Abhören einiger DOM-Ereignisse effizient wäre, aber ich würde mich lieber nicht darauf verlassen, da diese Ereignisse aus vielen verschiedenen Gründen generiert werden können.

Aktualisieren:

Hier ist eine jsfiddle (verwenden Sie Firefox 4 oder Chrome 8), die zeigt, dass sie onpopstatenicht ausgelöst wird, wenn sie pushStateaufgerufen wird (oder mache ich etwas falsch? Sie können sie jederzeit verbessern!).

Update 2:

Ein weiteres (Neben-) Problem ist, dass window.locationes bei der Verwendung nicht aktualisiert wird pushState(aber ich habe darüber bereits hier auf SO gelesen, denke ich).

Felix Kling
quelle

Antworten:

194

5.5.9.1 Ereignisdefinitionen

Das Popstate- Ereignis wird in bestimmten Fällen ausgelöst, wenn Sie zu einem Sitzungsverlaufseintrag navigieren.

Demnach gibt es keinen Grund, warum Popstate bei Verwendung ausgelöst wird pushState. Aber eine Veranstaltung wie diese pushstatewäre praktisch. Da historyes sich um ein Host-Objekt handelt, sollten Sie vorsichtig damit sein, aber Firefox scheint in diesem Fall nett zu sein. Dieser Code funktioniert einwandfrei:

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

Ihre jsfiddle wird :

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

Sie können Affen window.history.replaceStateauf die gleiche Weise patchen .

Hinweis: Natürlich können Sie onpushstatedem globalen Objekt einfach etwas hinzufügen und es sogar dazu bringen, mehr Ereignisse über zu verarbeitenadd/removeListener

gblazex
quelle
Sie sagten, Sie hätten das gesamte historyObjekt ersetzt. Dies kann in diesem Fall unnötig sein.
Gblazex
@ Galambalazs: Ja wahrscheinlich. Vielleicht (weiß nicht) window.historyist schreibgeschützt, aber die Eigenschaften des Verlaufsobjekts sind nicht ... Vielen Dank für diese Lösung :)
Felix Kling
Laut Spezifikation gibt es ein "Pagetransition" -Ereignis, es scheint, dass es noch nicht implementiert ist.
Mohamed Mansour
2
@ user280109 - Ich würde es dir sagen, wenn ich es wüsste. :) Ich denke, es gibt keine Möglichkeit, dies in Opera atm zu tun.
Gblazex
1
@cprcrack Dies dient dazu, den globalen Bereich sauber zu halten. Sie müssen die native pushStateMethode für die spätere Verwendung speichern . Anstelle einer globalen Variablen habe ich mich entschieden, den gesamten Code in ein IIFE zu kapseln: en.wikipedia.org/wiki/Immedially-invoked_function_expression
gblazex
4

Ich habe das benutzt:

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

Es ist fast das gleiche wie bei Galambalazs .

Normalerweise ist es jedoch übertrieben. Und es funktioniert möglicherweise nicht in allen Browsern. (Ich kümmere mich nur um meine Version meines Browsers.)

(Und es hinterlässt eine Var _wr, also möchten Sie es vielleicht einwickeln oder so. Das war mir egal.)

Rudie
quelle
4

Endlich den "richtigen" Weg gefunden, dies zu tun! Es erfordert das Hinzufügen eines Privilegs zu Ihrer Erweiterung und die Verwendung der Hintergrundseite (nicht nur eines Inhaltsskripts), aber es funktioniert.

Das gewünschte Ereignis browser.webNavigation.onHistoryStateUpdatedwird ausgelöst, wenn eine Seite die historyAPI zum Ändern der URL verwendet. Es wird nur für Websites ausgelöst, auf die Sie Zugriff haben, und Sie können auch einen URL-Filter verwenden, um den Spam bei Bedarf weiter zu reduzieren. Es erfordert die webNavigationErlaubnis (und natürlich die Host-Erlaubnis für die relevante (n) Domain (s)).

Der Ereignisrückruf erhält die Registerkarten-ID, die URL, zu der "navigiert" wird, und andere solche Details. Wenn Sie beim Auslösen des Ereignisses eine Aktion im Inhaltsskript auf dieser Seite ausführen müssen, fügen Sie entweder das entsprechende Skript direkt von der Hintergrundseite ein oder lassen Sie das Inhaltsskript beim Laden portauf der Hintergrundseite öffnen und die Hintergrundseite speichern Dieser Port in einer Sammlung wird durch die Registerkarten-ID indiziert und sendet eine Nachricht über den relevanten Port (vom Hintergrundskript zum Inhaltsskript), wenn das Ereignis ausgelöst wird.

CBHacking
quelle
2

Sie könnten an die binden window.onpopstate Ereignis ?

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

Aus den Dokumenten:

Ein Ereignishandler für das Popstate-Ereignis im Fenster.

Jedes Mal, wenn sich der aktive Verlaufseintrag ändert, wird ein Popstate-Ereignis an das Fenster gesendet. Wenn der aktivierte Verlaufseintrag durch einen Aufruf von history.pushState () erstellt wurde oder von einem Aufruf von history.replaceState () betroffen war, enthält die Statuseigenschaft des Popstate-Ereignisses eine Kopie des Statusobjekts des Verlaufseintrags.

stef
quelle
6
Ich habe dies bereits versucht und das Ereignis wird nur ausgelöst, wenn die Benutzer in den Verlauf zurückkehren (oder eine der history.go, .back) Funktionen aufgerufen werden. Aber nicht weiter pushState. Hier ist mein Versuch, es zu testen, vielleicht mache ich etwas falsch: jsfiddle.net/fkling/vV9vd Es scheint nur so verwandt zu sein, dass, wenn der Verlauf von geändert wurde pushState, das entsprechende Statusobjekt bei den anderen Methoden an den Ereignishandler übergeben wird werden genannt.
Felix Kling
1
Ah. In diesem Fall kann ich mir nur vorstellen, eine Zeitüberschreitung zu registrieren, um die Länge des Verlaufsstapels zu überprüfen und ein Ereignis auszulösen, wenn sich die Stapelgröße geändert hat.
stef
Ok, das ist eine interessante Idee. Das einzige ist, dass das Timeout oft genug ausgelöst werden muss, damit der Benutzer keine (lange) Verzögerung bemerkt (ich muss Daten für die neue URL laden und anzeigen). Ich versuche immer, Timeouts und Abstimmungen nach Möglichkeit zu vermeiden, aber bis jetzt scheint dies die einzige Lösung zu sein. Ich werde noch auf andere Vorschläge warten. Aber vielen Dank fürs Erste!
Felix Kling
Es könnte sogar ausreichen, die Länge des Verlaufssticks bei jedem Klick zu überprüfen.
Felix Kling
1
Ja - das würde funktionieren. Ich habe damit gespielt - ein Problem ist der replaceState-Aufruf, der kein Ereignis auslöst, da sich die Größe des Stapels nicht geändert hätte.
stef
1

Da Sie nach einem Firefox-Addon fragen, ist hier der Code, mit dem ich arbeiten muss. Die Verwendung unsafeWindowwird nicht mehr empfohlen und es treten Fehler auf, wenn pushState nach einer Änderung von einem Client-Skript aufgerufen wird:

Berechtigung für den Zugriff auf die Eigenschaft history.pushState verweigert

Stattdessen gibt es eine API namens exportFunction , mit der die Funktion window.historywie folgt eingefügt werden kann :

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
quelle
In der letzten Zeile sollten Sie ändern unsafeWindow.history,zu window.history. Bei unsafeWindow hat es bei mir nicht funktioniert.
Lindsay-Needs-Sleep
0

Galambalazs Antwort Affenflecken window.history.pushStateund window.history.replaceState, aber aus irgendeinem Grund funktionierte es nicht mehr für mich. Hier ist eine Alternative, die nicht so schön ist, weil sie Polling verwendet:

(function() {
    var previousState = window.history.state;
    setInterval(function() {
        if (previousState !== window.history.state) {
            previousState = window.history.state;
            myCallback();
        }
    }, 100);
})();
Flimm
quelle
Gibt es eine Alternative zum Abrufen?
SuperUberDuper
@ SuperUberDuper: siehe die anderen Antworten.
Flimm
@ Flimm Polling ist nicht schön, aber hässlich. Aber du hattest keine Wahl, die du gesagt hast.
Yairopro
Dies ist viel besser als das Patchen von Affen. Das Patchen von Affen kann durch ein anderes Skript rückgängig gemacht werden und Sie erhalten KEINE Benachrichtigungen darüber.
Ivan Castellanos
0

Ich denke, dieses Thema braucht eine modernere Lösung.

Ich bin mir sicher, dass nsIWebProgressListeneres damals da war. Ich bin überrascht, dass niemand es erwähnt hat.

Aus einem Framescript (für die Kompatibilität mit e10s):

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

Dann hören Sie in der onLoacationChange

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

Das wird anscheinend alle pushState's fangen. Es gibt jedoch eine Kommentarwarnung, dass "AUCH für pushState ausgelöst wird". Wir müssen hier also noch mehr filtern, um sicherzustellen, dass es sich nur um Pushstate-Inhalte handelt.

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

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

Noitidart
quelle
Diese Antwort hat nichts mit dem Kommentar zu tun, es sei denn, ich irre mich. WebProgressListener sind für FF-Erweiterungen? Diese Frage ist über HTML5 Geschichte
Kloar
@Kloar lassen Sie
uns
0

Nun, ich sehe viele Beispiele für das Ersetzen der pushStateEigenschaft von, historyaber ich bin nicht sicher, ob dies eine gute Idee ist. Ich würde es vorziehen, ein Serviceereignis zu erstellen, das auf einer ähnlichen API wie der Historie basiert, sodass Sie nicht nur den Push-Status steuern, sondern auch den Status ersetzen können Außerdem öffnet es Türen für viele andere Implementierungen, die nicht auf der globalen Verlaufs-API basieren. Bitte überprüfen Sie das folgende Beispiel:

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

Wenn Sie die EventEmitterDefinition benötigen , basiert der obige Code auf dem NodeJS-Ereignisemitter: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/events.js . utils.inheritsDie Implementierung finden Sie hier: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970

Victor Queiroz
quelle
Ich habe gerade meine Antwort mit den angeforderten Informationen bearbeitet, bitte überprüfen!
Victor Queiroz
@ VictorQueiroz Das ist eine schöne und saubere Idee, um die Funktionen zu kapseln. Wenn jedoch eine Bibliothek eines dritten Teils pushState anfordert, wird Ihr HistoryApi nicht benachrichtigt.
Yairopro
0

Ich möchte die native Verlaufsmethode lieber nicht überschreiben, damit diese einfache Implementierung meine eigene Funktion namens eventedPush-Status erstellt, die nur ein Ereignis auslöst und history.pushState () zurückgibt. In beiden Fällen funktioniert es einwandfrei, aber ich finde diese Implementierung etwas sauberer, da native Methoden weiterhin so funktionieren, wie es zukünftige Entwickler erwarten.

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
quelle
0

Basierend auf der von @gblazex angegebenen Lösung Sie denselben Ansatz verfolgen möchten, jedoch möchten, folgen Sie dem folgenden Beispiel in Ihrer Javascript-Logik:

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

Implementieren Sie dann eine andere Funktion _notifyUrl(url), die alle erforderlichen Aktionen auslöst, die Sie möglicherweise benötigen, wenn die aktuelle Seiten-URL aktualisiert wird (auch wenn die Seite überhaupt nicht geladen wurde).

  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.
quelle
0

Da ich nur die neue URL haben wollte, habe ich die Codes von @gblazex und @Alberto S. angepasst, um Folgendes zu erhalten:

(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);
Andre Lopes
quelle
0

Ich halte es nicht für eine gute Idee, native Funktionen zu ändern, auch wenn Sie können, und Sie sollten immer Ihren Anwendungsbereich beibehalten. Ein guter Ansatz ist daher, nicht die globale pushState-Funktion zu verwenden, sondern eine eigene:

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>

Beachten Sie, dass Sie keinen Ereignisauslöser erhalten, wenn ein anderer Code die native pushState-Funktion verwendet (in diesem Fall sollten Sie jedoch Ihren Code überprüfen).

Maxwell sc
quelle
-5

Standardmäßig:

Beachten Sie, dass nur das Aufrufen von history.pushState () oder history.replaceState () kein Popstate-Ereignis auslöst. Das Popstate-Ereignis wird nur durch eine Browseraktion ausgelöst, z. B. durch Klicken auf die Schaltfläche "Zurück" (oder Aufrufen von history.back () in JavaScript).

Wir müssen history.back () aufrufen, um WindowEventHandlers.onpopstate zu aktivieren

So insted von:

history.pushState(...)

machen:

history.pushState(...)
history.pushState(...)
history.back()
user2360102
quelle