Popstate o załadowaniu strony w Chrome

94

Używam interfejsu API historii dla mojej aplikacji internetowej i mam jeden problem. Wykonuję wywołania Ajax, aby zaktualizować niektóre wyniki na stronie i używam history.pushState () w celu zaktualizowania paska adresu przeglądarki bez ponownego ładowania strony. Następnie oczywiście używam window.popstate w celu przywrócenia poprzedniego stanu po kliknięciu przycisku wstecz.

Problem jest dobrze znany - Chrome i Firefox inaczej traktują to zdarzenie popstate. Chociaż Firefox nie uruchamia go przy pierwszym załadowaniu, Chrome tak. Chciałbym mieć styl Firefoksa i nie uruchamiać zdarzenia podczas ładowania, ponieważ po prostu aktualizuje wyniki przy użyciu dokładnie takich samych wyników podczas ładowania. Czy istnieje obejście problemu z wyjątkiem używania History.js ? Powodem, dla którego nie mam ochoty go używać, jest to, że sam potrzebuje zbyt wielu bibliotek JS, a ponieważ potrzebuję go zaimplementować w CMS z już zbyt dużą ilością JS, chciałbym zminimalizować JS, w to wstawiam .

Więc chciałbym wiedzieć, czy istnieje sposób, aby Chrome nie uruchamiał się „popstate” podczas ładowania, czy może ktoś próbował użyć History.js, ponieważ wszystkie biblioteki zostały połączone w jeden plik.

spliter
źródło
3
Aby dodać, zachowanie Firefoksa to określone i poprawne zachowanie. Od wersji Chrome 34 zostało to naprawione! Brawo dla twórców Opery!
Henry

Odpowiedzi:

49

W Google Chrome w wersji 19 przestało działać rozwiązanie z @spliter. Jak zauważył @johnnymire, history.state w Chrome 19 istnieje, ale jest zerowe.

Moim obejściem jest dodanie window.history.state! == null do sprawdzania, czy stan istnieje w window.history:

var popped = ('state' in window.history && window.history.state !== null), initialURL = location.href;

Przetestowałem to we wszystkich głównych przeglądarkach oraz w Chrome w wersjach 19 i 18. Wygląda na to, że działa.

