Comunicazione tra tab del browser – parte 2

Nel post precedente abbiamo visto due modalità che ci permettono di comunicare tra tab del browser, uno a parità di dominio e l’altro no. Entrambi i metodi avevano però il limite di dover avere il riferimento del tab aperto, cosa non sempre possibile.
Vediamo adesso come è possibile aggirare questo problema.

Le 4 vie – reprise

Le modalità viste nello scorso post erano basate sull’accesso diretto al DOM della nuova finestra, o la comunicazione tramite postMessage. Riprendiamo adesso le ultime due:

localStorage
ogni qualvolta il local storage relativo ad un certo dominio viene modificato in una certa finestra, qualsiasi altra finestra aperta su quel dominio viene notificata.
cookie
i cookie sono legati ad un dominio (o ad un suo path): è possibile quindi leggere e/o scrivere cookie anche da finestre diverse

localStorage

Cos’è il Local Storage? HTML5 ha introdotto il concetto di Web Storage, ovvero un contenitore di tipo chiave-valore dove poter salvare dei dati all’interno del browser. A differenza dei cookie, supportati sin dall’alba dei tempi (o quasi), gli webstorage sono più capienti e soprattutto le informazioni contenute non vengono mai inviate al server. Ci sono solo due vincoli: il limite di spazio disponibile (fino a 5MB) e il tipo di dato supportato. Si possono salvare infatti solo coppie di stringhe, ma niente vieta di serializzare un oggetto JSON e salvarlo.

Gli Web Storage si dividono in due categorie:

  • localStorage: può contenere dati, senza scadenza, accessibili da tutte le pagine di uno stesso dominio.
  • sessionStorage: può contenere dati relativi alla sessione utente, ovvero isolati nel tab in cui vi si fa accesso. Vengono cancellati alla chiusura del tab stesso.

Quindi solo il primo fa al caso nostro, perché persiste tra più finestre e nel tempo. Problemi di compatibilità tra browser? Ci viene in aiuto nuovamente Can I Use:

Can I Use localStorage

Tornando alla live demo su GitHub dedicata al localStorage, ecco quello che succede:

localStorage

Come si vede dall’immagine (o come potete provare), l’ordine temporale della comunicazione è il seguente:

beforeMessage "messaggio 1"
afterMessage "messaggio 1"
Received "messaggio 1"

Come nel caso del postMessage, anche in questo caso la comunicazione tra le finestre è asincrona, essendo basata su eventi. Dall’immagine si comprende bene la sequenza temporale. Ma cosa è successo? Per definizione, ogni volta che il localStorage viene modificato, altre finestre aperte su quel dominio (e quindi su quello storage) vengono notificate con l’evento storage: basta quindi avere un listener su questo evento ed è possibile effettuare una comunicazione bidirezionale senza bisogno di riferimenti tra finestre, perché l’anello di congiunzione è lo storage stesso.

Vediamo il codice del sender.html:

    $(function () {
        $('#send').click(function (e) {
            e.preventDefault();
            log('beforeMessage "' + $('#message').val() + '"');
            localStorage.setItem('new-item', $('#message').val()); //no object
            log('afterMessage "' + $('#message').val() + '"');
        });
        $('#open-tab').click(function () {
            window.open('receiver.html');
        });

        window.addEventListener("storage", function (event) {
            log('Received! ' + event.oldValue);
        }, false);

        function log(message) {
            $('#log').append('<li>' + moment().format('YYYY-MM-DD HH:mm:ss.SSS') + ': ' + message + '</li>');
        }
    });

Tramite la funzione localStorage.setItem è possibile salvare la coppia chiave-valore nello storage (da notare che il valore è di tipo stringa). Il listener sull’evento storage sta in ascolto dell risposta di conferma dall’altra finestra e riceve un oggetto StorageEvent di cui tre attributi sono molto importanti:

  • event.key: contiene la chiave della coppia aggiunta/modificata
  • event.newValue: contiene il nuovo valore aggiunto/modificato
  • event.oldValue: contiene il valore prima della modifica (o null se non c’era)

Lato receiver.html invece:

window.addEventListener("storage", function(event) {
    $('#message').append('<li>' + moment().format('YYYY-MM-DD HH:mm:ss.SSS') + ': ' + event.newValue + ', key: ' + event.key + '</li>');
    if (localStorage.getItem(event.key)) {
        localStorage.removeItem(event.key); //trigger per la notifica al mittente
    }
}, true);

