W3docs

Comunicazione tra finestre in JavaScript

Comunicazione tra finestre in JavaScript: postMessage, same-origin policy, riferimenti a finestre, eventi localStorage e Broadcast Channel API, con esempi eseguibili e best practice di sicurezza.

La comunicazione tra finestre è lo scambio di dati tra contesti di navigazione separati — una pagina genitore e un popup che ha aperto, una pagina e un iframe incorporato, o due schede dello stesso sito. Il browser isola deliberatamente questi contesti per ragioni di sicurezza, impedendo loro di leggere liberamente le variabili o il DOM altrui. JavaScript offre invece un piccolo insieme di canali ben definiti per passare messaggi tra di essi.

Questo capitolo spiega quando è necessaria la comunicazione tra finestre, la same-origin policy che la governa, e quattro meccanismi pratici: postMessage(), riferimenti diretti alle finestre, eventi di storage e la Broadcast Channel API. Si basa su window.open() e i popup; se sei nuovo al modello del browser, inizia con la panoramica dell'ambiente del browser.

Comprendere la comunicazione tra finestre

Un contesto di navigazione è qualsiasi cosa abbia il proprio object window: una scheda, un popup o un iframe. Due contesti possono raggiungersi solo attraverso un'API controllata, e quanto possono fare dipende dalla loro origin — la combinazione di protocollo, host e porta (ad esempio https://www.w3docs.com:443).

La same-origin policy

La same-origin policy (SOP) è la regola che stabilisce cosa può fare un contesto verso un altro:

  • Stessa origin (protocollo, host e porta identici): i contesti possono leggere direttamente il DOM l'uno dell'altro e chiamare liberamente postMessage().
  • Cross-origin (qualsiasi parte differisce): l'accesso diretto al DOM è bloccato. L'unico canale autorizzato è postMessage(), che il ricevitore deve validare.

Ecco perché postMessage() è l'approccio raccomandato quasi ovunque: funziona allo stesso modo sia che le finestre condividano o meno un'origin, e costringe a essere espliciti riguardo a chi ci si fida.

Quando è necessario

  1. Finestre popup. Una finestra aperta con window.open() spesso deve inviare risultati alla pagina che l'ha avviata (un popup di login OAuth, un selettore di file).
  2. Iframe. I widget incorporati — moduli di pagamento, mappe, player di terze parti — scambiano dati con la pagina host.
  3. Schede e altri contesti. Due schede della stessa applicazione potrebbero dover rimanere sincronizzate (un logout in una scheda dovrebbe disconnettere anche le altre).

Metodi di comunicazione tra finestre

Usare window.postMessage()

Il metodo window.postMessage() è il modo più sicuro e portabile per inviare dati tra finestre o frame — funziona sia per contesti same-origin che cross-origin. Il mittente chiama targetWindow.postMessage(data, targetOrigin), e il ricevitore resta in ascolto di un evento message.

Due argomenti sono rilevanti per la sicurezza:

  • targetOrigin (il secondo argomento di postMessage) limita chi può ricevere il messaggio. Passa l'origin esatta che ti aspetti ('https://example.com'); usa il carattere jolly '*' solo quando i dati non sono sensibili, poiché qualsiasi finestra su quel target potrà leggerli.
  • event.origin (sul ricevitore) indica chi ha inviato il messaggio. Verificalo sempre prima di fidarsi di event.data — è così che si rifiutano i messaggi da pagine non attendibili.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Cross-Window Communication</title>
  <style>
    #childIframe, #childPopup {
      width: 100%;
      height: 200px;
      border: 1px solid black;
      margin-top: 20px;
    }
  </style>
