Jak otrzymywać powiadomienia o zmianach w historii poprzez history.pushState?


154

Więc teraz, kiedy HTML5 wprowadza history.pushStatezmianę historii przeglądarek, strony internetowe zaczynają używać tego w połączeniu z Ajaxem zamiast zmieniać identyfikator fragmentu adresu URL.

Niestety oznacza to, że te połączenia nie mogą być już wykrywane przez onhashchange.

Moje pytanie brzmi: czy istnieje niezawodny sposób (włamanie?;)) Do wykrycia, kiedy witryna używa history.pushState? Specyfikacja nie mówi nic o zdarzeniach, które są wywoływane (przynajmniej nie mogłem nic znaleźć).
Próbowałem stworzyć fasadę i zastąpiłem window.historygo własnym obiektem JavaScript, ale nie przyniosło to żadnego efektu.

Dalsze wyjaśnienie: opracowuję dodatek do Firefoksa, który musi wykrywać te zmiany i odpowiednio działać.
Wiem, że kilka dni temu było podobne pytanie, które dotyczyło tego, czy odsłuchiwanie niektórych zdarzeń DOM byłoby wydajne, ale wolałbym nie polegać na tym, ponieważ zdarzenia te mogą być generowane z wielu różnych powodów.

Aktualizacja:

Oto jsfiddle (użyj przeglądarki Firefox 4 lub Chrome 8), który pokazuje, że onpopstatenie jest uruchamiany, gdy pushStatejest wywoływany (czy robię coś źle? Możesz to poprawić!).

Aktualizacja 2:

Innym (bocznym) problemem jest to, że window.locationnie jest aktualizowany podczas używania pushState(ale o tym czytałem już tutaj na SO tak myślę).

Odpowiedzi:


194

5.5.9.1 Definicje wydarzeń

Popstate zdarzenie jest zwolniony, w niektórych przypadkach, gdy przechodząc do pozycji historii sesji.

W związku z tym nie ma powodu, aby popstate był uruchamiany podczas korzystania z pushState. Ale takie wydarzenie pushstateby się przydało. Ponieważ historyjest to obiekt hosta, należy z nim uważać, ale w tym przypadku Firefox wydaje się być przyjemny. Ten kod działa dobrze:

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

Twoje jsfiddle staje się :

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

W window.history.replaceStateten sam sposób można małpować łaty .

Uwaga: oczywiście możesz onpushstatepo prostu dodać obiekt globalny, a nawet sprawić, by obsługiwał więcej zdarzeń za pośrednictwemadd/removeListener


Powiedziałeś, że wymieniłeś cały historyprzedmiot. W tym przypadku może to być niepotrzebne.
gblazex,

@galambalazs: Tak, prawdopodobnie. Może (nie wiem) window.historyjest tylko do odczytu, ale właściwości obiektu historii nie są ... Dzięki pęczek dla tego rozwiązania :)
Felix Kling

Zgodnie ze specyfikacją istnieje zdarzenie „pagetransition”, wydaje się, że nie zostało jeszcze wdrożone.
Mohamed Mansour,

2
@ user280109 - Powiedziałbym ci, gdybym wiedział. :) Myślę, że nie da się tego zrobić w operze atm.
gblazex

1
@cprcrack Służy do utrzymywania czystości globalnego zakresu. Musisz zapisać natywną pushStatemetodę do późniejszego wykorzystania. Więc zamiast zmiennej globalnej zdecydowałem się zamknąć cały kod w IIFE: en.wikipedia.org/wiki/Immediately-invoked_function_expression
gblazex

4

Używałem tego:

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

To prawie to samo, co galambalazs .

Zwykle jest to jednak przesada. I może nie działać we wszystkich przeglądarkach. (Dbam tylko o wersję mojej przeglądarki).

(I pozostawia var _wr, więc możesz chcieć to opakować czy coś. Nie obchodziło mnie to).


4

Wreszcie znalazłem „właściwy” sposób, aby to zrobić! Wymaga dodania uprawnienia do rozszerzenia i korzystania ze strony w tle (nie tylko skryptu zawartości), ale działa.