Pavel Linkesch
źródło
5
Dzięki za ostrzeżenia; Myślę, że możesz uprościć ini nullsprawdzić, != nullponieważ to również zajmie undefined.
pimvdb
5
Użycie tego będzie działać tylko wtedy, gdy zawsze używasz nullw swoich history.pushState()metodach, takich jak history.pushState(null,null,'http//example.com/). To prawda, to prawdopodobnie większość zastosowań (tak to jest ustawione w jquery.pjax.js i większości innych dem). Ale jeśli przeglądarki zaimplementują window.history.state(jak FF i Chrome 19+), window.history.state może mieć wartość różną od zera po odświeżeniu lub po ponownym uruchomieniu przeglądarki
niewyjaśnione
19
To już nie działa dla mnie w Chrome 21. Skończyło się na ustawieniu wyskakującego na false przy ładowaniu strony, a następnie ustawieniu wyskakującego na true przy każdym push lub pop. Ten neutery Chrome'a ​​pojawia się przy pierwszym załadowaniu. Nieco lepiej wyjaśniono to tutaj: github.com/defunkt/jquery-pjax/issues/143#issuecomment-6194330
Chad von Nau
1
@ChadvonNau świetny pomysł i działa świetnie - wielkie dzięki!
sowasred2012
Przyszedłem z tym samym problemem i skończyłem używając prostego rozwiązania @ChadvonNau.
Pherrymason
30

Jeśli nie chcesz podejmować specjalnych działań dla każdego handlera, którego dodajesz do onpopstate, moje rozwiązanie może być dla Ciebie interesujące. Dużym plusem tego rozwiązania jest również to, że zdarzenia onpopstate mogą być obsługiwane przed zakończeniem ładowania strony.

Po prostu uruchom ten kod raz, zanim dodasz jakiekolwiek programy obsługi onpopstate i wszystko powinno działać zgodnie z oczekiwaniami (podobnie jak w Mozilli ^^) .

(function() {
    // There's nothing to do for older browsers ;)
    if (!window.addEventListener)
        return;
    var blockPopstateEvent = document.readyState!="complete";
    window.addEventListener("load", function() {
        // The timeout ensures that popstate-events will be unblocked right
        // after the load event occured, but not in the same event-loop cycle.
        setTimeout(function(){ blockPopstateEvent = false; }, 0);
    }, false);
    window.addEventListener("popstate", function(evt) {
        if (blockPopstateEvent && document.readyState=="complete") {
            evt.preventDefault();
            evt.stopImmediatePropagation();
        }
    }, false);
})();

Jak to działa:

Chrome , Safari i prawdopodobnie inne przeglądarki webkit uruchamiają zdarzenie onpopstate po załadowaniu dokumentu. Nie jest to zamierzone, więc blokujemy zdarzenia popstate aż do pierwszego cyklu pętli zdarzeń po załadowaniu dokumentu. Odbywa się to za pomocą wywołań disableDefault i stopImmediatePropagation (w przeciwieństwie do stopPropagation stopImmediatePropagation natychmiast zatrzymuje wszystkie wywołania obsługi zdarzeń).

Jednak ponieważ readyState dokumentu jest już w stanie „complete”, gdy Chrome uruchamia się błędnie w trybie popstate, zezwalamy na zdarzenia opopstate, które były uruchamiane przed zakończeniem ładowania dokumentu, aby umożliwić wywołania onpopstate przed załadowaniem dokumentu.

Aktualizacja 2014-04-23: Naprawiono błąd, w którym zdarzenia popstate były blokowane, jeśli skrypt jest wykonywany po załadowaniu strony.

Torben
źródło
4
Jak dotąd najlepsze rozwiązanie dla mnie. Od lat używam metody setTimeout, ale powoduje ona błędne zachowanie, gdy strona ładuje się powoli / gdy użytkownik bardzo szybko uruchamia funkcję pushstate. window.history.statenie powiedzie się przy przeładowaniu, jeśli ustawiono stan. Ustawienie zmiennej przed pushState zaśmieca kod. Twoje rozwiązanie jest eleganckie i można je upuścić na inne skrypty, bez wpływu na resztę kodu. Dzięki.
kik
4
To jest rozwiązanie! Gdybym tylko przeczytał to wiele dni temu, próbując rozwiązać ten problem. To jedyny, który działał bez zarzutu. Dziękuję Ci!
Ben Fleming,
1
Doskonałe wyjaśnienie i jedyne rozwiązanie warte wypróbowania.
Sunny R Gupta
To świetne rozwiązanie. W chwili pisania tego tekstu, najnowszy Chrome i FF w porządku, problem dotyczy Safari (Mac i iPhone). To rozwiązanie działa jak urok.
Vin
1
Woot! Jeśli Safari psuje Ci dzień, to są kody, których szukasz!
DanO
28

Używanie tylko setTimeout nie jest poprawnym rozwiązaniem, ponieważ nie masz pojęcia, ile czasu zajmie załadowanie zawartości, więc możliwe jest, że zdarzenie popstate jest emitowane po przekroczeniu limitu czasu.

Oto moje rozwiązanie: https://gist.github.com/3551566

/*
* Necessary hack because WebKit fires a popstate event on document load
* https://code.google.com/p/chromium/issues/detail?id=63040
* https://bugs.webkit.org/process_bug.cgi
*/
window.addEventListener('load', function() {
  setTimeout(function() {
    window.addEventListener('popstate', function() {
      ...
    });
  }, 0);
});
Sonny Piers
źródło
To jedyne rozwiązanie, które u mnie zadziałało. Dzięki!
Justin
3
Słodkie! Działa lepiej niż najbardziej pozytywna i / lub zaakceptowana odpowiedź!
ProblemsOfSumit
Czy ktoś ma aktywny link do tego rozwiązania, nie mogę dotrzeć do podanego linku gist.github.com/3551566
Harbir
1
dlaczego potrzebujesz sedna?
Oleg Vaskevich
Doskonała odpowiedź. Dzięki.
sideroxylon
25

Rozwiązanie zostało znalezione w jquery.pjax.js liniach 195-225:

// Used to detect initial (useless) popstate.
// If history.state exists, assume browser isn't going to fire initial popstate.
var popped = ('state' in window.history), initialURL = location.href


// popstate handler takes care of the back and forward buttons
//
// You probably shouldn't use pjax on pages with other pushState
// stuff yet.
$(window).bind('popstate', function(event){
  // Ignore inital popstate that some browsers fire on page load
  var initialPop = !popped && location.href == initialURL
  popped = true
  if ( initialPop ) return

  var state = event.state

  if ( state && state.pjax ) {
    var container = state.pjax
    if ( $(container+'').length )
      $.pjax({
        url: state.url || location.href,
        fragment: state.fragment,
        container: container,
        push: false,
        timeout: state.timeout
      })
    else
      window.location = location.href
  }
})
spliter
źródło
8
To rozwiązanie przestało działać u mnie w systemie Windows (Chrome 19), nadal działa na komputerze Mac (Chrome 18). Wygląda na to, że history.state istnieje w Chrome 19, ale nie 18
johnnymire
1
@johnnymire Opublikowałem moje rozwiązanie dla Chrome 19 niżej: stackoverflow.com/a/10651028/291500
Pavel Linkesch
Ktoś powinien edytować tę odpowiedź, aby odzwierciedlić zachowanie po Chrome 19.
Jorge Suárez de Lis
5
Dla każdego, kto szuka rozwiązania, odpowiedź od Torbena działa jak urok w obecnych wersjach Chrome i Firefox od dzisiaj
Jorge Suárez de Lis
1
Teraz, w kwietniu 2015 r., Użyłem tego rozwiązania do rozwiązania problemu z Safari. Musiałem użyć zdarzenia onpopstate, aby uzyskać obsługę naciśnięcia przycisku Wstecz w hybrydowej aplikacji iOS / Android za pomocą Cordova. Safari uruchamia swój onpopstate nawet po ponownym załadowaniu, które wywołałem programowo. Sposób, w jaki mój kod został skonfigurowany, wszedł w nieskończoną pętlę po wywołaniu funkcji reload (). To naprawiło. Potrzebowałem tylko pierwszych 3 wierszy po deklaracji zdarzenia (plus zadania na górze).
Martavis P.
14

Bardziej bezpośrednim rozwiązaniem niż ponowna implementacja pjax jest ustawienie zmiennej w pushState i sprawdzenie zmiennej w popState, aby początkowe polecenie popState nie było niespójnie uruchamiane podczas ładowania (nie jest to rozwiązanie specyficzne dla jquery, po prostu używanie go do zdarzeń):

$(window).bind('popstate', function (ev){
  if (!window.history.ready && !ev.originalEvent.state)
    return; // workaround for popstate on load
});

// ... later ...

function doNavigation(nextPageId) {
  window.history.ready = true;

  history.pushState(state, null, 'content.php?id='+ nextPageId); 
  // ajax in content instead of loading server-side
}
Tom McKenzie
źródło
Czy nie byłoby to, gdyby zawsze oceniało się jako fałsz? To znaczy, window.history.readyzawsze jest prawdą.
Andriy Drozdyuk
Zaktualizowałem przykład, aby był nieco bardziej przejrzysty. Zasadniczo chcesz obsługiwać zdarzenia popstate tylko wtedy, gdy wszystko jest jasne. Możesz osiągnąć ten sam efekt z setTimeout 500 ms po załadowaniu, ale szczerze mówiąc jest to trochę tanie.
Tom McKenzie
3
To powinno być odpowiedź IMO, wypróbowałem rozwiązanie Pavelsa i nie działało ono poprawnie w Firefoksie
Porco
To działało dla mnie jako łatwe rozwiązanie tego irytującego problemu. Wielkie dzięki!
nirazul
!window.history.ready && !ev.originalEvent.state- te dwie rzeczy nigdy nie są definiowane, kiedy odświeżam stronę. dlatego żaden kod nie jest wykonywany.
chovy
4

Początkowe zdarzenie onpopstate Webkit nie ma przypisanego stanu, więc możesz użyć tego do sprawdzenia niepożądanego zachowania:

window.onpopstate = function(e){
    if(e.state)
        //do something
};

Kompleksowe rozwiązanie, pozwalające na nawigację z powrotem do oryginalnej strony, opierałoby się na tym pomyśle:

<body onload="init()">
    <a href="page1" onclick="doClick(this); return false;">page 1</a>
    <a href="page2" onclick="doClick(this); return false;">page 2</a>
    <div id="content"></div>
</body>

<script>
function init(){
   openURL(window.location.href);
}
function doClick(e){
    if(window.history.pushState)
        openURL(e.getAttribute('href'), true);
    else
        window.open(e.getAttribute('href'), '_self');
}
function openURL(href, push){
    document.getElementById('content').innerHTML = href + ': ' + (push ? 'user' : 'browser'); 
    if(window.history.pushState){
        if(push)
            window.history.pushState({href: href}, 'your page title', href);
        else
            window.history.replaceState({href: href}, 'your page title', href);
    }
}
window.onpopstate = function(e){
    if(e.state)
        openURL(e.state.href);
};
</script>

Chociaż nadal może to wystrzelić dwukrotnie (z pewną sprytną nawigacją), można to zrobić po prostu sprawdzając poprzedni href.

som
źródło
1
Dzięki. Rozwiązanie pjax przestało działać w systemie iOS 5.0, ponieważ window.history.statew iOS jest również zerowe. Wystarczy sprawdzić, czy właściwość e.state ma wartość null.
Husky
Czy są jakieś znane problemy z tym rozwiązaniem? Działa doskonale w każdej testowanej przeglądarce.
Jacob Ewing
3

To jest moje obejście.

window.setTimeout(function() {
  window.addEventListener('popstate', function() {
    // ...
  });
}, 1000);
Baggz
źródło
Nie dla mnie na Chromium 28 na Ubuntu. Czasami zdarzenie jest nadal uruchamiane.
Jorge Suárez de Lis
Sam to wypróbowałem, nie zawsze działa. Czasami popstate uruchamia się później niż limit czasu w zależności od początkowego załadowania strony
Andrew Burgess
1

Oto moje rozwiązanie:

var _firstload = true;
$(function(){
    window.onpopstate = function(event){
        var state = event.state;

        if(_firstload && !state){ 
            _firstload = false; 
        }
        else if(state){
            _firstload = false;
            // you should pass state.some_data to another function here
            alert('state was changed! back/forward button was pressed!');
        }
        else{
            _firstload = false;
            // you should inform some function that the original state returned
            alert('you returned back to the original state (the home state)');
        }
    }
})   
prograhammer
źródło
0

W przypadku użycia event.state !== nullpowrót do historii do pierwszej załadowanej strony nie będzie działać w przeglądarkach innych niż mobilne. Używam sessionStorage, aby zaznaczyć, kiedy naprawdę zaczyna się nawigacja Ajax.

history.pushState(url, null, url);
sessionStorage.ajNavStarted = true;

window.addEventListener('popstate', function(e) {
    if (sessionStorage.ajNavStarted) {
        location.href = (e.state === null) ? location.href : e.state;
    }
}, false);

iluzjonista
źródło
window.onload = function () {if (history.state === null) {history.replaceState (location.pathname + location.search + location.hash, null, location.pathname + location.search + location.hash); }}; window.addEventListener ('popstate', function (e) {if (e.state! == null) {location.href = e.state;}}, false);
iluzjonista
-1

Przedstawione rozwiązania mają problem z przeładowaniem strony. Poniższe wydaje się działać lepiej, ale testowałem tylko przeglądarki Firefox i Chrome. Wykorzystuje fakt, że wydaje się istnieć różnica między e.event.statei window.history.state.

window.addEvent('popstate', function(e) {
    if(e.event.state) {
        window.location.reload(); // Event code
    }
});
user2604836
źródło
e.eventjest niezdefiniowane
chovy
-1

Wiem, że sprzeciwiłeś się temu, ale naprawdę powinieneś po prostu użyć History.js, ponieważ usuwa milion niezgodności przeglądarek. Poszedłem ręczną trasą naprawczą tylko po to, aby później odkryć, że jest coraz więcej problemów, o których dowiesz się dopiero po drodze. To naprawdę nie jest takie trudne w dzisiejszych czasach:

<script src="//cdnjs.cloudflare.com/ajax/libs/history.js/1.8/native.history.min.js" type="text/javascript"></script>

Przeczytaj interfejs API pod adresem https://github.com/browserstate/history.js

ubershmekel
źródło
-1

To rozwiązało problem. Wszystko, co zrobiłem, to ustawienie funkcji limitu czasu, która opóźnia wykonanie funkcji na tyle długo, aby przegapić zdarzenie popstate uruchamiane podczas ładowania strony

if (history && history.pushState) {
  setTimeout(function(){
    $(window).bind("popstate", function() {
      $.getScript(location.href);
    });
  },3000);
}
Jeremy Lynch
źródło
-2

Możesz utworzyć zdarzenie i uruchomić je po obsłudze ładowania.

var evt = document.createEvent("PopStateEvent");
evt.initPopStateEvent("popstate", false, false, { .. state object  ..});
window.dispatchEvent(evt);

Uwaga, w Chrome / Safari jest to trochę zepsute, ale przesłałem łatkę do WebKit i powinna być wkrótce dostępna, ale jest to „najbardziej poprawny” sposób.

Kinlan
źródło
-2

To działało dla mnie w Firefoksie i Chrome

window.onpopstate = function(event) { //back button click
    console.log("onpopstate");
    if (event.state) {
        window.location.reload();
    }
};
Alexey
źródło