Popstate en la carga de la página en Chrome

94

Estoy usando History API para mi aplicación web y tengo un problema. Hago llamadas Ajax para actualizar algunos resultados en la página y uso history.pushState () para actualizar la barra de ubicación del navegador sin recargar la página. Luego, por supuesto, uso window.popstate para restaurar el estado anterior cuando se hace clic en el botón Atrás.

El problema es bien conocido: Chrome y Firefox tratan ese evento popstate de manera diferente. Si bien Firefox no lo enciende en la primera carga, Chrome lo hace. Me gustaría tener el estilo de Firefox y no activar el evento en la carga, ya que solo actualiza los resultados con exactamente los mismos en la carga. ¿Existe una solución alternativa excepto usar History.js ? La razón por la que no tengo ganas de usarlo es: necesita demasiadas bibliotecas JS por sí mismo y, dado que necesito que se implemente en un CMS con demasiado JS, me gustaría minimizar el JS que estoy poniendo en él. .

Entonces, me gustaría saber si hay una manera de hacer que Chrome no inicie 'popstate' al cargar o, tal vez, alguien intentó usar History.js ya que todas las bibliotecas se combinaron en un solo archivo.

spliter
fuente
3
Para agregar, el comportamiento de Firefox es el comportamiento correcto y especificado. ¡A partir de Chrome 34, ahora está arreglado! ¡Bien por los desarrolladores de Opera!
Henry Chan

Respuestas:

49

En Google Chrome en la versión 19, la solución de @spliter dejó de funcionar. Como señaló @johnnymire, history.state en Chrome 19 existe, pero es nulo.

Mi solución es agregar window.history.state! == null para verificar si el estado existe en window.history:

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

Lo probé en todos los navegadores principales y en las versiones 19 y 18 de Chrome. Parece que funciona.

Pavel Linkesch
fuente
5
Gracias por el aviso; Creo que puede simplificar el iny nullverificar con solo != nullya que eso también se solucionará undefined.
pimvdb
5
Usar esto solo funcionará si siempre usa nullen sus history.pushState()métodos como history.pushState(null,null,'http//example.com/). Por supuesto, eso probablemente sería la mayoría de los usos (así es como está configurado en jquery.pjax.js y la mayoría de las otras demostraciones). Pero si los navegadores se implementan window.history.state(como FF y Chrome 19+), window.history.state podría no ser nulo en una actualización o después de reiniciar el navegador
inexplicableBacn
19
Esto ya no funciona para mí en Chrome 21. Terminé configurando popped en falso en la carga de la página y luego estableciendo popped en verdadero en cualquier push o pops. Esto neutraliza el pop de Chrome en la primera carga. Se explica un poco mejor aquí: github.com/defunkt/jquery-pjax/issues/143#issuecomment-6194330
Chad von Nau
1
@ChadvonNau excelente idea, y funciona de maravilla, ¡muchas gracias!
sowasred2012
Vine con este mismo problema y terminé usando la solución simple @ChadvonNau.
Pherrymason
30

En caso de que no desee tomar medidas especiales para cada controlador que agregue a onpopstate, mi solución puede ser interesante para usted. Una gran ventaja de esta solución es también que los eventos onpopstate se pueden manejar antes de que finalice la carga de la página.

Simplemente ejecute este código una vez antes de agregar cualquier controlador onpopstate y todo debería funcionar como se esperaba (también conocido como en 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);
})();

Cómo funciona:

Chrome , Safari y probablemente otros navegadores webkit activan el evento onpopstate cuando se carga el documento. Esto no está previsto, por lo que bloqueamos los eventos popstate hasta el primer ciclo de bucle de eventos después de que se haya cargado el documento. Esto se realiza mediante las llamadas preventDefault y stopImmediatePropagation (a diferencia de stopPropagation, stopImmediatePropagation detiene instantáneamente todas las llamadas al controlador de eventos).

Sin embargo, dado que el readyState del documento ya está en "completo" cuando Chrome activa onpopstate por error, permitimos los eventos opopstate, que se activaron antes de que finalice la carga del documento, para permitir llamadas a onpopstate antes de que se cargue el documento.

Actualización 23-04-2014: se corrigió un error por el cual los eventos popstate se bloqueaban si el script se ejecutaba después de cargar la página.

Torben
fuente
4
La mejor solución hasta ahora para mí. He estado usando el método setTimeout durante años, pero causa un comportamiento erróneo cuando la página se carga lentamente / cuando el usuario activa pushstate muy rápidamente. window.history.statefalla en la recarga si se estableció un estado. Configuración de la variable antes del código desordenado de pushState. Su solución es elegante y puede colocarse encima de otros scripts, sin afectar al resto del código base. Gracias.
kik
4
¡Ésta es LA solución! Si tan solo leyera esto hace días cuando trato de resolver este problema. Este es el único que ha funcionado a la perfección. ¡Gracias!
Ben Fleming
1
Excelente explicación y la única solución que vale la pena intentar.
Sunny R Gupta
Esta es una gran solucion. Al momento de escribir, la última multa de Chrome y FF, el problema es con Safari (Mac y iPhone) Esta solución funciona a las mil maravillas.
Vin
1
¡Woot! Si Safari te está arruinando el día, ¡estos son los códigos que estás buscando!
DanO
28

Usar solo setTimeout no es una solución correcta porque no tiene idea de cuánto tiempo llevará cargar el contenido, por lo que es posible que el evento popstate se emita después del tiempo de espera.

Aquí está mi solución: 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
fuente
Esta es la única solución que funcionó para mí. ¡Gracias!
Justin
3
¡dulce! ¡Funciona mejor que la respuesta más votada y / o aceptada!
ProblemsOfSumit
¿Alguien tiene el enlace activo a esta solución? No puedo acceder al enlace dado gist.github.com/3551566
Harbir
1
¿Por qué necesitas lo esencial?
Oleg Vaskevich
Respuesta perfecta. Gracias.
sideroxylon
25

