Comunicação entre guias usando localStorage

Pode haver casos em que você precise comunicar algo (talvez sincronizar algum estado?) Entre várias guias abertas (no mesmo navegador) de seu aplicativo.

Esta não é uma situação muito comum, mas há um caminho para isso.

Explicação

Quando um valor é gravado na chave localStorage, um evento de armazenamento é disparado em todas as guias (que estão abertas no mesmo domínio), exceto aquela que fez a gravação. Isso pode ser explorado para comunicar mensagens entre guias.

localStorage é um armazenamento de valor-chave. Faz sentido reservar algum nome de chave (por exemplo: “crossTabRelay”) e usá-lo exclusivamente para comunicar mensagens entre guias.

localStorage só pode armazenar strings. Quero ser capaz de retransmitir mensagens mais complicadas, então acho que devemos usar o json (des) seralizer integrado. Antes de escrevermos qualquer coisa, convertemos em uma string json usando JSON.stringifye, quando lemos a mensagem, a convertemos de volta em um objeto usando JSON.parse.

Isso nos permitiria enviar qualquer objeto como uma mensagem, desde que ele possa ser convertido para json.

Então, com isso, é muito simples: para enviar uma mensagem, nós a transformamos em uma string json e a gravamos em localStorage em uma chave específica. Para ler mensagens enviadas de outras guias, ouvimos o evento de armazenamento e, se for disparado para nossa chave específica, é uma mensagem de tabulação cruzada.

Observe que isso é um hack: não acho que localStorage foi projetado com esse caso de uso em mente.

Uma advertência é esta: se você gravar um valor em uma chave localStorage que seja exatamente igual ao valor existente (string) para essa chave em localStorage, nenhum evento será disparado, já que nenhuma alteração foi feita!

Isso não vai servir para mim. Quero poder enviar a mesma mensagem várias vezes seguidas!

Para contornar isso, envolvemos cada mensagem em um objeto de “retransmissão” e colocamos um carimbo de data / hora nele, junto com a mensagem real. Quando recebemos a retransmissão, ignoramos o carimbo de data / hora e apenas extraímos a mensagem dele.

Por fim, quero expor uma API mais simples sobre isso. Quando eu uso a funcionalidade de crosstab, não quero pensar em localStorage ou eventos de armazenamento ou algo parecido. Para este fim, acho útil utilizar uma biblioteca pub / sub. Neste exemplo, estou usando o módulo de eventos da Durandal (documentos)

Também estou definindo isso como um módulo requirejs.

Esta é a API que iremos expor:

Para enviar mensagens:

crosstab.relay(message)

Para ouvir mensagens:

var listener = crosstab.listen(function(message) {
// ... do something with message ...
});

Para desligar um ouvinte específico:

listener.off();

Implementação

define('crosstab', function(require) {
var Events = require('durandal/events');

var crosstab = {}
Events.includeIn(crosstab);

var key = "crossTabRelay";

// listen to storage events
window
.addEventListener('storage', function(e) {
if(e.key == key) {
var relay = JSON.parse(e.newValue);
crosstab
.trigger("relay", relay.message);
}
});

// send a message to all open tabs; including this tab
crosstab
.relay = function(message) {
// wrap the message in a relay object
var relay = {
message
: message,
timestamp
: Date.now()
}

try {
localStorage
[key] = JSON.stringify(relay);
} catch(e) {
console
.error("Tried to cross-tab relay non-json-able message:", message);
throw e;
}

// fire the event locally
crosstab
.trigger("relay", message);
}

crosstab
.listen = function(callback) {
return crosstab.on("relay").then(callback);
}

return crosstab;
});

Observe que isso só funciona para navegadores modernos. O IE tem várias peculiaridades, especialmente o IE8. Existe uma maneira de fazer isso funcionar para o IE (e até mesmo para o IE8, até certo ponto), mas é muito complicado e quero manter isso simples.

Exemplo de uso

Para testar isso sem configurar um projeto baseado em requirejs com durandal e tudo mais, apenas encontre um webapp que já use durandal, abra duas abas dele e abra o console dev em ambas as abas. Por exemplo: http://ninjamock.com/

Em ambas as guias, copie / cole o código acima de dentro do definebloco, excluindo a última returninstrução.

Em seguida, na primeira guia, escute as mensagens:

crosstab.listen(function(message) {
console
.log("Received cross-tab message:", message);
})

Na segunda guia, envie uma mensagem:

crosstab.relay("Hello earth, from outer space");

Agora volte para a primeira guia. Você deve ver no console:

Received cross-tab message: Hello earth, from outer space 

Então aí está!