</head>
<body>
  <h1>Cross-Window Communication Examples</h1>

  <!-- Button to Open Popup -->
  <button id="openPopup">Open Popup</button>
  <div id="parentPopupDisplay"></div>

  <!-- Iframe -->
  <iframe id="childIframe" srcdoc="
    <!DOCTYPE html>
    <html lang='en'>
    <head>
      <meta charset='UTF-8' />
      <title>Child Iframe</title>
    </head>
    <body>
      <div id='childIframeDisplay'></div>
      <script>
        window.addEventListener('message', (event) => {
          // Note: For cross-origin contexts, replace window.location.origin with the hardcoded parent origin.
          if (event.origin !== window.location.origin) return;
          document.getElementById('childIframeDisplay').innerText = 'Message from parent: ' + event.data;
          event.source.postMessage('Hello, Parent Window!', event.origin);
        });
      </script>
    </body>
    </html>
  "></iframe>
  <div id="iframeDisplay"></div>

  <!-- Scripts for Parent Window -->
  <script>
    // Handle Popup Communication
    document.getElementById('openPopup').addEventListener('click', () => {
      const popup = window.open('', 'popupWindow', 'width=600,height=400');
      popup.document.write(`
        <!DOCTYPE html>
        <html lang='en'>
        <head>
          <meta charset='UTF-8' />
          <title>Popup Window</title>
        </head>
        <body>
          <div id='popupDisplay'></div>
          <script>
            window.addEventListener('message', (event) => {
              // Note: For cross-origin contexts, replace window.location.origin with the hardcoded parent origin.
              if (event.origin !== window.location.origin) return;
              document.getElementById('popupDisplay').innerText = 'Message from parent: ' + event.data;
              event.source.postMessage('Hello, Parent Window!', event.origin);
            });
          <\/script>
        </body>
        </html>
      `);
      setTimeout(() => {
        // For cross-origin, replace '*' with the exact target origin (e.g., 'https://example.com')
        popup.postMessage('Hello from parent!', '*');
      }, 1000);
    });

    // Handle Iframe Communication
    const iframe = document.getElementById('childIframe');

    iframe.onload = () => {
      iframe.contentWindow.postMessage('Hello from parent window!', '*');
    };

    window.addEventListener('message', (event) => {
      if (event.origin !== window.location.origin) return;
      if (event.source === iframe.contentWindow) {
        document.getElementById('iframeDisplay').innerText = 'Message from iframe: ' + event.data;
      } else {
        document.getElementById('parentPopupDisplay').innerText = 'Message from popup: ' + event.data;
      }
    });
  </script>
</body>
</html>

In questo esempio combinato, la finestra genitore apre un popup e incorpora un iframe. Sia il popup che l'iframe possono comunicare con la finestra genitore usando postMessage(). I messaggi vengono visualizzati all'interno dei rispettivi elementi div per una chiara visibilità.

Nota

Sebbene document.write() funzioni per semplici demo, le moderne best practice raccomandano di usare DOMParser o URL Blob per inserire contenuto nei popup in modo sicuro.

Accedere ai riferimenti di finestra

Quando apri una nuova finestra con window.open(), il valore restituito è un riferimento a quella finestra. La finestra aperta, a sua volta, può risalire al suo aprente tramite window.opener, e un genitore può raggiungere un iframe tramite iframe.contentWindow. Questi riferimenti diretti funzionano solo quando entrambi i contesti condividono la stessa origin — in caso contrario la SOP lancia un errore di sicurezza. Usali per pagine same-origin strettamente collegate; ricorri a postMessage() ogni volta che è coinvolto un confine di origin.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Direct Manipulation Example</title>
  <style>
    #childIframe {
      width: 100%;
      height: 200px;
      border: 1px solid black;
      margin-top: 20px;
    }
  </style>
