Micro Frontend Communication Mechanisms
Event Bus Implementation
An Event Bus is an implementation of the publish-subscribe pattern, enabling micro frontend applications to communicate through events without direct references to each other.
Basic Implementation
// eventBus.js
class EventBus {
constructor() {
this.events = {};
}
on(eventName, listener) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(listener);
}
off(eventName, listener) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(
l => l !== listener
);
}
}
emit(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].forEach(listener => {
try {
listener.apply(null, args);
} catch (error) {
console.error(`Error in event listener for ${eventName}:`, error);
}
});
}
}
}
// Create a global event bus instance
const eventBus = new EventBus();
export default eventBus;
Cross-Micro Frontend Usage
Main Application Registers Event Listener:
import eventBus from './eventBus';
// Listen for events from sub-applications
eventBus.on('user-login', (userData) => {
console.log('User logged in:', userData);
// Update main application state
});
Sub-Application Triggers Event:
import eventBus from './eventBus';
// Trigger event after successful login
function handleLoginSuccess(userData) {
eventBus.emit('user-login', userData);
}
Cross-Window/Iframe Communication
When micro frontend applications are distributed across different windows or iframes, window.postMessage is used to implement the event bus:
// In the main application
const eventBus = {
on(eventName, listener) {
window.addEventListener('message', (event) => {
if (event.data?.type === eventName) {
listener(event.data.payload);
}
});
},
emit(eventName, payload) {
window.parent.postMessage(
{ type: eventName, payload },
'*' // In production, specify a specific origin
);
}
};
Custom Events and Publish-Subscribe
A custom event mechanism allows micro frontend applications to define their own event types and communicate through a global event system.
Implementing Custom Events
// customEvent.js
class CustomEvent {
constructor(type, detail = {}) {
this.type = type;
this.detail = detail;
this.timeStamp = Date.now();
}
}
class EventTarget {
constructor() {
this.listeners = {};
}
addEventListener(type, callback) {
if (!this.listeners[type]) {
this.listeners[type] = [];
}
this.listeners[type].push(callback);
}
removeEventListener(type, callback) {
if (this.listeners[type]) {
this.listeners[type] = this.listeners[type].filter(
cb => cb !== callback
);
}
}
dispatchEvent(event) {
if (this.listeners[event.type]) {
this.listeners[event.type].forEach(callback => {
try {
callback.call(this, event);
} catch (error) {
console.error(`Error in event listener for ${event.type}:`, error);
}
});
}
}
}
// Create a global event target
const globalEventTarget = new EventTarget();
export { CustomEvent, globalEventTarget };
Usage Example
Main Application Registers Listener:
import { CustomEvent, globalEventTarget } from './customEvent';
// Listen for custom events
globalEventTarget.addEventListener('cart-updated', (event) => {
console.log('Cart updated:', event.detail);
// Update main application cart UI
});
Sub-Application Triggers Event:
import { CustomEvent, globalEventTarget } from './customEvent';
// Trigger event after adding a product to the cart
function addToCart(product) {
// ...Add product logic
// Dispatch global event
globalEventTarget.dispatchEvent(
new CustomEvent('cart-updated', { detail: { product, timestamp: Date.now() } })
);
}
Window.postMessage Practice
postMessage is a browser-provided API for cross-document communication, suitable for cross-window/iframe communication in micro frontend architectures.
Basic Usage
Sending a Message:
// Main application sends a message to sub-application
const childWindow = document.getElementById('child-iframe').contentWindow;
childWindow.postMessage(
{ type: 'INITIALIZE', payload: { user: currentUser } },
'https://child-app-domain.com'
);
Receiving a Message:
// Sub-application listens for messages
window.addEventListener('message', (event) => {
// Verify origin
if (event.origin !== 'https://main-app-domain.com') return;
// Handle different message types
switch (event.data.type) {
case 'INITIALIZE':
initializeApp(event.data.payload);
break;
case 'USER_UPDATED':
updateUser(event.data.payload.user);
break;
default:
console.warn('Unknown message type:', event.data.type);
}
});
Security Considerations
- Origin Verification: Always check
event.originandevent.source. - Restrict Target Domain: Specify the target domain in
postMessage. - Data Validation: Validate and sanitize received data.
Advanced Pattern
Request-Response Pattern:
// Main application sends a request and awaits a response
function requestFromChild(action, data) {
return new Promise((resolve) => {
function handleMessage(event) {
if (event.data.responseTo === messageId && event.origin === childOrigin) {
window.removeEventListener('message', handleMessage);
resolve(event.data.result);
}
}
const messageId = generateUniqueId();
window.addEventListener('message', handleMessage);
childWindow.postMessage(
{
type: 'REQUEST',
action,
data,
messageId
},
childOrigin
);
});
}
// Sub-application processes requests and returns responses
window.addEventListener('message', (event) => {
if (event.origin !== mainOrigin) return;
if (event.data.type === 'REQUEST') {
// Process request
const result = handleRequest(event.data.action, event.data.data);
// Return response
event.source.postMessage(
{
type: 'RESPONSE',
responseTo: event.data.messageId,
result
},
event.origin
);
}
});
Micro Frontend State Management
Shared State Design
Shared State Patterns
- Global State Tree: Design a global state tree containing all shared state.
- State Partitioning: Divide state into areas based on functional domains.
- State Version Control: Add version numbers to state for synchronization and conflict resolution.
Example State Structure:
{
user: {
id: '123',
name: 'John Doe',
auth: { token: 'abc123' }
},
cart: {
items: [{ id: 'p1', qty: 2 }],
total: 19.99
},
settings: {
theme: 'dark',
language: 'en'
}
}
State Sharing Strategies
- Explicit Sharing: Pass shared state explicitly through props or context.
- Implicit Sharing: Share state implicitly via global state management tools.
- On-Demand Sharing: Share only the minimal necessary state set.
Redux Implementation in Micro Frontends
Approach 1: Single Redux Store (Suitable for Tightly Coupled Micro Frontends)
// Main application configures Redux store
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './features/userSlice';
import cartReducer from './features/cartSlice';
const store = configureStore({
reducer: {
user: userReducer,
cart: cartReducer
}
});
// Provide to sub-applications
window.sharedStore = store;
Sub-Application Accesses Store:
// Access shared store in sub-application
const dispatch = window.sharedStore.dispatch;
const state = window.sharedStore.getState();
function updateCart(item) {
dispatch(addToCart(item));
}
Approach 2: Multi-Store Pattern (Suitable for Loosely Coupled Micro Frontends)
// Main application store
const mainStore = configureStore({
reducer: { /* Main application state */ }
});
// Sub-application store
const childStore = configureStore({
reducer: { /* Sub-application state */ },
// Middleware for syncing with main application
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false
}).concat(syncWithMainStore)
});
Synchronization Middleware Example:
const syncWithMainStore = store => next => action => {
const result = next(action);
// Notify main application of sub-application state changes
window.parent.postMessage({
type: 'STATE_UPDATE',
appName: 'child-app',
state: store.getState()
}, '*');
return result;
};
State Isolation and Synchronization
State Isolation Strategies
- Modular State: Each micro frontend maintains its own state module.
- Sandbox Environment: Use iframes or Web Workers to isolate state.
- Namespaces: Add namespace prefixes to shared state.
Namespace Example:
// Main application state
{
'app1.user': { id: '1', name: 'Alice' },
'app2.user': { id: '2', name: 'Bob' },
'shared.cart': { items: [...] }
}
State Synchronization Solutions
- Event-Driven Synchronization: Synchronize state changes through an event bus.
- Polling Synchronization: Periodically check for state changes.
- Operation Log: Record state change operations for replay.
Event-Based Synchronization Implementation:
// Main application listens for state changes
eventBus.on('state-change', ({ appName, stateKey, newState }) => {
// Merge into main state
const currentState = store.getState()[stateKey] || {};
store.dispatch(updateState({
[stateKey]: { ...currentState, ...newState }
}));
});
// Sub-application triggers state change event
function updateLocalState(newState) {
// Update local state
// ...
// Publish state change event
eventBus.emit('state-change', {
appName: 'child-app',
stateKey: 'user-profile',
newState
});
}
Communication Optimization
Communication Performance Optimization
Batch Message Processing
// Main application sends messages in batches
let messageQueue = [];
let isSending = false;
function sendMessageBatch(messages) {
if (isSending) {
messageQueue.push(...messages);
return;
}
isSending = true;
function sendNextBatch() {
const batch = messageQueue.splice(0, 10); // Send up to 10 messages at a time
if (batch.length > 0) {
window.postMessage({ type: 'BATCH', messages: batch }, '*');
setTimeout(sendNextBatch, 50); // Delay sending next batch
} else {
isSending = false;
if (messageQueue.length > 0) {
sendNextBatch();
}
}
}
sendNextBatch();
}
Lazy Loading Communication Modules
// Load communication module on demand
let communicationModule;
function getCommunicationModule() {
if (!communicationModule) {
communicationModule = import('./communication').then(module => {
// Initialize communication module
return module.default;
});
}
return communicationModule;
}
// Usage
getCommunicationModule().then(communication => {
communication.send('data');
});
Data Serialization and Transmission
Efficient Serialization Solutions
JSON Optimization:
// Custom serialization to exclude unnecessary data
function optimizedStringify(data) {
const filtered = filterSensitiveData(data);
return JSON.stringify(filtered);
}
Binary Serialization (Suitable for complex scenarios):
// Use MessagePack or similar binary format
import msgpack from 'msgpack-lite';
const packed = msgpack.encode(data);
window.postMessage({ type: 'DATA', payload: packed }, '*');
Transmission Optimization
Compressed Transmission:
// Use gzip compression
import pako from 'pako';
const compressed = pako.deflate(JSON.stringify(data));
window.postMessage({
type: 'DATA',
payload: btoa(String.fromCharCode(...compressed))
}, '*');
Incremental Updates:
// Send only changed parts
function getDelta(oldState, newState) {
const delta = {};
for (const key in newState) {
if (oldState[key] !== newState[key]) {
delta[key] = newState[key];
}
}
return delta;
}
Error Handling and Retry
Robust Communication Mechanism
class RobustMessenger {
constructor(targetWindow, targetOrigin) {
this.targetWindow = targetWindow;
this.targetOrigin = targetOrigin;
this.retryQueue = [];
this.maxRetries = 3;
this.retryDelay = 1000;
}
send(message, retry = true) {
try {
this.targetWindow.postMessage(message, this.targetOrigin);
// Set timeout for retry
const timeoutId = setTimeout(() => {
if (retry && this.retryQueue.some(m => m.id === message.id)) {
this.retry(message, retry - 1);
}
}, 5000);
// Store message ID for tracking
if (retry) {
this.retryQueue.push({
id: message.id,
message,
retryCount: 0,
timeoutId
});
}
} catch (error) {
console.error('Message send failed:', error);
if (retry) {
this.retry(message, this.maxRetries - 1);
}
}
}
retry(message, attemptsLeft) {
clearTimeout(this.retryQueue.find(m => m.id === message.id)?.timeoutId);
if (attemptsLeft <= 0) {
console.error('Max retries reached for message:', message.id);
return;
}
setTimeout(() => {
this.send(message, false);
}, this.retryDelay * Math.pow(2, this.maxRetries - attemptsLeft));
}
// Requires message listening and acknowledgment mechanism
}
Error Recovery Strategies
Message Acknowledgment Mechanism:
// Sender
function sendMessageWithAck(message) {
return new Promise((resolve, reject) => {
const id = generateUniqueId();
const timeout = setTimeout(() => {
reject(new Error('Timeout waiting for ACK'));
}, 3000);
function handleAck(event) {
if (event.data?.type === 'ACK' && event.data.id === id) {
clearTimeout(timeout);
window.removeEventListener('message', handleAck);
resolve();
}
}
window.addEventListener('message', handleAck);
window.postMessage({ ...message, id }, '*');
});
}
Dead Letter Queue:
// Record failed messages for later processing
const deadLetterQueue = [];
function handleFailedMessage(message, error) {
deadLetterQueue.push({
message,
error,
timestamp: Date.now(),
retryCount: 0
});
// Can set up a scheduled task to process dead letter queue
}
Summary
Communication and state management in micro frontend architectures are among the core challenges of system design, requiring comprehensive consideration of the following key points:
- Communication Mechanisms:
- Select appropriate communication methods (event bus, postMessage, etc.) based on the scenario.
- Design efficient serialization and transmission solutions.
- Implement robust error handling and retry mechanisms.
- State Management:
- Reasonably divide shared and local state.
- Design clear state synchronization strategies.
- Ensure state isolation and security.
- Performance Optimization:
- Batch processing and lazy loading of communication modules.
- Optimize data transmission formats and sizes.
- Implement incremental updates to reduce transmission volume.
- Maintainability:
- Establish unified communication protocols and standards.
- Provide clear error handling and logging.
- Design scalable architectures to support future requirement changes.
By adopting thoughtful architectural design and optimization strategies, it is possible to build efficient, reliable, and maintainable micro frontend systems that meet the complex demands of modern web applications.



