Popstate lors du chargement de la page dans Chrome

94

J'utilise l'API History pour mon application Web et j'ai un problème. Je fais des appels Ajax pour mettre à jour certains résultats sur la page et j'utilise history.pushState () afin de mettre à jour la barre d'emplacement du navigateur sans recharger la page. Ensuite, bien sûr, j'utilise window.popstate afin de restaurer l'état précédent lorsque le bouton retour est cliqué.

Le problème est bien connu - Chrome et Firefox traitent cet événement popstate différemment. Alors que Firefox ne le lance pas lors du premier chargement, Chrome le fait. Je voudrais avoir le style Firefox et ne pas déclencher l'événement en charge car il met simplement à jour les résultats avec exactement les mêmes lors du chargement. Existe-t-il une solution de contournement sauf en utilisant History.js ? La raison pour laquelle je n'ai pas envie de l'utiliser est - il a besoin de beaucoup trop de bibliothèques JS par lui-même et, comme j'ai besoin qu'il soit implémenté dans un CMS avec déjà trop de JS, je voudrais minimiser JS que je mets dedans .

J'aimerais donc savoir s'il existe un moyen de faire en sorte que Chrome ne lance pas 'popstate' lors du chargement ou, peut-être, que quelqu'un a essayé d'utiliser History.js car toutes les bibliothèques se sont rassemblées dans un seul fichier.

séparateur
la source
3
Pour ajouter, le comportement de Firefox est le comportement spécifié et correct. À partir de Chrome 34, il est maintenant corrigé! Yay pour les développeurs d'Opéra!
Henry Chan

Réponses:

49

Dans Google Chrome dans la version 19, la solution de @spliter a cessé de fonctionner. Comme @johnnymire l'a souligné, history.state dans Chrome 19 existe, mais il est nul.

Ma solution de contournement consiste à ajouter window.history.state! == null pour vérifier si l'état existe dans window.history:

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

Je l'ai testé dans tous les principaux navigateurs et dans les versions Chrome 19 et 18. Il semble que cela fonctionne.

