Table of Contents
Communication entre fenêtres : utiliser .postMessage() et WeakMap
Introduction
Dans le monde du developpement web moderne, la communication entre differents contextes d’execution JavaScript est devenue une necessite incontournable. Que ce soit pour integrer des widgets tiers, communiquer avec des iframes, orchestrer des Web Workers, ou gerer des Service Workers, les developpeurs doivent maitriser les mecanismes de communication inter-contextes.
La Same-Origin Policy (SOP) est une mesure de securite fondamentale des navigateurs qui empeche les scripts d’un origin (combinaison protocole + domaine + port) d’acceder aux ressources d’un autre origin. Cette politique protege les utilisateurs contre les attaques de type Cross-Site Scripting (XSS) et Cross-Site Request Forgery (CSRF).
Cependant, cette restriction pose un defi majeur : comment permettre a des applications legitimes de communiquer entre elles de maniere securisee ? C’est la que .postMessage() entre en jeu, offrant un canal de communication securise et standardise.
Parallelement, la gestion de la memoire en JavaScript est un sujet crucial, particulierement dans les applications Single Page (SPA) qui tournent pendant de longues periodes. Les WeakMap et WeakSet offrent des solutions elegantes pour associer des donnees a des objets sans creer de fuites memoire.
Dans cet article approfondi, nous explorerons :
- La methode
postMessage()dans tous ses contextes d’utilisation - Les bonnes pratiques de securite pour la communication cross-origin
- L’utilisation avancee des
WeakMapetWeakSet - Les patterns de conception pour eviter les fuites memoire
Utiliser .postMessage() - Guide Complet
La methode .postMessage() est le mecanisme standard pour la communication asynchrone entre differents contextes d’execution JavaScript. Elle respecte la Same-Origin Policy tout en permettant une communication controlee entre origins differents.
Syntaxe complete
La signature complete de postMessage() est la suivante :
targetWindow.postMessage(message, targetOrigin, [transfer]);
Parametres :
| Parametre | Type | Description |
|---|---|---|
message | any | Le message a envoyer. Sera clone via l’algorithme de clonage structure |
targetOrigin | string | L’origin cible. Utilisez "*" pour n’importe quel origin (deconseille) ou une URL specifique |
transfer | array | (Optionnel) Tableau d’objets Transferable a transferer plutot qu’a cloner |
Communication window vers iframe
Le cas d’utilisation le plus courant est la communication entre une page parente et une iframe. Voici un exemple complet :
Page parente (parent.html) :
// Obtenir une reference a l'iframe
const iframe = document.getElementById('myIframe');
// Attendre que l'iframe soit chargee
iframe.addEventListener('load', () => {
// Creer un message structure
const message = {
type: 'INIT',
payload: {
userId: 12345,
theme: 'dark',
language: 'fr'
},
timestamp: Date.now()
};
// Envoyer le message a l'iframe
// IMPORTANT: Specifier l'origin exact pour la securite
iframe.contentWindow.postMessage(message, 'https://widget.example.com');
});
// Ecouter les reponses de l'iframe
window.addEventListener('message', (event) => {
// CRUCIAL: Toujours valider l'origine
if (event.origin !== 'https://widget.example.com') {
console.warn('Message rejete - origin non autorise:', event.origin);
return;
}
// Traiter le message
const { type, payload } = event.data;
switch (type) {
case 'READY':
console.log('Widget pret');
break;
case 'USER_ACTION':
handleUserAction(payload);
break;
case 'ERROR':
console.error('Erreur widget:', payload.message);
break;
default:
console.warn('Type de message inconnu:', type);
}
});
Iframe (widget.html) :
// Signaler que le widget est pret
window.parent.postMessage(
{ type: 'READY', timestamp: Date.now() },
'https://parent.example.com'
);
// Ecouter les messages du parent
window.addEventListener('message', (event) => {
// Valider l'origine
if (event.origin !== 'https://parent.example.com') {
return;
}
const { type, payload } = event.data;
if (type === 'INIT') {
// Initialiser le widget avec les donnees recues
initWidget(payload);
}
});
function initWidget(config) {
document.body.classList.add(`theme-${config.theme}`);
setLanguage(config.language);
loadUserData(config.userId);
}
Communication avec les Web Workers
Les Web Workers permettent d’executer du code JavaScript dans un thread separe. postMessage() est le seul moyen de communiquer avec eux :
Thread principal :
// Creer un Worker
const worker = new Worker('worker.js');
// Envoyer des donnees au Worker
worker.postMessage({
action: 'PROCESS_DATA',
data: largeDataArray
});
// Transferer un ArrayBuffer (zero-copy)
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
worker.postMessage(
{ action: 'PROCESS_BUFFER', buffer },
[buffer] // Le buffer est transfere, pas clone
);
// Attention: buffer n'est plus utilisable ici apres le transfert
// Recevoir les resultats
worker.addEventListener('message', (event) => {
const { action, result, error } = event.data;
if (error) {
console.error('Erreur Worker:', error);
return;
}
switch (action) {
case 'PROCESS_COMPLETE':
updateUI(result);
break;
case 'PROGRESS':
updateProgressBar(result.percentage);
break;
}
});
// Gerer les erreurs du Worker
worker.addEventListener('error', (error) => {
console.error('Erreur fatale Worker:', error.message);
});
worker.js :
// Ecouter les messages du thread principal
self.addEventListener('message', (event) => {
const { action, data, buffer } = event.data;
try {
switch (action) {
case 'PROCESS_DATA':
// Traitement intensif sans bloquer l'UI
const result = heavyComputation(data);
self.postMessage({ action: 'PROCESS_COMPLETE', result });
break;
case 'PROCESS_BUFFER':
// Traiter le buffer transfere
const view = new Uint8Array(buffer);
processBuffer(view);
// Renvoyer le buffer au thread principal
self.postMessage(
{ action: 'PROCESS_COMPLETE', buffer },
[buffer]
);
break;
}
} catch (error) {
self.postMessage({
action: 'ERROR',
error: error.message
});
}
});
function heavyComputation(data) {
// Simuler un traitement lourd
let result = 0;
for (let i = 0; i < data.length; i++) {
result += Math.sqrt(data[i]);
// Envoyer la progression tous les 10%
if (i % (data.length / 10) === 0) {
self.postMessage({
action: 'PROGRESS',
result: { percentage: (i / data.length) * 100 }
});
}
}
return result;
}
Communication avec les Service Workers
Les Service Workers sont utilises pour le caching, les notifications push et le fonctionnement hors-ligne. La communication avec eux differe legerement :
// Enregistrer le Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('SW enregistre:', registration.scope);
});
}
// Envoyer un message au Service Worker actif
function sendToServiceWorker(message) {
return new Promise((resolve, reject) => {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event) => {
if (event.data.error) {
reject(event.data.error);
} else {
resolve(event.data);
}
};
navigator.serviceWorker.controller.postMessage(message, [messageChannel.port2]);
});
}
// Utilisation
async function clearCache() {
const response = await sendToServiceWorker({
action: 'CLEAR_CACHE',
cacheName: 'api-cache-v1'
});
console.log('Cache efface:', response.success);
}
// Ecouter les messages du Service Worker
navigator.serviceWorker.addEventListener('message', (event) => {
const { type, payload } = event.data;
if (type === 'CACHE_UPDATED') {
// Rafraichir les donnees affichees
refreshData();
}
});
sw.js (Service Worker) :
self.addEventListener('message', (event) => {
const { action, cacheName } = event.data;
if (action === 'CLEAR_CACHE') {
caches.delete(cacheName)
.then(() => {
// Repondre via le port
event.ports[0].postMessage({ success: true });
})
.catch((error) => {
event.ports[0].postMessage({ error: error.message });
});
}
});
// Notifier les clients d'une mise a jour
async function notifyClients(message) {
const clients = await self.clients.matchAll();
clients.forEach(client => {
client.postMessage(message);
});
}
L’algorithme de clonage structure (Structured Clone)
Contrairement a JSON.stringify(), l’algorithme de clonage structure peut serialiser :
// Types supportes par le clonage structure (mais pas JSON)
const complexData = {
date: new Date(),
regex: /pattern/gi,
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
arrayBuffer: new ArrayBuffer(8),
blob: new Blob(['contenu']),
file: fileInput.files[0],
imageData: ctx.getImageData(0, 0, 100, 100),
error: new Error('Test error')
};
// Ces objets seront correctement clones
worker.postMessage(complexData);
// ATTENTION: Ces types ne sont PAS supportes
const unsupported = {
func: () => {}, // Fonctions
symbol: Symbol('test'), // Symbols
dom: document.body, // Noeuds DOM
weakMap: new WeakMap(), // WeakMap
weakSet: new WeakSet() // WeakSet
};
// worker.postMessage(unsupported); // ERREUR!
Validation de l’origine - Securite critique
La validation de l’origine est ESSENTIELLE pour la securite. Voici les bonnes pratiques :
// Liste blanche des origines autorisees
const ALLOWED_ORIGINS = [
'https://trusted-partner.com',
'https://widget.myapp.com',
'https://api.myapp.com'
];
// En developpement, ajouter localhost
if (process.env.NODE_ENV === 'development') {
ALLOWED_ORIGINS.push('http://localhost:3000');
}
window.addEventListener('message', (event) => {
// Methode 1: Verification stricte
if (!ALLOWED_ORIGINS.includes(event.origin)) {
console.warn(`Origin bloque: ${event.origin}`);
return;
}
// Methode 2: Verification par pattern (pour sous-domaines)
const trustedPattern = /^https:\/\/[\w-]+\.myapp\.com$/;
if (!trustedPattern.test(event.origin)) {
return;
}
// Validation du format du message
if (!isValidMessage(event.data)) {
console.warn('Format de message invalide');
return;
}
// Traitement securise
processMessage(event.data, event.source);
});
function isValidMessage(data) {
return (
typeof data === 'object' &&
data !== null &&
typeof data.type === 'string' &&
data.type.length <= 50
);
}
Utiliser WeakMap - Guide Complet
Une WeakMap est une collection de paires cle-valeur ou les cles doivent etre des objets et sont referencees “faiblement”. Cela signifie que si aucune autre reference a l’objet cle n’existe, celui-ci peut etre garbage-collecte.
Differences fondamentales avec Map
// Comparaison Map vs WeakMap
const map = new Map();
const weakMap = new WeakMap();
let obj = { id: 1 };
map.set(obj, 'valeur map');
weakMap.set(obj, 'valeur weakmap');
// La Map maintient une reference forte
// La WeakMap maintient une reference faible
obj = null; // On supprime la reference
// Apres garbage collection:
// - map contient toujours l'entree (fuite memoire potentielle)
// - weakMap a perdu l'entree automatiquement
// Differences de fonctionnalites
console.log(map.size); // 1 (meme apres obj = null)
// console.log(weakMap.size); // ERREUR: size n'existe pas
// Iteration possible sur Map
for (const [key, value] of map) {
console.log(key, value);
}
// Iteration IMPOSSIBLE sur WeakMap
// for (const [key, value] of weakMap) {} // ERREUR!
| Caracteristique | Map | WeakMap |
|---|---|---|
| Types de cles | Tout type | Objets uniquement |
| References | Fortes | Faibles |
| Iteration | Oui (forEach, for…of) | Non |
| Propriete size | Oui | Non |
| Methode clear() | Oui | Non |
| Garbage collection | Manuel | Automatique |
Cas d’usage 1 : Donnees privees
Les WeakMap permettent de simuler des proprietes privees sans fuites memoire :
// Pattern des donnees privees avec WeakMap
const privateData = new WeakMap();
class User {
constructor(name, email, password) {
// Donnees publiques
this.name = name;
this.email = email;
// Donnees privees (non accessibles de l'exterieur)
privateData.set(this, {
password: hashPassword(password),
createdAt: new Date(),
loginAttempts: 0,
lastLogin: null
});
}
checkPassword(password) {
const data = privateData.get(this);
const isValid = verifyHash(password, data.password);
if (!isValid) {
data.loginAttempts++;
if (data.loginAttempts >= 5) {
throw new Error('Compte bloque');
}
} else {
data.loginAttempts = 0;
data.lastLogin = new Date();
}
return isValid;
}
getAccountInfo() {
const data = privateData.get(this);
return {
name: this.name,
email: this.email,
memberSince: data.createdAt,
lastLogin: data.lastLogin
// Note: password n'est jamais expose
};
}
}
const user = new User('Alice', 'alice@example.com', 'secret123');
console.log(user.password); // undefined (non accessible)
console.log(privateData.get(user)); // Accessible seulement en interne
// Quand user n'est plus reference, les donnees privees
// sont automatiquement garbage-collectees
Cas d’usage 2 : Cache avec garbage collection automatique
// Cache qui ne bloque pas le garbage collection
const computationCache = new WeakMap();
function expensiveComputation(obj) {
// Verifier le cache
if (computationCache.has(obj)) {
console.log('Cache hit!');
return computationCache.get(obj);
}
console.log('Computing...');
// Simulation d'un calcul couteux
const result = {
hash: calculateHash(obj),
analysis: deepAnalyze(obj),
timestamp: Date.now()
};
// Stocker en cache
computationCache.set(obj, result);
return result;
}
// Utilisation
let data = { values: [1, 2, 3, 4, 5] };
expensiveComputation(data); // Computing...
expensiveComputation(data); // Cache hit!
// Quand data n'est plus utilise:
data = null;
// Le cache est automatiquement nettoye par le GC
// Pas besoin de gestion manuelle!
Cas d’usage 3 : Metadonnees sur objets DOM
// Associer des metadonnees aux elements DOM sans polluer l'element
const elementMetadata = new WeakMap();
function trackElement(element, metadata) {
const existing = elementMetadata.get(element) || {};
elementMetadata.set(element, { ...existing, ...metadata });
}
function getElementMetadata(element) {
return elementMetadata.get(element);
}
// Exemple: Tracking d'interactions
document.querySelectorAll('.trackable').forEach(element => {
trackElement(element, {
createdAt: Date.now(),
clicks: 0,
hovers: 0
});
element.addEventListener('click', () => {
const meta = getElementMetadata(element);
meta.clicks++;
meta.lastClick = Date.now();
});
element.addEventListener('mouseenter', () => {
const meta = getElementMetadata(element);
meta.hovers++;
});
});
// Quand un element est retire du DOM et n'a plus de references,
// ses metadonnees sont automatiquement nettoyees
Cas d’usage 4 : Memoization d’objets
// Memoization sans fuite memoire
function createMemoizedRenderer() {
const cache = new WeakMap();
return function render(component) {
if (cache.has(component)) {
const cached = cache.get(component);
if (cached.props === component.props) {
return cached.result;
}
}
const result = renderComponent(component);
cache.set(component, {
props: component.props,
result
});
return result;
};
}
const memoizedRender = createMemoizedRenderer();
Utiliser WeakSet
Un WeakSet est similaire a un Set, mais ne contient que des objets et utilise des references faibles.
Syntaxe de base
const weakSet = new WeakSet();
let obj1 = { id: 1 };
let obj2 = { id: 2 };
weakSet.add(obj1);
weakSet.add(obj2);
console.log(weakSet.has(obj1)); // true
console.log(weakSet.has({ id: 1 })); // false (objet different)
weakSet.delete(obj1);
console.log(weakSet.has(obj1)); // false
// Pas d'iteration possible
// weakSet.forEach(...); // ERREUR
// for (const item of weakSet) {} // ERREUR
// console.log(weakSet.size); // ERREUR
Cas d’usage : Tracking d’objets visites
// Detecter les references circulaires
function detectCircular(obj, visited = new WeakSet()) {
if (typeof obj !== 'object' || obj === null) {
return false;
}
if (visited.has(obj)) {
return true; // Reference circulaire detectee!
}
visited.add(obj);
for (const key in obj) {
if (detectCircular(obj[key], visited)) {
return true;
}
}
return false;
}
// Test
const normal = { a: { b: { c: 1 } } };
const circular = { a: { b: null } };
circular.a.b = circular; // Reference circulaire
console.log(detectCircular(normal)); // false
console.log(detectCircular(circular)); // true
Cas d’usage : Marquer des objets comme traites
// Systeme de traitement qui evite les doublons
class EventProcessor {
constructor() {
this.processed = new WeakSet();
}
process(event) {
if (this.processed.has(event)) {
console.log('Event deja traite, ignore');
return;
}
// Traiter l'event
this.handleEvent(event);
// Marquer comme traite
this.processed.add(event);
}
handleEvent(event) {
console.log('Traitement:', event.type);
}
}
const processor = new EventProcessor();
const event = { type: 'click', target: 'button' };
processor.process(event); // Traitement: click
processor.process(event); // Event deja traite, ignore
Cas d’usage : Validation d’objets
// Valider que des objets proviennent d'une source fiable
const trustedObjects = new WeakSet();
function createTrustedObject(data) {
const obj = { ...data, _verified: true };
trustedObjects.add(obj);
return obj;
}
function isTrusted(obj) {
return trustedObjects.has(obj);
}
function processSecurely(obj) {
if (!isTrusted(obj)) {
throw new Error('Objet non verifie!');
}
// Traitement securise...
}
const trusted = createTrustedObject({ userId: 1 });
const untrusted = { userId: 1, _verified: true };
processSecurely(trusted); // OK
// processSecurely(untrusted); // ERREUR: Objet non verifie!
Bonnes Pratiques
Pour postMessage()
- Toujours valider l’origine : Ne jamais utiliser
"*"comme targetOrigin en production
// MAL
otherWindow.postMessage(data, '*');
// BIEN
otherWindow.postMessage(data, 'https://trusted-domain.com');
- Valider le format des messages : Ne jamais faire confiance aux donnees recues
window.addEventListener('message', (event) => {
// Valider l'origine
if (event.origin !== EXPECTED_ORIGIN) return;
// Valider le type
if (typeof event.data !== 'object') return;
if (!event.data.type) return;
// Valider le contenu selon le type
switch (event.data.type) {
case 'UPDATE_USER':
if (!isValidUserId(event.data.userId)) return;
break;
}
// Traiter le message
});
- Utiliser des Transferables pour les gros volumes : Eviter le clonage couteux
// Transferer au lieu de cloner
const buffer = new ArrayBuffer(10 * 1024 * 1024); // 10MB
worker.postMessage({ buffer }, [buffer]); // Zero-copy transfer
- Implementer un protocole de communication : Messages types et versiones
const message = {
version: '1.0',
type: 'ACTION_NAME',
payload: { /* ... */ },
timestamp: Date.now(),
correlationId: generateUUID()
};
Pour WeakMap et WeakSet
-
Utiliser pour les metadonnees : Associer des donnees a des objets sans modifier ces objets
-
Preferer WeakMap pour le caching : Le cache se nettoie automatiquement
-
Eviter pour les donnees persistantes : Les entrees peuvent disparaitre a tout moment
-
Ne pas compter sur l’enumeration : Impossible d’iterer ou de connaitre la taille
Pieges Courants
Piege 1 : Utiliser "*" comme targetOrigin
// DANGER: N'importe qui peut intercepter
window.parent.postMessage(sensitiveData, '*');
// Solution: Specifier l'origine exacte
window.parent.postMessage(sensitiveData, 'https://parent.myapp.com');
Piege 2 : Oublier de valider event.origin
// VULNERABLE
window.addEventListener('message', (event) => {
document.body.innerHTML = event.data.html; // XSS!
});
// SECURISE
window.addEventListener('message', (event) => {
if (event.origin !== TRUSTED_ORIGIN) return;
// Traiter le message de facon securisee
});
Piege 3 : Cles primitives dans WeakMap
const wm = new WeakMap();
// ERREUR: Les primitives ne sont pas autorisees
wm.set('string', 'value'); // TypeError
wm.set(123, 'value'); // TypeError
wm.set(Symbol('sym'), 'value'); // TypeError
// CORRECT: Utiliser des objets
wm.set({ key: 'string' }, 'value');
Piege 4 : Attendre un comportement synchrone du GC
const wm = new WeakMap();
let obj = { data: 'test' };
wm.set(obj, 'metadata');
obj = null;
// FAUX: L'entree n'est pas immediatement supprimee
console.log(wm.has(obj)); // Peut etre true ou false!
// Le GC est non deterministe, ne jamais compter
// sur un timing precis
Piege 5 : Confondre clonage et transfert
const buffer = new ArrayBuffer(1024);
const view = new Uint8Array(buffer);
view[0] = 42;
// Apres transfert, le buffer original est "detache"
worker.postMessage({ buffer }, [buffer]);
console.log(buffer.byteLength); // 0! Le buffer est vide
console.log(view[0]); // TypeError ou 0
Conclusion
La maitrise de postMessage(), WeakMap et WeakSet est essentielle pour tout developpeur JavaScript moderne. Ces outils permettent de :
- Communiquer de facon securisee entre differents contextes (iframes, Workers, Service Workers)
- Gerer la memoire efficacement sans fuites
- Associer des metadonnees a des objets sans les modifier
- Implementer des patterns avances comme le caching intelligent ou les donnees privees
Tableau Comparatif : Map vs WeakMap
| Critere | Map | WeakMap |
|---|---|---|
| Types de cles | Tout type (primitives et objets) | Objets uniquement |
| References | Fortes (empechent le GC) | Faibles (permettent le GC) |
| Enumerable | Oui (for…of, forEach, keys(), values()) | Non |
| Taille | Accessible via .size | Non disponible |
| Effacement | .clear() disponible | Non disponible |
| Cas d’usage | Collections generales, lookups | Cache, metadonnees, donnees privees |
| Risque memoire | Fuite possible si non gere | Aucun (auto-nettoye) |
| Performance | Excellente | Excellente |
Tableau Comparatif : Set vs WeakSet
| Critere | Set | WeakSet |
|---|---|---|
| Types de valeurs | Tout type | Objets uniquement |
| References | Fortes | Faibles |
| Enumerable | Oui | Non |
| Cas d’usage | Collections uniques | Tracking d’objets, flags |
En maitrisant ces concepts, vous serez capable de construire des applications web robustes, securisees et performantes, tout en evitant les pieges courants lies a la communication inter-contextes et a la gestion memoire.
Prochaines etapes
- Explorer les Shared Workers pour la communication entre onglets
- Implementer un systeme de messaging avec Broadcast Channel API
- Combiner WeakMap avec Proxy pour un systeme de reactivite avance
- Etudier les patterns de communication avec les iframes sandboxees
In-Article Ad
Dev Mode
Tags
Mahmoud DEVO
Senior Full-Stack Developer
I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.
Enjoyed this article?
Subscribe to get more tech content delivered to your inbox.
Related Articles
Commentaire sur les API batterie et Fluent API en JavaScript
Explorez les APIs web avancees : Battery Status API, design pattern Fluent API et Web Cryptography pour le chiffrement cote client en JavaScript.
Guide complet de manipulation des tableaux en JavaScript
Creez et manipulez des tableaux JavaScript : splice, filter, map, join, entries et plus. Toutes les methodes essentielles avec exemples pratiques.
Manipulation des dates en JavaScript : UTC, conversion et formatage
Guide complet sur les dates JavaScript : conversion en chaine, creation de dates UTC, methodes setUTC et bonnes pratiques pour eviter les problemes de fuseaux.