</head>
<body>
  <h1>Direct Manipulation Example</h1>

  <!-- Button to Open Popup -->
  <button id="openChild">Open Child Window</button>
  <div id="parentChildDisplay"></div>

  <!-- Iframe -->
  <iframe id="childIframe" srcdoc="
    <!DOCTYPE html>
    <html lang='en'>
    <head>
      <meta charset='UTF-8' />
      <title>Child Iframe</title>
    </head>
    <body>
      <div id='childIframeContent'>Initial Content</div>
    </body>
    </html>
  "></iframe>

  <!-- Scripts for Parent Window -->
  <script>
    document.getElementById('openChild').addEventListener('click', () => {
      const childWindow = window.open('', 'childWindow', 'width=600,height=400');
      childWindow.document.write(`
        <!DOCTYPE html>
        <html lang='en'>
        <head>
          <meta charset='UTF-8' />
          <title>Child Window</title>
        </head>
        <body>
          <div id='childContent'>Initial Content</div>
        </body>
        </html>
      `);
      // Ensure the content is updated after the window has fully loaded
      setTimeout(() => {
        childWindow.document.body.innerHTML += '<p>Message from parent window</p>';
      }, 1000); // Adjust the timeout duration as necessary
    });

    const iframe = document.getElementById('childIframe');

    iframe.onload = () => {
      const iframeDoc = iframe.contentWindow.document;
      iframeDoc.getElementById('childIframeContent').innerText += ' - Updated by Parent Window';
    };
  </script>
</body>
</html>

In questo esempio, la finestra genitore apre una finestra figlio e ne modifica direttamente il contenuto una volta caricato. Inoltre, aggiorna il contenuto di un iframe incorporato.

Attenzione

La manipolazione diretta del DOM tramite contentWindow.document o window.opener è limitata dalla Same-Origin Policy (SOP) per i contesti cross-origin. Per una comunicazione sicura e affidabile, preferisci sempre postMessage(). Per i popup same-origin, window.opener può essere usato come alternativa per accedere direttamente alla finestra genitore.

Nota

Quando si usa srcdoc, il contenuto dell'iframe viene caricato in modo asincrono. Il gestore onload garantisce che il DOM sia pronto, ma per scenari complessi considera di avviare la comunicazione tramite un evento DOMContentLoaded inviato dall'interno dell'iframe.

Usare Local Storage e Session Storage

localStorage è condiviso da ogni scheda e finestra con la stessa origin, e la scrittura su di esso attiva un evento storage in tutti gli altri contesti. Questo lo rende un modo semplice per trasmettere una modifica tra schede senza alcun riferimento diretto alla finestra. (sessionStorage è per singola scheda e non si propaga, quindi non è utile per la messaggistica tra schede.) Per uno sguardo più approfondito agli oggetti storage, consulta localStorage e sessionStorage.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Local Storage Example</title>
</head>
<body>
  <h1>Local Storage Example</h1>
  <button id="storeData">Store Data</button>
  <button id="retrieveData">Retrieve Data</button>
  <div id="storageDisplay"></div>

  <script>
    // Listen for changes triggered by other windows/tabs
    window.addEventListener('storage', (event) => {
      if (event.key === 'sharedData') {
        document.getElementById('storageDisplay').innerText = 'Updated Data: ' + event.newValue;
      }
    });

    document.getElementById('storeData').addEventListener('click', () => {
      localStorage.setItem('sharedData', 'This is shared data');
    });

    document.getElementById('retrieveData').addEventListener('click', () => {
      const data = localStorage.getItem('sharedData');
      document.getElementById('storageDisplay').innerText = 'Stored Data: ' + data;
    });
  </script>
</body>
</html>

In questo esempio, la finestra genitore memorizza i dati in localStorage e li recupera al clic dei pulsanti. Per abilitare la sincronizzazione tra finestre, viene aggiunto un listener dell'evento storage. Nota che l'evento storage si attiva solo negli altri contesti di navigazione, non in quello che ha scatenato la modifica.

Broadcast Channel API