Pavel Linkesch
la source
5
Merci pour l'information; Je pense que vous pouvez simplifier inet nullvérifier avec, != nullcar cela prendra également en charge undefined.
pimvdb
5
Son utilisation ne fonctionnera que si vous utilisez toujours nulldans vos history.pushState()méthodes comme history.pushState(null,null,'http//example.com/). Certes, c'est probablement la plupart des utilisations (c'est ainsi que cela est configuré dans jquery.pjax.js et la plupart des autres démos). Mais si les navigateurs implémentent window.history.state(comme FF et Chrome 19+), window.history.state pourrait être non nul lors d'une actualisation ou après un redémarrage du navigateur
inexpliquéBacn
19
Cela ne fonctionne plus pour moi dans Chrome 21. J'ai fini par définir simplement popped sur false lors du chargement de la page, puis sur true sur n'importe quel push ou pop. Ce pop de Chrome neuters lors du premier chargement. C'est un peu mieux expliqué ici: github.com/defunkt/jquery-pjax/issues/143#issuecomment-6194330
Chad von Nau
1
@ChadvonNau excellente idée, et cela fonctionne un régal - merci beaucoup!
sowasred2012
Je suis venu avec ce même problème et j'ai fini par utiliser la solution simple @ChadvonNau.
Pherrymason
30

Si vous ne souhaitez pas prendre de mesures spéciales pour chaque gestionnaire que vous ajoutez à onpopstate, ma solution pourrait vous intéresser. Un gros avantage de cette solution est également que les événements onpopstate peuvent être gérés avant la fin du chargement de la page.

Exécutez simplement ce code une fois avant d'ajouter des gestionnaires onpopstate et tout devrait fonctionner comme prévu (c'est-à-dire comme dans Mozilla ^^) .

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

Comment ça fonctionne:

Chrome , Safari et probablement d'autres navigateurs Webkit déclenchent l'événement onpopstate lorsque le document a été chargé. Ce n'est pas prévu, donc nous bloquons les événements popstate jusqu'à ce que le premier événement boucle cicle après le chargement du document. Cela est effectué par les appels preventDefault et stopImmediatePropagation (contrairement à stopPropagation, stopImmediatePropagation arrête instantanément tous les appels de gestionnaire d'événements).

Cependant, étant donné que le readyState du document est déjà sur «terminé» lorsque Chrome se déclenche par erreur surpopstate, nous autorisons les événements opopstate, qui ont été déclenchés avant la fin du chargement du document, pour autoriser les appels onpopstate avant le chargement du document.

Mise à jour 23/04/2014: Correction d'un bug où les événements popstate étaient bloqués si le script est exécuté après le chargement de la page.

Torben
la source
4
La meilleure solution à ce jour pour moi. J'utilise la méthode setTimeout depuis des années, mais cela provoque un comportement bogué lorsque la page se charge lentement / lorsque l'utilisateur déclenche le pushstate très rapidement. window.history.stateéchoue lors du rechargement si un état a été défini. La définition de la variable avant pushState encombre le code. Votre solution est élégante et peut être ajoutée à d'autres scripts, sans impact sur le reste de la base de code. Merci.
kik
4
C'est LA solution! Si seulement j'avais lu cela il y a quelques jours en essayant de résoudre ce problème. C'est le seul qui a parfaitement fonctionné. Je vous remercie!
Ben Fleming
1
Excellente explication et la seule solution qui vaut la peine d'être essayée.
Sunny R Gupta
C'est une excellente solution. Au moment de la rédaction de cet article, dernier chrome et FF très bien, le problème concerne Safari (Mac et iPhone). Cette solution fonctionne comme un charme.
Vin le
1
Woot! Si Safari gâche votre journée, ce sont les codes que vous recherchez!
DanO
28

L'utilisation de setTimeout n'est pas une solution correcte car vous n'avez aucune idée du temps qu'il faudra pour que le contenu soit chargé, il est donc possible que l'événement popstate soit émis après le délai d'expiration.

Voici ma solution: 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
la source
C'est la seule solution qui a fonctionné pour moi. Merci!
Justin
3
doux! Fonctionne mieux que la réponse la plus votée et / ou acceptée!
ProblemsOfSumit
Quelqu'un a-t-il le lien actif vers cette solution, je ne peux pas accéder au lien donné gist.github.com/3551566
Harbir
1
pourquoi avez-vous besoin de l'essentiel?
Oleg Vaskevich
Réponse parfaite. Merci.
sideroxylon
25

La solution a été trouvée dans jquery.pjax.js lignes 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
  }
})
séparateur
la source
8
Cette solution a cessé de fonctionner pour moi sous Windows (Chrome 19), fonctionnant toujours sur Mac (Chrome 18). On dirait que history.state existe dans Chrome 19 mais pas 18.
johnnymire
1
@johnnymire J'ai publié ma solution pour Chrome 19 inférieur: stackoverflow.com/a/10651028/291500
Pavel Linkesch
Quelqu'un doit modifier cette réponse pour refléter ce comportement post-Chrome 19.
Jorge Suárez de Lis
5
Pour ceux qui recherchent une solution, la réponse de Torben fonctionne comme un charme sur les versions actuelles de Chrome et Firefox à partir d'aujourd'hui
Jorge Suárez de Lis
1
Maintenant, en avril 2015, j'ai utilisé cette solution pour résoudre un problème avec Safari. J'ai dû utiliser l'événement onpopstate afin de réaliser une manipulation en appuyant sur le bouton retour sur une application hybride iOS / Android utilisant Cordova. Safari déclenche son onpopstate même après un rechargement, que j'ai appelé par programme. Avec la façon dont mon code a été configuré, il est entré dans une boucle infinie après l'appel de reload (). Cela l'a corrigé. Je n'avais besoin que des 3 premières lignes après la déclaration d'événement (plus les affectations en haut).
Martavis P.
14