La solución se ha encontrado en jquery.pjax.js líneas 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
fuente
8
Esta solución dejó de funcionar para mí en Windows (Chrome 19), todavía funciona en Mac (Chrome 18). Parece que history.state existe en Chrome 19 pero no 18.
johnnymire
1
@johnnymire Publiqué mi solución para Chrome 19 inferior: stackoverflow.com/a/10651028/291500
Pavel Linkesch
Alguien debería editar esta respuesta para reflejar este comportamiento posterior a Chrome 19.
Jorge Suárez de Lis
5
Para cualquiera que busque una solución, la respuesta de Torben funciona como un encanto en las versiones actuales de Chrome y Firefox a partir de hoy
Jorge Suárez de Lis
1
Ahora, en abril de 2015, utilicé esta solución para resolver un problema con Safari. Tuve que usar el evento onpopstate para lograr un manejo presionando el botón Atrás en una aplicación híbrida de iOS / Android usando Cordova. Safari dispara su onpopstate incluso después de una recarga, que llamé programáticamente. Con la forma en que se configuró mi código, entró en un bucle infinito después de que se llamó a reload (). Esto lo arregló. Solo necesitaba las primeras 3 líneas después de la declaración del evento (más las asignaciones en la parte superior).
Martavis P.
14

Una solución más directa que reimplementar pjax es establecer una variable en pushState y verificar la variable en popState, por lo que el popState inicial no se dispara de manera inconsistente en la carga (no es una solución específica de jquery, solo se usa para eventos):

$(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
fuente
¿No sería eso si siempre se evalúa como falso? Quiero decir, window.history.readysiempre es cierto.
Andriy Drozdyuk
Se actualizó el ejemplo para que sea un poco más claro. Básicamente, solo desea manejar eventos popstate una vez que dé el visto bueno. Podría lograr el mismo efecto con setTimeout 500ms después de la carga, pero para ser honesto, es un poco barato.
Tom McKenzie
3
Esta debería ser la respuesta en mi opinión, probé la solución de Pavels y no funcionó correctamente en Firefox
Porco
Esto funcionó para mí como una solución fácil a este molesto problema. ¡Muchas gracias!
nirazul
!window.history.ready && !ev.originalEvent.state- esas dos cosas nunca se definen cuando actualizo la página. por lo tanto, no se ejecuta ningún código.
chovy
4

El evento onpopstate inicial de Webkit no tiene un estado asignado, por lo que puede usar esto para verificar el comportamiento no deseado:

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

Una solución integral, que permita la navegación de regreso a la página original, se basaría en esta idea:

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

Si bien esto aún podría dispararse dos veces (con una navegación ingeniosa), se puede manejar simplemente con una verificación contra el href anterior.

som
fuente
1
Gracias. La solución pjax dejó de funcionar en iOS 5.0 porque window.history.statetambién es nula en iOS. Solo comprobar si la propiedad e.state es nula es suficiente.
Husky
¿Hay algún problema conocido con esta solución? Funciona perfectamente en cualquier navegador que pruebe.
Jacob Ewing
3

Esta es mi solución.

window.setTimeout(function() {
  window.addEventListener('popstate', function() {
    // ...
  });
}, 1000);
Baggz
fuente
No para mí en Chromium 28 en Ubuntu. A veces, el evento aún se dispara.
Jorge Suárez de Lis
Probé esto yo mismo, no siempre funciona. A veces, el estado emergente se activa más tarde que el tiempo de espera, según la carga de la página inicial
Andrew Burgess
1

Esta es mi solución:

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)');
        }
    }
})   
programador
fuente
0

La mejor manera de hacer que Chrome no active popstate en la carga de una página es votando a favor https://code.google.com/p/chromium/issues/detail?id=63040 . ¡Saben que Chrome no cumple con la especificación HTML5 durante dos años completos y aún no lo han solucionado!

Dave
fuente
2
Esto se ha solucionado desde Chrome 34.
matys84pl
0

En caso de uso, event.state !== nullregresar en el historial a la primera página cargada no funcionará en navegadores que no sean móviles. Utilizo sessionStorage para marcar cuándo comienza realmente la navegación 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);

ilusionista
fuente
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;}}, falso);
ilusionista
-1

Las soluciones presentadas tienen un problema en la recarga de la página. Lo siguiente parece funcionar mejor, pero solo he probado Firefox y Chrome. Utiliza la actualidad, que parece haber una diferencia entre e.event.statey window.history.state.

window.addEvent('popstate', function(e) {
    if(e.event.state) {
        window.location.reload(); // Event code
    }
});
user2604836
fuente
e.eventno está definido
chovy
-1

Sé que preguntaste en contra, pero realmente deberías usar History.js, ya que elimina un millón de incompatibilidades de navegadores. Seguí la ruta de reparación manual solo para descubrir más tarde que había más y más problemas que solo descubrirás más adelante. Realmente no es tan difícil hoy en día:

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

Y lea la api en https://github.com/browserstate/history.js

ubershmekel
fuente
-1

Esto resolvió mi problema. Todo lo que hice fue establecer una función de tiempo de espera que retrasa la ejecución de la función el tiempo suficiente para perder el evento popstate que se activa al cargar la página

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

Puede crear un evento y dispararlo después de su controlador de carga.

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

Tenga en cuenta que esto está un poco roto en Chrome / Safari, pero envié el parche a WebKit y debería estar disponible pronto, pero es la forma "más correcta".

Kinlan
fuente
-2

Esto funcionó para mí en Firefox y Chrome

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