In questo caso, per notificare che il messaggio è stato ricevuto, viene rimosso dallo storage (altrimenti vi rimarrebbe in eterno). Se ovviamente avete più di due finestre receiver aperte solo una riceverà il messaggio perché il primo che lo riceve lo rimuove dallo storage.

Pro e Contro
Pro
E’ possibile usare questa tecnica da IE8 in poi, quindi non ci sono grossi problemi di compatibilità. Permette di disaccoppiare le finestre perché la comunicazione è basata su eventi, e soprattutto non è necessario avere un riferimento reciproco
Contro
Alcuni browser in modalità privata non supportano la scrittura sugli web storage, quindi questa tecnica non funziona.

Cookie

L’ultima tecnica è quella che utilizza i cari e vecchi cookie: li conosciamo bene, sono quei biscottini legati ad un dominio (o ad un suo sotto percorso) che rappresentano una coppia chiave-valore con una data scadenza. Spesso usati per identificare una sessione web lato server, in realtà possono essere usati per una miriade di scopi, anche per comunicare tra finestre dello stesso dominio!

Non essendo però associato nessun evento alla lettura scrittura dei cookies, l’unica possibilità che abbiamo per intercettare modifiche ai cookie è il polling.

Facendo subito una prova con la live demo dei cookie, ecco cosa succede:

cookie

La ricezione del messaggio non può che essere asincrona all’invio per via del polling. Vediamo una parte del codice di sender.html:

        $('#send').click(function (e) {
            e.preventDefault();

            log('beforeMessage "' + $('#message').val() + '"');
            Cookies.set('message', {message: $('#message').val()});
            log('afterMessage "' + $('#message').val() + '"');

            var polling = setInterval(function() {
                console.log('Polling for reply');
                var cookie = Cookies.get('reply');
                if (cookie) {
                    Cookies.remove('reply');
                    log(cookie);
                    clearInterval(polling);
                }
            }, 1000);
        });

con l’aiuto di Javascript Cookie, la gestione dei cookies è più semplice: possiamo anche passare oggetti JSON che vengono automaticamente serializzati in stringhe, anche se dobbiamo stare attenti alle dimensioni. Solitamente i browser non supportano più di 4Kb: potete comunque testare i limiti del vostro browser preferito su browsercookielimits.

Una volta inviato il messaggio, si attiva un polling che attende la risposta (nel cookie reply).

Dall’altro lato, la pagina receiver.html deve avere sempre un polling attivo:

    setInterval(function() {
        console.log('Polling for message');
        var cookie = Cookies.get('message');
        if (cookie) {
            Cookies.remove('message');
            $('#message').append('<li>' + moment().format('YYYY-MM-DD HH:mm:ss.SSS') + ': ' + cookie.message + '</li>');
            Cookies.set('reply', 'Received ' + cookie.message);
        }
    }, 1000);

Per evitare loop è bene rimuovere il cookie una volta letto, quindi se si hanno più di due finestre, come nel caso precedente, ce ne sarà solo una che riceve il messaggio.

Pro e Contro
Pro
Si tratta di una tecnica concettualmente molto semplice e che sicuramente non ha problemi di compatibilità con nessun browser.
Contro
Il polling continuo lato “ricevente” non è che sia proprio performante… inoltre le dimensioni del messaggio sono limitate a 4Kb.

Conclusioni

Abbiamo visto quindi pro e contro di queste tipologie di approcci. Tirando le somme, non esiste il “silver bullet”, dipende da cosa vogliamo implementare. Se abbiamo controllo sui tab che vengono aperti o abbiamo a che fare con iframe, a mio avviso la soluzione del postMessage risulta quella più flessibile, soprattutto perché può essere usata anche cross domain. In alternativa, se sono sullo stesso dominio, mi sentirei tranquillo ad usare la strada del localStorage perché largamente supportato, ovviando così al problema del riferimento tra finestre. Per quanto riguarda le altre due soluzioni: quella con l’accesso diretto al DOM è efficace, ma accoppia il codice delle pagine; quella dei invece cookie mi sento onestamente di sconsigliarla per via del polling e della limitatezza delle dimensioni del messaggio.

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio curriculum pubblico. Follow me on Twitter - LinkedIn profile - Google+