Popstate no carregamento da página no Chrome

94

Estou usando a API de histórico para meu aplicativo da web e tenho um problema. Eu faço chamadas Ajax para atualizar alguns resultados na página e uso o history.pushState () para atualizar a barra de localização do navegador sem recarregar a página. Então, é claro, eu uso window.popstate para restaurar o estado anterior quando o botão Voltar é clicado.

O problema é bem conhecido - o Chrome e o Firefox tratam esse evento popstate de maneira diferente. Embora o Firefox não inicialize na primeira vez, o Chrome o faz. Eu gostaria de ter o estilo do Firefox e não disparar o evento no carregamento, pois ele apenas atualiza os resultados exatamente com os mesmos no carregamento. Existe uma solução alternativa, exceto usar History.js ? Não estou com vontade de usá-lo - ele precisa de muitas bibliotecas JS sozinho e, como preciso implementá-lo em um CMS com muito JS, gostaria de minimizar o JS que estou colocando nele .

Então, gostaria de saber se existe uma maneira de fazer o Chrome não disparar 'popstate' no carregamento ou, talvez, alguém tentou usar History.js com todas as bibliotecas agrupadas em um arquivo.

divisor
fonte
3
Para adicionar, o comportamento do Firefox é o comportamento especificado e correto. No Chrome 34, agora está consertado! Yay para os desenvolvedores do Opera!
Henry Chan

Respostas:

49

Na versão 19 do Google Chrome, a solução de @spliter parou de funcionar. Como @johnnymire apontou, history.state no Chrome 19 existe, mas é nulo.

Minha solução alternativa é adicionar window.history.state! == null para verificar se existe estado em window.history:

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

Eu testei em todos os principais navegadores e nas versões 19 e 18 do Chrome. Parece que funciona.

Pavel Linkesch
fonte
5
Obrigado pelo aviso; Acho que você pode simplificar o ine nullverificar apenas, != nullpois isso também resolverá undefined.
pimvdb
5
Usar isso só funcionará se você sempre usar nullem seus history.pushState()métodos como history.pushState(null,null,'http//example.com/). Concedido, isso provavelmente seria a maioria dos usos (é assim que é configurado no jquery.pjax.js e na maioria das outras demos). Mas se os navegadores forem implementados window.history.state(como FF e Chrome 19+), window.history.state pode ser não nulo em uma atualização ou após a reinicialização do navegador
inexplicávelBacn
19
Isso não está mais funcionando para mim no Chrome 21. Acabei definindo popped como false no carregamento da página e, em seguida, definindo pop como true em qualquer push ou pops. Isso neutraliza o estouro do Chrome no primeiro carregamento. É explicado um pouco melhor aqui: github.com/defunkt/jquery-pjax/issues/143#issuecomment-6194330
Chad von Nau
1
Excelente ideia @ChadvonNau, e funciona muito bem - muito obrigado!
sowasred2012
Vim com esse mesmo problema e acabei usando a solução simples do @ChadvonNau.
Pherrymason de
30

Caso você não queira tomar medidas especiais para cada gerenciador adicionado ao onpopstate, minha solução pode ser interessante para você. Uma grande vantagem dessa solução é também que os eventos onpopstate podem ser manipulados antes que o carregamento da página seja concluído.

Apenas execute este código uma vez antes de adicionar qualquer manipulador onpopstate e tudo deve funcionar como esperado (também conhecido como no 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);
})();

Como funciona:

Chrome , Safari e provavelmente outros navegadores de webkit disparam o evento onpopstate quando o documento é carregado. Isso não é intencional, portanto, bloqueamos eventos popstate até o primeiro ciclo de loop de eventos após o documento ter sido carregado. Isso é feito pelas chamadas preventDefault e stopImmediatePropagation (ao contrário de stopPropagation, stopImmediatePropagation interrompe todas as chamadas de manipulador de eventos instantaneamente).

No entanto, como o readyState do documento já está em "complete" quando o Chrome dispara onpopstate erroneamente, permitimos eventos opopstate, que foram disparados antes de o carregamento do documento terminar para permitir chamadas onpopstate antes do documento ser carregado.

Atualização 2014-04-23: Corrigido um bug em que os eventos popstate eram bloqueados se o script fosse executado depois que a página fosse carregada.

Torben
fonte
4
Melhor solução até agora para mim. Eu uso o método setTimeout há anos, mas ele causa um comportamento de bugs quando a página está carregando lentamente / quando o usuário aciona o pushstate muito rapidamente. window.history.statefalha na recarga se um estado foi definido. A configuração da variável antes do código de desordem pushState. Sua solução é elegante e pode ser descartada em cima de outros scripts, sem afetar o resto da base de código. Obrigado.
kik
4
Esta é a solução! Se eu tivesse lido isso há muito tempo atrás, ao tentar resolver este problema. Este é o único que funcionou perfeitamente. Obrigado!
Ben Fleming,
1
Excelente explicação e a única solução que vale a pena tentar.
Sunny R Gupta
Esta é uma otima soluçao. No momento em que este artigo foi escrito, o problema é o safari (mac e iphone). Esta solução funciona perfeitamente.
Vin
1
Uau! Se o Safari está estragando o seu dia, estes são os códigos que você está procurando!
DanO
28

Usar apenas setTimeout não é uma solução correta porque você não tem ideia de quanto tempo levará para o conteúdo ser carregado, então é possível que o evento popstate seja emitido após o tempo limite.

