Como ser notificado sobre alterações no histórico via history.pushState?

154

Portanto, agora que o HTML5 se propõe history.pushStatea alterar o histórico dos navegadores, os sites começam a usá-lo em combinação com o Ajax, em vez de alterar o identificador de fragmento da URL.

Infelizmente, isso significa que essas chamadas não podem mais ser detectadas onhashchange.

Minha pergunta é: Existe uma maneira confiável (hack?;)) De detectar quando um site é usado history.pushState? A especificação não declara nada sobre eventos gerados (pelo menos, não consegui encontrar nada).
Tentei criar uma fachada e substituí-lo window.historypor meu próprio objeto JavaScript, mas não teve nenhum efeito.

Mais explicações: estou desenvolvendo um complemento do Firefox que precisa detectar essas alterações e agir de acordo.
Sei que havia uma pergunta semelhante, há alguns dias, que perguntava se a escuta de alguns eventos do DOM seria eficiente, mas prefiro não confiar nisso, porque esses eventos podem ser gerados por várias razões diferentes.

Atualizar:

Aqui está um jsfiddle (use o Firefox 4 ou Chrome 8) que mostra que onpopstatenão é acionado quando pushStateé chamado (ou estou fazendo algo errado? Fique à vontade para aprimorá-lo!).

Atualização 2:

Outro problema (lateral) é que window.locationnão é atualizado ao usar pushState(mas eu li sobre isso já aqui no SO, eu acho).

Felix Kling
fonte

Respostas:

194

5.5.9.1 Definições de eventos

O evento popstate é acionado em certos casos ao navegar para uma entrada do histórico da sessão.

De acordo com isso, não há razão para o popstate ser demitido quando você o usa pushState. Mas um evento como esse pushstateseria útil. Por historyser um objeto host, você deve ter cuidado com ele, mas o Firefox parece bom nesse caso. Este código funciona bem:

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

Seu jsfiddle se torna :

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

Você pode fazer o patch do macaco window.history.replaceStateda mesma maneira.

Nota: é claro que você pode adicionar onpushstatesimplesmente ao objeto global e até fazer com que ele lide com mais eventos viaadd/removeListener

gblazex
fonte
Você disse que substituiu o historyobjeto inteiro . Isso pode ser desnecessário neste caso.
precisa saber é o seguinte
@galambalazs: Sim, provavelmente. Talvez (não sei) window.historyé somente leitura, mas as propriedades do objeto a história não é ... Muito obrigada por esta solução :)
Felix Kling
De acordo com as especificações, existe um evento "pagetransition", parece que ainda não foi implementado.
Mohamed Mansour
2
@ user280109 - eu diria se soubesse. :) Eu acho que não há como fazer isso no caixa eletrônico do Opera.
precisa saber é o seguinte
1
@cprcrack É para manter limpo o escopo global. Você precisa salvar o pushStatemétodo nativo para uso posterior. Então, ao invés de uma variável global que eu escolhi para encapsular todo o código em um IIFE: en.wikipedia.org/wiki/Immediately-invoked_function_expression
gblazex
4

Eu costumava usar isso:

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

É quase o mesmo que os galambalazs fizeram.

Geralmente é um exagero. E pode não funcionar em todos os navegadores. (Só me preocupo com a minha versão do meu navegador.)

(E deixa um var _wr, então você pode querer envolvê-lo ou algo assim. Eu não me importo com isso.)

Rudie
fonte
4

Finalmente encontrei a maneira "correta" de fazer isso! Requer adicionar um privilégio à sua extensão e usar a página de plano de fundo (não apenas um script de conteúdo), mas funciona.

O evento que você deseja é browser.webNavigation.onHistoryStateUpdateddisparado quando uma página usa a historyAPI para alterar o URL. Ele é acionado apenas nos sites aos quais você tem permissão para acessar e também pode usar um filtro de URL para reduzir ainda mais o spam, se necessário. Requer a webNavigationpermissão (e, claro, a permissão de host para os domínios relevantes).

O retorno de chamada do evento obtém o ID da guia, o URL que está sendo "navegado" e outros detalhes. Se você precisar executar uma ação no script de conteúdo dessa página quando o evento for disparado, injete o script relevante diretamente da página de plano de fundo ou faça com que o script de conteúdo abra porta página de plano de fundo quando ela for carregada, salve a página de plano de fundo essa porta em uma coleção indexada pelo ID da guia e envia uma mensagem pela porta relevante (do script em segundo plano para o script de conteúdo) quando o evento é acionado.

CBHacking
fonte
3

Você poderia se associar ao window.onpopstateevento?

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

Dos documentos:

Um manipulador de eventos para o evento popstate na janela.

Um evento popstate é despachado para a janela toda vez que a entrada do histórico ativo é alterada. Se a entrada do histórico que está sendo ativada foi criada por uma chamada para history.pushState () ou foi afetada por uma chamada para history.replaceState (), a propriedade state do evento popstate conterá uma cópia do objeto de estado da entrada history.