Une solution plus directe que la réimplémentation de pjax est de définir une variable sur pushState et de vérifier la variable sur popState, de sorte que le popState initial ne se déclenche pas de manière incohérente à la charge (pas une solution spécifique à jquery, juste en l'utilisant pour les événements):

$(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
la source
Ne serait-ce pas si toujours évalué à faux? Je veux dire, window.history.readyc'est toujours vrai.
Andriy Drozdyuk
Mise à jour de l'exemple pour être un peu plus clair. Fondamentalement, vous ne voulez gérer les événements popstate qu'une fois que vous avez donné le feu vert. Vous pourriez obtenir le même effet avec un setTimeout 500ms après le chargement, mais pour être honnête, c'est un peu bon marché.
Tom McKenzie
3
Cela devrait être la réponse IMO, j'ai essayé la solution Pavels et cela ne fonctionnait pas correctement dans Firefox
Porco
Cela a fonctionné pour moi comme une solution facile à ce problème ennuyeux. Merci beaucoup!
nirazul
!window.history.ready && !ev.originalEvent.state- ces deux choses ne sont jamais définies lorsque j'actualise la page. donc aucun code ne s'exécute.
chovy
4

L' événement onpopstate initial de Webkit n'a aucun état attribué, vous pouvez donc l'utiliser pour vérifier le comportement indésirable:

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

Une solution complète, permettant de revenir à la page d'origine, s'appuierait sur cette idée:

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

Bien que cela puisse encore se déclencher deux fois (avec une navigation astucieuse), il peut être géré simplement avec une vérification par rapport au href précédent.

som
la source
1
Merci. La solution pjax a cessé de fonctionner sur iOS 5.0 car la valeur window.history.stateest également nulle dans iOS. Il suffit de vérifier si la propriété e.state est nulle.
Husky
Y a-t-il des problèmes connus avec cette solution? Cela fonctionne parfaitement sur tous les navigateurs que je teste.
Jacob Ewing
3

C'est ma solution de contournement.

window.setTimeout(function() {
  window.addEventListener('popstate', function() {
    // ...
  });
}, 1000);
Baggz
la source
Pas pour moi sur Chromium 28 sur Ubuntu. Parfois, l'événement est toujours déclenché.
Jorge Suárez de Lis
J'ai essayé cela moi-même, ne fonctionne pas toujours. Parfois, le popstate se déclenche plus tard que le délai d'expiration en fonction du chargement initial de la page
Andrew Burgess
1

Voici ma solution:

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
la source
0

En cas d'utilisation, le event.state !== nullretour dans l'historique à la première page chargée ne fonctionnera pas dans les navigateurs non mobiles. J'utilise sessionStorage pour marquer quand la navigation ajax démarre vraiment.

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

illusionniste
la source
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);
ilusionist
-1

Les solutions présentées ont un problème lors du rechargement de la page. Ce qui suit semble mieux fonctionner, mais je n'ai testé que Firefox et Chrome. Il utilise la réalité, qu'il semble y avoir une différence entre e.event.stateet window.history.state.

window.addEvent('popstate', function(e) {
    if(e.event.state) {
        window.location.reload(); // Event code
    }
});
user2604836
la source
e.eventest undefined
chovy
-1

Je sais que vous vous êtes opposé à cela, mais vous devriez vraiment utiliser History.js car il élimine un million d'incompatibilités de navigateur. J'ai suivi la voie de la réparation manuelle pour découvrir plus tard qu'il y avait de plus en plus de problèmes que vous ne découvrirez que plus tard. Ce n'est vraiment pas si difficile de nos jours:

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

Et lisez l'API sur https://github.com/browserstate/history.js

ubershmekel
la source
-1

Cela a résolu le problème pour moi. Tout ce que j'ai fait a été de définir une fonction de délai d'expiration qui retarde l'exécution de la fonction suffisamment longtemps pour manquer l'événement popstate qui est déclenché sur pageload

if (history && history.pushState) {
  setTimeout(function(){
    $(window).bind("popstate", function() {
      $.getScript(location.href);
    });
  },3000);
}
Jeremy Lynch
la source
-2

Vous pouvez créer un événement et le déclencher après votre gestionnaire de chargement.

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

Notez que ceci est légèrement cassé dans Chrome / Safari, mais j'ai soumis le correctif à WebKit et il devrait être disponible bientôt, mais c'est la manière la plus "correcte".

Kinlan
la source
-2

Cela a fonctionné pour moi dans Firefox et Chrome

window.onpopstate = function(event) { //back button click
    console.log("onpopstate");
    if (event.state) {
        window.location.reload();
    }
};
Alexey
la source