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:
- campo di input che contiene il messaggio da inviare
- 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.
- 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.
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('
writeMessage
è una funzione globale presente in receiver.html:
function writeMessage(message) { $('#message').append('
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:
E’ l’unica modalità che permette la comunicazione tra tab anche cross-domain. Vediamo come si comporta all’opera:
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('
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('
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 diorigin
, 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.
Pingback: ()