Wybranym zdarzeniem browser.webNavigation.onHistoryStateUpdatedjest wywoływane, gdy strona używa historyinterfejsu API do zmiany adresu URL. Uruchamia się tylko w przypadku witryn, do których masz uprawnienia dostępu, a jeśli zajdzie taka potrzeba, możesz również użyć filtru adresów URL, aby jeszcze bardziej ograniczyć spam. Wymaga webNavigationpozwolenia (i oczywiście pozwolenia hosta dla odpowiednich domen).

Wywołanie zwrotne zdarzenia pobiera identyfikator karty, adres URL, do którego następuje „nawigacja”, i inne tego typu szczegóły. Jeśli musisz wykonać jakąś czynność w skrypcie zawartości na tej stronie, gdy zdarzenie się uruchamia, albo wstrzyknij odpowiedni skrypt bezpośrednio ze strony w tle, albo otwórz skrypt zawartości na portstronę w tle po załadowaniu, zapisz stronę w tle ten port w kolekcji indeksowany przez identyfikator karty i wysyła komunikat przez odpowiedni port (ze skryptu działającego w tle do skryptu zawartości), gdy zdarzenie zostanie uruchomione.


3

Mógłbyś związać się z window.onpopstatewydarzeniem?

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

Z dokumentów:

Program obsługi zdarzenia dla zdarzenia popstate w oknie.

Zdarzenie popstate jest wysyłane do okna za każdym razem, gdy zmienia się aktywny wpis historii. Jeśli aktywowany wpis historii został utworzony przez wywołanie history.pushState () lub wywołało na niego wywołanie history.replaceState (), właściwość stanu zdarzenia popstate zawiera kopię obiektu stanu pozycji historii.


6
Próbowałem to już a zdarzenie jest wyzwalany tylko wtedy, gdy użytkownik wraca w historii (lub dowolną z history.go, .back) funkcje są wywoływane. Ale nie włączone pushState. Oto moja próba przetestowania, być może zrobię coś złego: jsfiddle.net/fkling/vV9vd Wydaje się, że jest to powiązane tylko w ten sposób, że jeśli historia została zmieniona przez pushState, odpowiedni obiekt stanu jest przekazywany do obsługi zdarzeń, gdy inne metody są nazywane.
Felix Kling

1
Ach. W takim przypadku jedyne, o czym przychodzi mi do głowy, to zarejestrowanie limitu czasu, aby sprawdzić długość stosu historii i uruchomić zdarzenie, jeśli rozmiar stosu się zmienił.
stef

Ok, to ciekawy pomysł. Jedyną rzeczą jest to, że limit czasu musiałby być uruchamiany wystarczająco często, aby użytkownik nie zauważył żadnego (długiego) opóźnienia (muszę załadować i wyświetlić dane dla nowego adresu URL). Zawsze staram się unikać przekroczenia limitów czasu i sondowania tam, gdzie to możliwe, ale do tej pory wydaje się to jedyne rozwiązanie. Nadal będę czekał na inne propozycje. Ale teraz bardzo dziękuję!
Felix Kling

Może nawet wystarczyć sprawdzenie długości paska historii przy każdym kliknięciu.
Felix Kling,

1
Tak - to zadziała. Miałem grę z tym - jednym z problemów jest wywołanie replaceState, które nie spowodowało zdarzenia, ponieważ rozmiar stosu nie zmieniłby się.
stef

1

Ponieważ pytasz o dodatek do Firefoksa, oto kod, który mam do pracy. Używanie nieunsafeWindow jest już zalecane , a błędy są wyświetlane, gdy pushState jest wywoływana ze skryptu klienta po modyfikacji:

Odmowa dostępu do historii właściwości.pushState

Zamiast tego istnieje interfejs API o nazwie exportFunction, który umożliwia wstrzyknięcie funkcji w window.historynastępujący sposób:

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

W ostatniej linii powinieneś zmienić unsafeWindow.history,na window.history. Nie działało to dla mnie z unsafeWindow.
Lindsay-Needs-Sleep

0

galambalazs w odpowiedzi małpie łatki window.history.pushStatei window.history.replaceState, ale z jakiegoś powodu przestał działać dla mnie. Oto alternatywa, która nie jest tak przyjemna, ponieważ wykorzystuje odpytywanie:

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

czy istnieje alternatywa dla ankietowania?
SuperUberDuper

@SuperUberDuper: zobacz inne odpowiedzi.
Flimm,

