Comunicazione tra tab del browser – parte 1

Lo sviluppo degli applicativi web è sempre più orientato al versante client: gli utenti richiedono sempre di più che le interfacce web siano sempre più “rich”, con una UX paragonabile a quella degli applicativi client, reattiva al cambiamento dei dati. Lo stato dell’applicazione risiede quindi nel client stesso, mentre il server, stateless, espone le proprie funzionalità come servizi web: secondo questa filosofia, i server possono scalare senza problemi all’aumento del carico, mentre le pagine web di fatto sono diventate dei veri e propri applicativi (non a caso anche con AngularJS spesso si parla spesso di single page webapp). L’architettura dei browser però permette di aprire più istanze contemporanee dello stesso applicativo in diversi tab: spesso quindi può fare comodo tenere allineato lo stato delle varie istanze direttamente lato client, senza che il server se ne preoccupi. Come fare? Esistono quattro tecniche che permettono la comunicazione tra tab di pagine provenienti dallo stesso dominio, vediamo come fare.

Le 4 vie

La comunicazione tra tab è realizzabile in diverse modalità: può trattarsi di pagine diverse di un applicativo o di applicativi web diversi, purché risiedano sullo stesso dominio! Di fatto però, la comunicazione avviene tra oggetti javascript “window“, che rappresentano il contenitore della pagina, indipendentemente dal fatto che siano visibili in un iframe o in un tab. E’ quindi più corretto parlare di comunicazione tra finestre, le quali possono essere una dentro l’altra (iframe) o aperte separatamente in parallelo (tab). Per comodità si parlerà in generale di “finestre”, intendendo quindi sia iframe che tab.
Detto questo, possiamo scegliere le seguenti modalità, ognuna con i propri pro e contro.

accesso diretto
essendo sullo stesso dominio, è possibile accedere al dom di una finestra aperta a partire da un’altra (tab) o creata all’interno di un’altra (iframe). Tutti i dati e le funzioni globali sono accessibili al chiamante e viceversa.
postMessage
è possibile inviare un messaggio (contenente dati) ad una o più finestre, basta registrarsi ad un opportuno evento.
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

Su GitHub, è presente un progetto con una live demo dove è possibile provare nel dettaglio queste modalità. Ognuna è costituita da 2 pagine, sender.html e receiver.html:

sender.html
è formata da 3 parti fondamentali:
  1. campo di input che contiene il messaggio da inviare
  2. area di log che indica l’ordine in cui sono effettuate le operazioni. Si distinguono 3 eventi: prima dell’invio (beforeMessage), ricezione del messaggio dall’altra finestra (received), e dopo l’invio (postMessage). Questo ci permette di capire se la comunicazione è sincrona o asincrona alla all’invio del messaggio.
  3. iframe contenente receiver.html, in modo da vedere in tempo reale cosa viene ricevuto. E’ disponibile anche il bottone “Open Tab”, per aprire receiver.html in un tab separato.
receiver.html
ha un unico blocco contenente i messaggi ricevuti

Accesso Diretto al DOM

Sulla pagina di demo con l’accesso diretto al DOM possiamo provare come mandare i messaggi tra finestre.

direct_access

Come si vede dall’immagine, vengono inviati due messaggi (solo nel secondo caso era stato aperto un tab). Dai log, si capisce che l’invio e la ricezione sono sincroni:

beforeMessage "messaggio 1"
Received from iframe!
afterMessage "messaggio 1"

E non poteva che essere così, visto il frammento di codice di sender.html:

    $(function () {
        var tab;
        $('#send').click(function (e) {
            e.preventDefault();
            var frame = window.frames['result'];
            log('beforeMessage "' + $('#message').val() + '"');
            frame.writeMessage({message: $('#message').val()});
            tab ? tab.writeMessage({message: $('#message').val()}) : void(0);
            log('afterMessage "' + $('#message').val() + '"');
        });
        $('#open-tab').click(function () {
            tab = window.open('receiver.html');
        });
    });
    function log(message) {
        $('#log').append('<li>' + moment().format('YYYY-MM-DD HH:mm:ss.SSS') + ': ' + message + '</li>');
    }

writeMessage è una funzione globale presente in receiver.html:

    function writeMessage(message) {
        $('#message').append('<li>' + moment().format('YYYY-MM-DD HH:mm:ss.SSS') + ': ' + message.message + '</li>');
        if (window.opener) {
            window.opener.log('Received from new tab!');
        }
        if (window.parent != window) {
            window.parent.log('Received from iframe!');
        }
    }

Affinché una finestra possa accedere ad un’altra, è necessario avere il riferimento l’una dell’altra.

Nel sender infatti, la variabile frame punta all’iframe, mentre tab punta alla pagina aperta in un nuovo tab. Senza i riferimenti non è possibile usare questa tecnica: nel caso del tab quindi, il secondo deve essere stato aperto dal primo tramite la chiamata alla funzione window.open.