Aqui está minha solução: 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
fonte
Esta é a única solução que funcionou para mim. Obrigado!
Justin,
3
doce! Funciona melhor do que a resposta mais votada e / ou aceita!
ProblemsOfSumit
Alguém tem o link ativo para esta solução, não consigo acessar o link fornecido gist.github.com/3551566
Harbir
1
por que você precisa da essência?
Oleg Vaskevich
Resposta perfeita. Obrigado.
sideroxylon
25

A solução foi encontrado em jquery.pjax.js linhas 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
  }
})
divisor
fonte
8
Esta solução parou de funcionar para mim no Windows (Chrome 19), ainda funciona no Mac (Chrome 18). Parece que history.state existe no Chrome 19, mas não 18.
johnnymire
1
@johnnymire Publiquei minha solução para o Chrome 19 inferior: stackoverflow.com/a/10651028/291500
Pavel Linkesch
Alguém deveria editar esta resposta para refletir esse comportamento pós-Chrome 19.
Jorge Suárez de Lis
5
Para quem procura uma solução, a resposta de Torben funciona perfeitamente nas versões atuais do Chrome e do Firefox a partir de hoje
Jorge Suárez de Lis
1
Agora, em abril de 2015, usei essa solução para resolver um problema com o Safari. Tive de usar o evento onpopstate para conseguir lidar com o botão Voltar em um aplicativo híbrido iOS / Android usando Cordova. O Safari dispara seu onpopstate mesmo após uma recarga, que chamei de forma programática. Da forma como meu código foi configurado, ele entrou em um loop infinito depois que reload () foi chamado. Isso consertou. Eu só precisava das primeiras 3 linhas após a declaração do evento (mais as atribuições no topo).
Martavis P.
14

Uma solução mais direta do que reimplementar pjax é definir uma variável em pushState e verificar a variável em popState, para que o popState inicial não seja disparado inconsistentemente no carregamento (não é uma solução específica de jquery, apenas usando-o 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
fonte
Isso não seria sempre avaliado como falso? Quer dizer, window.history.readyé sempre verdade.
Andriy Drozdyuk
Atualizado o exemplo para ficar um pouco mais claro. Basicamente, você só quer lidar com eventos popstate depois de liberar tudo. Você poderia obter o mesmo efeito com setTimeout 500ms após o carregamento, mas, para ser honesto, é um pouco barato.
Tom McKenzie
3
Esta deve ser a resposta IMO, tentei a solução Pavels e não funcionou corretamente no Firefox
Porco
Isso funcionou para mim como uma solução fácil para esse problema irritante. Muito obrigado!
nirazul
!window.history.ready && !ev.originalEvent.state- essas duas coisas nunca são definidas quando eu atualizo a página. portanto, nenhum código é executado.
chovy
4

O evento onpopstate inicial do Webkit não tem estado atribuído, então você pode usar isso para verificar o comportamento indesejado:

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

Uma solução abrangente, permitindo a navegação de volta à página original, se basearia nesta ideia:

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

Embora isso ainda possa disparar duas vezes (com alguma navegação bacana), pode ser tratado simplesmente com uma verificação em relação ao href anterior.

som
fonte
1
Obrigado. A solução pjax parou de funcionar no iOS 5.0 porque o window.history.statetambém é nulo no iOS. Basta verificar se a propriedade e.state é nula.
Husky
Existem problemas conhecidos com esta solução? Funciona perfeitamente em qualquer navegador que eu testar.
Jacob Ewing
3

Esta é a minha solução alternativa.

window.setTimeout(function() {
  window.addEventListener('popstate', function() {
    // ...
  });
}, 1000);
Baggz
fonte
Não para mim no Chromium 28 no Ubuntu. Às vezes, o evento ainda é disparado.
Jorge Suárez de Lis
Tentei sozinho, nem sempre funciona. Às vezes, o popstate dispara depois do tempo limite, dependendo do carregamento da página inicial
Andrew Burgess
1

Esta é minha solução:

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

A melhor maneira de fazer o Chrome não disparar popstate no carregamento de uma página é votar https://code.google.com/p/chromium/issues/detail?id=63040 . Eles sabem que o Chrome não está em conformidade com as especificações HTML5 há dois anos inteiros e ainda não corrigiu isso!

Dave
fonte
2
Isso foi corrigido desde o Chrome 34.
matys84pl
0

Em caso de uso, event.state !== nullretornar ao histórico para a primeira página carregada não funcionará em navegadores não móveis. Eu uso sessionStorage para marcar quando a navegação ajax realmente começa.

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
fonte
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);
ilusionista de
-1

As soluções apresentadas apresentam um problema no recarregamento da página. O seguinte parece funcionar melhor, mas eu só testei o Firefox e o Chrome. Ele usa a realidade, que parece haver uma diferença entre e.event.statee window.history.state.

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

Eu sei que você pediu contra isso, mas você realmente deveria apenas usar History.js, pois ele limpa um milhão de incompatibilidades do navegador. Eu segui a rota de conserto manual apenas para descobrir mais tarde que havia mais e mais problemas que você só descobrirá no futuro. Realmente não é tão difícil hoje em dia:

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

E leia a api em https://github.com/browserstate/history.js

ubershmekel
fonte
-1

Isso resolveu o problema para mim. Tudo o que fiz foi definir uma função de tempo limite que atrasa a execução da função por tempo suficiente para perder o evento popstate que é disparado no pageload

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

Você pode criar um evento e dispará-lo após o manipulador onload.

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

Note, isso está um pouco quebrado no Chrome / Safari, mas eu enviei o patch para o WebKit e ele deve estar disponível em breve, mas é a forma "mais correta".

Kinlan
fonte
-2

Isso funcionou para mim no Firefox e Chrome

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