@Flimm Polling nie jest przyjemny, ale brzydki. Ale cóż, powiedziałeś, że nie miałeś wyboru.
Yairopro,

Jest to znacznie lepsze niż łatanie małpy, łatanie małpy może zostać cofnięte przez inny skrypt i NIE otrzymujesz żadnych powiadomień o tym.
Ivan Castellanos

0

Myślę, że ten temat wymaga bardziej nowoczesnego rozwiązania.

Jestem pewien, że wtedy nsIWebProgressListenerbył w pobliżu, jestem zaskoczony, że nikt o tym nie wspomniał.

Ze skryptu ramek (dla zgodności z e10s):

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

Następnie słuchaj w onLoacationChange

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

To najwyraźniej złapie wszystkie pushState. Ale jest komentarz ostrzegający, że "RÓWNIEŻ wyzwala pushState". Musimy więc trochę więcej filtrować, aby upewnić się, że to tylko rzeczy pushstate.

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

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


Ta odpowiedź nie ma nic wspólnego z komentarzem, chyba że się mylę. WebProgressListeners są dla rozszerzeń FF? To pytanie dotyczy historii HTML5
Kloar

@Kloar pozwala usunąć te komentarze, proszę
Noitidart

0

Cóż, widzę wiele przykładów zamiany pushStatewłaściwości, historyale nie jestem pewien, czy to dobry pomysł, wolałbym utworzyć zdarzenie usługi oparte na podobnym API do historii, aby można było kontrolować nie tylko stan wypychania, ale także stan zastępowania również i otwiera drzwi dla wielu innych implementacji, które nie opierają się na globalnym API historii. Proszę sprawdzić następujący przykład:

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

Jeśli potrzebujesz EventEmitterdefinicji, powyższy kod jest oparty na emiterze zdarzeń NodeJS: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/events.js . utils.inheritsimplementację można znaleźć tutaj: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970


Właśnie zredagowałem moją odpowiedź z żądanymi informacjami, sprawdź!
Victor Queiroz

@VictorQueiroz To ładny i czysty pomysł na hermetyzację funkcji. Ale jeśli jakaś biblioteka trzeciej części wywoła pushState, Twój HistoryApi nie zostanie powiadomiony.
Yairopro

0

Wolałbym nie nadpisywać natywnej metody historii, więc ta prosta implementacja tworzy moją własną funkcję o nazwie eventedPush, która po prostu wywołuje zdarzenie i zwraca history.pushState (). Tak czy inaczej działa dobrze, ale uważam, że ta implementacja jest nieco czystsza, ponieważ metody natywne będą nadal działać zgodnie z oczekiwaniami przyszłych programistów.

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

W oparciu o rozwiązanie podane przez @gblazex , jeśli chcesz zastosować to samo podejście, ale używając funkcji strzałek, postępuj zgodnie z poniższym przykładem w logice 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();
});

Następnie zaimplementuj inną funkcję, _notifyUrl(url)która wyzwala wszelkie wymagane działania, których możesz potrzebować, gdy aktualny adres URL strony zostanie zaktualizowany (nawet jeśli strona nie została w ogóle załadowana)

  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

Ponieważ chciałem tylko nowego adresu URL, dostosowałem kody @gblazex i @Alberto S., aby uzyskać to:

(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

Nie sądzę, aby modyfikowanie funkcji natywnych, nawet jeśli jest to możliwe, nie jest dobrym pomysłem i zawsze należy zachować zakres aplikacji, więc dobrym podejściem jest nie używanie funkcji globalnej pushState, zamiast tego użyj jednej z własnych:

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>

Zauważ, że jeśli jakikolwiek inny kod używa natywnej funkcji pushState, nie otrzymasz wyzwalacza zdarzenia (ale jeśli tak się stanie, powinieneś sprawdzić swój kod)


-5

Standardowo stwierdza:

Zauważ, że samo wywołanie history.pushState () lub history.replaceState () nie wywoła zdarzenia popstate. Zdarzenie popstate jest wyzwalane tylko przez wykonanie czynności przeglądarki, takiej jak kliknięcie przycisku Wstecz (lub wywołanie history.back () w JavaScript)

musimy wywołać history.back (), aby wywołać WindowEventHandlers.onpopstate

Więc zamiast:

history.pushState(...)

robić:

history.pushState(...)
history.pushState(...)
history.back()
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.