La Broadcast Channel API è lo strumento appositamente pensato per la messaggistica same-origin tra schede, finestre e iframe. Qualsiasi contesto che apra un canale con lo stesso nome riceve ogni messaggio pubblicato su di esso — senza riferimenti alle finestre e senza workaround tramite eventi storage. Non può attraversare le origin, quindi per gli iframe di terze parti hai ancora bisogno di postMessage().

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Broadcast Channel Example</title>
</head>
<body>
  <h1>Broadcast Channel Example</h1>
  <button id="sendMessage">Send Message</button>
  <div id="broadcastDisplay"></div>

  <script>
    const channel = new BroadcastChannel('example_channel');

    channel.onmessage = (event) => {
      document.getElementById('broadcastDisplay').innerText = 'Broadcast message received: ' + event.data;
    };

    document.getElementById('sendMessage').addEventListener('click', () => {
      channel.postMessage('Hello from another context!');
    });
  </script>
</body>
</html>

In questo esempio, viene creato un Broadcast Channel e viene inviato un messaggio quando si fa clic sul pulsante. Il messaggio viene ricevuto e visualizzato all'interno di un elemento div.

Per testare correttamente questo esempio:

  1. Fai clic sul pulsante 'Try it Yourself' due volte, per avere la pagina dell'esempio in due schede diverse.
  2. Poi fai clic sul pulsante "Send Message" in una delle schede/finestre.
  3. Dovresti vedere il messaggio apparire nell'altra scheda/finestra.

La BroadcastChannel API è progettata per la comunicazione inter-scheda, quindi il messaggio verrà inviato da una scheda/finestra a tutte le altre aperte sulla stessa origin (lo stesso file HTML in questo caso).

Scegliere il metodo giusto

MetodoCross-origin?Ideale per
postMessage()Il metodo predefinito. Popup e iframe di terze parti, ovunque esista un confine di origin.
Riferimenti diretti alle finestreNo (solo same-origin)Popup/iframe same-origin strettamente collegati che controlli completamente.
Evento storageNo (solo same-origin)Trasmettere cambiamenti di stato ad altre schede senza API aggiuntive.
Broadcast ChannelNo (solo same-origin)Messaggistica many-to-many pulita tra schede e frame della stessa origin.

In caso di dubbio, usa postMessage() — è l'unico metodo che funziona tra origin diverse e l'unico con un modello di sicurezza integrato.

Best practice

Serializza i dati complessi come JSON. postMessage() usa l'algoritmo di clonazione strutturata e può passare oggetti direttamente, ma un JSON esplicito rende il contratto chiaro e funziona con gli eventi storage (che trasportano solo stringhe):

const message = { type: 'greeting', content: 'Hello, Child Window!' };
// JSON.stringify produces: {"type":"greeting","content":"Hello, Child Window!"}
childWindow.postMessage(JSON.stringify(message), '*');

Gestisci i target chiusi o inaccessibili. Un popup potrebbe essere chiuso dall'utente, e una finestra cross-origin genererà un errore se la tocchi direttamente. Proteggi le tue chiamate:

if (childWindow && !childWindow.closed) {
  try {
    childWindow.postMessage('Hello, Child Window!', '*');
  } catch (e) {
    console.error('Failed to send message:', e);
  }
}

Valida sempre il mittente. Sul lato ricevente, controlla event.origin rispetto a una lista di elementi consentiti prima di agire su event.data, e non eseguire mai eval() su un messaggio in arrivo.

Conclusione

La comunicazione tra finestre in JavaScript è una funzionalità potente che, se usata correttamente, può migliorare significativamente l'interattività e l'esperienza utente delle applicazioni web. Utilizzando metodi come window.postMessage(), il local storage e la Broadcast Channel API, gli sviluppatori possono gestire in modo efficiente lo scambio di dati tra finestre, schede e frame diversi. Segui le best practice per garantire una comunicazione sicura e robusta, e sfrutta gli esempi forniti per integrare queste tecniche nei tuoi progetti.

Esercizi

Pratica
Cosa è vero riguardo alla comunicazione tra finestre in JavaScript?
Cosa è vero riguardo alla comunicazione tra finestre in JavaScript?
Was this page helpful?