Comunicación entre Ventanas en JavaScript
Aprende la comunicación entre ventanas en JavaScript: postMessage, política de mismo origen, referencias de ventana, eventos localStorage y Broadcast Channel API.
La comunicación entre ventanas es el intercambio de datos entre contextos de navegación separados — una página padre y una ventana emergente que abrió, una página y un iframe incrustado, o dos pestañas del mismo sitio. El navegador aísla deliberadamente estos contextos por seguridad, por lo que no pueden leer libremente las variables o el DOM del otro. En su lugar, JavaScript ofrece un pequeño conjunto de canales bien definidos para transmitir mensajes entre ellos.
Este capítulo cubre cuándo necesitas comunicación entre ventanas, la política de mismo origen que la regula, y cuatro mecanismos prácticos: postMessage(), referencias directas a ventanas, eventos de almacenamiento y la Broadcast Channel API. Se basa en window.open() y ventanas emergentes; si eres nuevo en el modelo del navegador, comienza con la descripción general del entorno del navegador.
Entendiendo la Comunicación entre Ventanas
Un contexto de navegación es cualquier cosa con su propio objeto window: una pestaña, una ventana emergente o un iframe. Dos contextos pueden comunicarse entre sí solo a través de una API controlada, y cuánto se les permite hacer depende de su origen — la combinación de protocolo, host y puerto (por ejemplo, https://www.w3docs.com:443).
La política de mismo origen
La política de mismo origen (SOP) es la regla que decide lo que un contexto puede hacer con otro:
- Mismo origen (protocolo, host y puerto idénticos): los contextos pueden leer el DOM del otro directamente y llamar a
postMessage()libremente. - Origen cruzado (cualquier parte difiere): el acceso directo al DOM está bloqueado. El único canal autorizado es
postMessage(), que el receptor debe validar.
Es por eso que postMessage() es el enfoque recomendado en casi todas partes: funciona de la misma manera tanto si las ventanas comparten un origen como si no, y te obliga a ser explícito sobre en quién confías.
Cuándo lo necesitas
- Ventanas emergentes. Una ventana abierta con
window.open()a menudo necesita enviar resultados de vuelta a la página que la lanzó (una ventana emergente de inicio de sesión OAuth, un selector de archivos). - Iframes. Los widgets incrustados — formularios de pago, mapas, reproductores de terceros — intercambian datos con la página anfitriona.
- Pestañas y otros contextos. Dos pestañas de la misma aplicación pueden necesitar mantenerse sincronizadas (un cierre de sesión en una pestaña debería cerrar la sesión en las demás).
Métodos de Comunicación entre Ventanas
Usando window.postMessage()
El método window.postMessage() es la forma más segura y portable de enviar datos entre ventanas o frames — funciona tanto para contextos del mismo origen como de origen cruzado. El remitente llama a targetWindow.postMessage(data, targetOrigin), y el receptor escucha un evento message.
Dos argumentos son importantes para la seguridad:
targetOrigin(el segundo argumento depostMessage) restringe quién puede recibir el mensaje. Pasa el origen exacto que esperas ('https://example.com'); usa el comodín'*'solo cuando los datos no son sensibles, ya que cualquier ventana en ese destino podrá leerlos.event.origin(en el receptor) te indica quién envió el mensaje. Siempre compruébalo antes de confiar enevent.data— así es como rechazas mensajes de páginas no confiables.
<!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>En este ejemplo combinado, la ventana padre abre una ventana emergente e incrusta un iframe. Tanto la ventana emergente como el iframe pueden comunicarse con la ventana padre usando postMessage(). Los mensajes se muestran dentro de los elementos div correspondientes para una visibilidad clara.
Si bien document.write() funciona para demostraciones simples, las mejores prácticas modernas recomiendan usar DOMParser o URLs de tipo Blob para insertar contenido en ventanas emergentes de forma segura.
Accediendo a Referencias de Ventana
Cuando abres una nueva ventana con window.open(), el valor de retorno es una referencia a esa ventana. La ventana abierta, a su vez, puede acceder a su abridor mediante window.opener, y un padre puede acceder a un iframe a través de iframe.contentWindow. Estas referencias directas funcionan solo cuando ambos contextos comparten el mismo origen — de lo contrario, la SOP lanza un error de seguridad. Úsalas para páginas del mismo origen estrechamente acopladas; recurre a postMessage() siempre que haya un límite de origen.
<!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>En este ejemplo, la ventana padre abre una ventana hija y modifica directamente su contenido una vez que se ha cargado. Además, actualiza el contenido de un iframe incrustado.
La manipulación directa del DOM mediante contentWindow.document o window.opener está restringida por la Política de Mismo Origen (SOP) para contextos de origen cruzado. Para una comunicación segura y confiable, siempre prefiere postMessage(). Para ventanas emergentes del mismo origen, se puede usar window.opener como alternativa para acceder directamente a la ventana padre.
Al usar srcdoc, el contenido del iframe se carga de forma asíncrona. El controlador onload garantiza que el DOM esté listo, pero para escenarios complejos, considera desencadenar la comunicación mediante un evento DOMContentLoaded despachado desde dentro del iframe.
Usando Local Storage y Session Storage
localStorage es compartido por todas las pestañas y ventanas del mismo origen, y escribir en él dispara un evento storage en todos los demás contextos. Esto lo convierte en una forma sencilla de transmitir un cambio entre pestañas sin ninguna referencia directa a la ventana. (sessionStorage es por pestaña y no se propaga, por lo que no es útil para la mensajería entre pestañas.) Para un análisis más profundo de los objetos de almacenamiento, consulta localStorage y 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>En este ejemplo, la ventana padre almacena datos en localStorage y los recupera al hacer clic en los botones. Para habilitar la sincronización entre ventanas, se agrega un detector de eventos storage. Ten en cuenta que el evento storage solo se dispara en otros contextos de navegación, no en el que desencadenó el cambio.
Broadcast Channel API
La Broadcast Channel API es la herramienta diseñada específicamente para la mensajería del mismo origen entre pestañas, ventanas e iframes. Cualquier contexto que abra un canal con el mismo nombre recibe todos los mensajes publicados en él — sin referencias a ventanas ni soluciones alternativas con eventos storage. No puede cruzar orígenes, por lo que para iframes de terceros aún necesitas 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>En este ejemplo, se crea un Broadcast Channel y se envía un mensaje al hacer clic en el botón. El mensaje se recibe y se muestra dentro de un elemento div.
Para probar este ejemplo correctamente:
- Haz clic en el botón 'Try it Yourself' dos veces, para tener la página de ejemplo en dos pestañas diferentes.
- Luego, haz clic en el botón "Send Message" en una de las pestañas/ventanas.
- Deberías ver el mensaje aparecer en la otra pestaña/ventana.
La API BroadcastChannel está diseñada para la comunicación entre pestañas, por lo que el mensaje se enviará desde una pestaña/ventana a todas las demás que estén abiertas en el mismo origen (el mismo archivo HTML en este caso).
Elegir el método correcto
| Método | ¿Origen cruzado? | Ideal para |
|---|---|---|
postMessage() | Sí | La opción predeterminada. Ventanas emergentes e iframes de terceros, en cualquier lugar donde exista un límite de origen. |
| Referencias directas a ventanas | No (solo mismo origen) | Ventanas emergentes/iframes del mismo origen estrechamente acoplados que controlas completamente. |
Evento storage | No (solo mismo origen) | Transmitir cambios de estado a otras pestañas sin API adicional. |
| Broadcast Channel | No (solo mismo origen) | Mensajería limpia de muchos a muchos entre pestañas y frames del mismo origen. |
En caso de duda, usa postMessage() — es el único método que funciona entre orígenes y el único con un modelo de seguridad integrado.
Buenas prácticas
Serializa los datos complejos como JSON. postMessage() usa el algoritmo de clonación estructurada y puede pasar objetos directamente, pero el JSON explícito mantiene el contrato claro y funciona con los eventos storage (que solo transportan cadenas de texto):
const message = { type: 'greeting', content: 'Hello, Child Window!' };
// JSON.stringify produces: {"type":"greeting","content":"Hello, Child Window!"}
childWindow.postMessage(JSON.stringify(message), '*');Maneja los destinos cerrados o inaccesibles. El usuario puede cerrar una ventana emergente, y una ventana de origen cruzado lanzará un error si la tocas directamente. Protege tus llamadas:
if (childWindow && !childWindow.closed) {
try {
childWindow.postMessage('Hello, Child Window!', '*');
} catch (e) {
console.error('Failed to send message:', e);
}
}Siempre valida al remitente. En el lado receptor, comprueba event.origin contra una lista de permitidos antes de actuar sobre event.data, y nunca uses eval() con un mensaje entrante.
Conclusión
La comunicación entre ventanas en JavaScript es una característica poderosa que, cuando se usa correctamente, puede mejorar significativamente la interactividad y la experiencia del usuario en las aplicaciones web. Al emplear métodos como window.postMessage(), el almacenamiento local y la Broadcast Channel API, los desarrolladores pueden gestionar eficientemente el intercambio de datos entre diferentes ventanas, pestañas y frames. Sigue las buenas prácticas para garantizar una comunicación segura y robusta, y aprovecha los ejemplos proporcionados para integrar estas técnicas en tus proyectos.