Il receiver come fa a sapere chi lo ha aperto? Nel caso di nuovo tab, il riferimento al sender è presente nella variabile window.opener; se invece window.parent è diverso da window, significa che siamo dentro un iframe. In entrambe i casi, è possibile chiamare la funzione globale log della pagina sender per notificare la ricezione del messagio.

Pro e contro
Pro
tecnica molto semplice da usare, non ha nessun problema di compatibilità con i browser e la ricezione è sincrona all’invio. Uno scenario in cui può essere usata è quando due webapp collaborano: in questo caso sicuramente una verrà aperta dall’altra o viceversa.
Contro
è necessario che ci sia un riferimento tra le finestre: permette quindi la comunicazione tra tab ma non soddisfa lo scenario in cui si voglia sincronizzare lo stato di due istanze della webapp se un utente apre un tab manualmente. Inoltre, le pagine vengono fortemente accoppiate perché una usa le funzioni globali dell’altra e viceversa.

postMessage

window.postMessage è una funzionalità che esiste addirittura dai tempi di Internet Explorer 8, anche se supportata parzialmente, come ci insegna Can I Use:

Can I Use postMessage

E’ l’unica modalità che permette la comunicazione tra tab anche cross-domain. Vediamo come si comporta all’opera:

postMessage

La prima cosa che colpisce è l’ordine con cui sono effettuate le operazioni:

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

La ricezione del messaggio avviene dopo che il “thread” della funzione del sender è terminata: la comunicazione è quindi asincrona (dall’immagine si capisce meglio anche perché ci sono i millisecondi). Era un comportamento atteso vista la natura da eventi della comunicazione lato sender:

    $(function () {
        var tab;
        $('#send').click(function (e) {
            e.preventDefault();
            var frame = window.frames['result'];
            log('beforeMessage "' + $('#message').val() + '"');
            frame.postMessage({message: $('#message').val()}, '*');
            tab ? tab.postMessage({message: $('#message').val()}, '*') : void(0);
            log('afterMessage "' + $('#message').val() + '"');
        });
        $('#open-tab').click(function () {
            tab = window.open('receiver.html');
        });

        window.addEventListener("message", function (event) {
            log(event.data);
        }, false);

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

Il codice è molto simile al precedente: anche in questo caso è necessario avere il riferimento alla finestra a cui vogliamo mandare il messaggio. Osserviamo meglio però la funzione postMessage, chiamata sul riferimento all’altra finestra, che ha due argomenti:

  • l’oggetto che vogliamo passare all’altra finestra (occhio che in IE8 e 9 si possono passare solo stringhe…)
  • il target origin, ovvero l’indirizzo (protocollo + dominio + porta) del destinatario. A differenza di questo esempio che invia a tutti (usando la wildcard *), è buona norma specificare l’indirizzo del destinatario, per evitare broadcast indesiderati.
  • ci sarebbe anche un terzo parametro opzionale ma è poco supportato

Lato receiver invece si registra un listener sull’evento message:

    window.addEventListener("message", function(event) {
        $('#message').append('<li>' + moment().format('YYYY-MM-DD HH:mm:ss.SSS') + ': ' + event.data.message + '</li>');
        $('#origin').html(event.origin);
        event.source.postMessage('Received "' + event.data.message + '"', event.origin);
    }, false);

La risposta viene inviata nuovamente con un postMessage. L’oggetto event che arriva al listener ha una serie di attributi utili:

  • event.data: contiene il messaggio inviato.
  • event.source: contiene il riferimento alla finestra del chiamante, è quindi possibile rispondere con un altro messaggio.
  • event.origin: è l’indirizzo del mittente. A differenza di questo esempio, è buona norma controllare sempre il valore di origin, in modo da consumare messaggi solo da domini attendibili.
Pro e contro
Pro
copre gli stessi scenari del caso precedente, ma in più permette la comunicazione cross-dominio e disaccoppia le pagine perché ci si basa su eventi e non su chiamate dirette.
Contro
è necessario che ci sia un riferimento tra le finestre: come nel caso precedente, non è possibile sincronizzare lo stato di due istanze della webapp se non si ha il riferimento. Essendo una comunicazione asincrona, non è possibile avere l’immediata certezza che un messaggio sia stato ricevuto dall’altra finestra.

Conclusioni

Queste due modalità permettono quindi la comunicazione tra finestre, con diverse peculiarità tra loro (comunicazione sincrona vs asincrona, comunicazione cross-domain vs stesso dominio). Funzionano molto bene per la comunicazione di webapp aperte in tab diversi in modo programmatico e non dall’utente.

Nel prossimo post vedremo che con le altre due tecniche è possibile ovviare a questo problema.

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+