stef
fonte
6
Eu tentei isso e já o evento só é acionado quando os usuários remonta na história (ou qualquer um dos history.go, .back) funções são chamadas. Mas não por diante pushState. Aqui está minha tentativa de testá-lo, talvez eu faça algo errado: jsfiddle.net/fkling/vV9vd Parece apenas relacionado dessa maneira que, se o histórico foi alterado pushState, o objeto de estado correspondente é passado para o manipulador de eventos quando os outros métodos são chamados.
Felix Kling
1
Ah Nesse caso, a única coisa em que consigo pensar é registrar um tempo limite para examinar o comprimento da pilha do histórico e disparar um evento se o tamanho da pilha tiver sido alterado.
stef
Ok, esta é uma ideia interessante. A única coisa é que o tempo limite teria que disparar com frequência suficiente para que o usuário não notasse nenhum atraso (longo) (eu tenho que carregar e mostrar dados para o novo URL). Eu sempre tento evitar timeouts e pesquisas sempre que possível, mas até agora essa parece ser a única solução. Ainda vou esperar por outras propostas. Mas muito obrigado por enquanto!
Felix Kling
Pode ser suficiente verificar o tamanho do histórico em cada clique.
Felix Kling
1
Sim - isso funcionaria. Eu estou brincando com isso - um problema é a chamada replaceState que não lançaria um evento porque o tamanho da pilha não teria mudado.
stef
1

Como você está perguntando sobre um complemento do Firefox, eis o código que eu comecei a trabalhar. O uso nãounsafeWindow é mais recomendado e ocorre um erro quando o pushState é chamado a partir de um script do cliente após ser modificado:

Permissão negada para acessar o histórico da propriedade.pushState

Em vez disso, há uma API chamada exportFunction que permite que a função seja injetada da window.historyseguinte maneira:

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});
nathancahill
fonte
Na última linha, você deve mudar unsafeWindow.history,para window.history. Não estava funcionando para mim com o Windows inseguro.
Lindsay-Needs-Sleep
0

A resposta de galambalazs é o remendo do macaco window.history.pushStatee window.history.replaceState, por algum motivo, ele parou de funcionar para mim. Aqui está uma alternativa que não é tão boa porque usa a pesquisa:

(function() {
    var previousState = window.history.state;
    setInterval(function() {
        if (previousState !== window.history.state) {
            previousState = window.history.state;
            myCallback();
        }
    }, 100);
})();
Flimm
fonte
existe uma alternativa para a votação?
SuperUberDuper
@SuperUberDuper: veja as outras respostas.
Flimm
A pesquisa @Flimm não é agradável, mas feia. Mas bem, você não teve escolha, disse.
Yairopro
Isso é muito melhor do que o patch do macaco, o patch do macaco pode ser desfeito por outro script e você não recebe notificações sobre isso.
Ivan Castellanos
0

Eu acho que esse tópico precisa de uma solução mais moderna.

Tenho certeza que nsIWebProgressListenerestava por aí, então estou surpreso que ninguém tenha mencionado.

De um framescript (para compatibilidade com e10s):

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

Então ouvindo no onLoacationChange

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

Aparentemente, isso pegará todos os pushState's. Mas há um comentário avisando que "TAMBÉM dispara para pushState". Portanto, precisamos fazer mais algumas filtragem aqui para garantir que sejam apenas coisas do passado.

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

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

Noitidart
fonte
Esta resposta não tem nada a ver com o comentário, a menos que eu esteja enganado. Os WebProgressListeners são para extensões FF? Esta pergunta é sobre a história do HTML5
Kloar, 25/03
@Kloar permite excluir esses comentários, por favor
Noitidart 26/03
0

Bem, eu vejo muitos exemplos de substituição da pushStatepropriedade, historymas não tenho certeza de que seja uma boa ideia. Prefiro criar um evento de serviço com uma API semelhante ao histórico, para que você possa controlar não apenas o estado push, mas também o estado e também abre portas para muitas outras implementações que não dependem da API de histórico global. Por favor, verifique o seguinte exemplo:

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

Se você precisar da EventEmitterdefinição, o código acima será baseado no emissor do evento NodeJS: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/events.js . utils.inheritsA implementação pode ser encontrada aqui: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970

Victor Queiroz
fonte
Acabei de editar minha resposta com as informações solicitadas, verifique!
Victor Queiroz
@VictorQueiroz Essa é uma idéia agradável e limpa para encapsular as funções. Mas se alguma biblioteca de terceiros solicitar pushState, seu HistoryApi não será notificado.
Yairopro
0

Prefiro não sobrescrever o método de histórico nativo, portanto, essa implementação simples cria minha própria função chamada eventedPush state, que apenas envia um evento e retorna history.pushState (). De qualquer maneira, funciona bem, mas acho essa implementação um pouco mais limpa, pois os métodos nativos continuarão a funcionar como os desenvolvedores futuros esperam.

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"); 
jopfre
fonte
0

Com base na solução fornecida por @gblazex , caso você queira seguir a mesma abordagem, mas usando as funções de seta, siga o exemplo abaixo em sua lógica 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();
});

Em seguida, implemente outra função _notifyUrl(url)que aciona qualquer ação necessária que você possa precisar quando o URL da página atual for atualizado (mesmo que a página ainda não tenha sido carregada)

  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}`);
  }
Alberto S.
fonte
0

Como eu só queria a nova URL, adaptei os códigos de @gblazex e @Alberto S. para obter isso:

(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);
Andre Lopes
fonte
0

Não acho que seja uma boa ideia modificar funções nativas, mesmo que você possa, e você deve sempre manter o escopo do aplicativo, para que uma boa abordagem não seja usar a função pushState global; em vez disso, use uma de sua preferência:

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>

Observe que, se qualquer outro código usar a função pushState nativa, você não obterá um gatilho de evento (mas, se isso acontecer, verifique seu código)

Maxwell sc
fonte
-5

Como estados padrão:

Observe que apenas chamar history.pushState () ou history.replaceState () não acionará um evento popstate. O evento popstate é acionado apenas executando uma ação do navegador, como clicar no botão voltar (ou chamar history.back () em JavaScript)

precisamos chamar history.back () para transformar WindowEventHandlers.onpopstate

Então, insted de:

history.pushState(...)

Faz:

history.pushState(...)
history.pushState(...)
history.back()
user2360102
fonte