Moderne Webanwendungen bestehen oft aus verschiedenen Frontend-Modulen, die von unterschiedlichen Teams entwickelt werden. Ein E-Commerce-System könnte separate Microfrontends für Produktkatalog, Warenkorb, Checkout und Kundenkonto haben. Wie synchronisieren Sie diese unabhängigen Frontend-Komponenten mit Events aus Ihrem Backend?
Event-driven Microfrontend-Integration schafft eine konsistente Benutzererfahrung ohne enge Kopplung zwischen Frontend-Modulen.
Anstatt Frontend-Komponenten direkt miteinander kommunizieren zu lassen, propagieren Sie Backend-Events bis in die UI. Jedes Microfrontend reagiert autonom auf relevante Events und aktualisiert seine Darstellung entsprechend.
Denken Sie an folgendes Szenario: Ein Kunde fügt ein Produkt zum Warenkorb hinzu. Welche UI-Komponenten müssen sich aktualisieren?
| Microfrontend | Event-Interesse | UI-Update |
|---|---|---|
| Warenkorb-Widget | CartItemAdded | Badge-Zähler aktualisieren |
| Produktseite | InventoryChanged | Verfügbarkeit anzeigen |
| Checkout | CartChanged | Gesamtsumme neuberechnen |
| Empfehlungen | CartItemAdded | Ähnliche Produkte anzeigen |
Frontend Event Bus Pattern mit JavaScript:
// Zentraler Event Bus für Microfrontend-Kommunikation
class MicrofrontendEventBus {
constructor() {
this.eventListeners = new Map();
this.backendEventSource = null;
this.initializeBackendConnection();
}
initializeBackendConnection() {
// Server-Sent Events für Backend-Integration
this.backendEventSource = new EventSource('/api/events/stream');
this.backendEventSource.onmessage = (event) => {
const eventData = JSON.parse(event.data);
this.emit(eventData.eventType, eventData);
};
this.backendEventSource.onerror = (error) => {
console.error('Backend event connection lost:', error);
this.reconnectToBackend();
};
}
subscribe(eventType, callback) {
if (!this.eventListeners.has(eventType)) {
this.eventListeners.set(eventType, []);
}
this.eventListeners.get(eventType).push(callback);
return () => this.unsubscribe(eventType, callback);
}
emit(eventType, eventData) {
const listeners = this.eventListeners.get(eventType) || [];
listeners.forEach(callback => {
try {
callback(eventData);
} catch (error) {
console.error(`Error in event listener for ${eventType}:`, error);
}
});
}
reconnectToBackend() {
setTimeout(() => {
this.initializeBackendConnection();
}, 5000);
}
}
// Globaler Event Bus für alle Microfrontends
window.microfrontendEventBus = new MicrofrontendEventBus();
// Warenkorb Microfrontend
class CartMicrofrontend {
constructor() {
this.cartItems = [];
this.initialize();
}
initialize() {
// Backend Events abonnieren
window.microfrontendEventBus.subscribe('CartItemAdded', (event) => {
this.handleItemAdded(event.data);
});
window.microfrontendEventBus.subscribe('CartItemRemoved', (event) => {
this.handleItemRemoved(event.data);
});
window.microfrontendEventBus.subscribe('OrderPlaced', (event) => {
this.handleOrderPlaced(event.data);
});
}
handleItemAdded(eventData) {
const item = {
productId: eventData.productId,
quantity: eventData.quantity,
price: eventData.price
};
this.cartItems.push(item);
this.updateCartDisplay();
// Lokales Event für andere UI-Komponenten
window.microfrontendEventBus.emit('CartDisplayUpdated', {
itemCount: this.cartItems.length,
totalAmount: this.calculateTotal()
});
}
updateCartDisplay() {
const cartBadge = document.querySelector('.cart-badge');
if (cartBadge) {
cartBadge.textContent = this.cartItems.length;
cartBadge.classList.add('updated');
setTimeout(() => {
cartBadge.classList.remove('updated');
}, 300);
}
}
}
// Produktseite Microfrontend
class ProductPageMicrofrontend {
constructor() {
this.currentProductId = this.extractProductIdFromUrl();
this.initialize();
}
initialize() {
window.microfrontendEventBus.subscribe('InventoryChanged', (event) => {
if (event.data.productId === this.currentProductId) {
this.updateAvailabilityDisplay(event.data);
}
});
window.microfrontendEventBus.subscribe('CartDisplayUpdated', (event) => {
this.updateAddToCartButton(event);
});
}
updateAvailabilityDisplay(inventoryData) {
const availabilityElement = document.querySelector('.product-availability');
if (availabilityElement) {
if (inventoryData.quantity > 0) {
availabilityElement.textContent = `${inventoryData.quantity} verfügbar`;
availabilityElement.className = 'product-availability available';
} else {
availabilityElement.textContent = 'Ausverkauft';
availabilityElement.className = 'product-availability out-of-stock';
}
}
}
}React-basierte Microfrontend-Integration:
// React Hook für Event Bus Integration
import { useEffect, useState } from 'react';
function useBackendEvents(eventTypes) {
const [events, setEvents] = useState({});
useEffect(() => {
const unsubscribers = eventTypes.map(eventType => {
return window.microfrontendEventBus.subscribe(eventType, (eventData) => {
setEvents(prev => ({
...prev,
[eventType]: eventData
}));
});
});
return () => {
unsubscribers.forEach(unsubscribe => unsubscribe());
};
}, [eventTypes]);
return events;
}
// Cart Component
function CartWidget() {
const [cartState, setCartState] = useState({ items: [], total: 0 });
const events = useBackendEvents(['CartItemAdded', 'CartItemRemoved', 'OrderPlaced']);
useEffect(() => {
if (events.CartItemAdded) {
setCartState(prev => ({
items: [...prev.items, events.CartItemAdded.data],
total: prev.total + events.CartItemAdded.data.price
}));
}
if (events.CartItemRemoved) {
setCartState(prev => ({
items: prev.items.filter(item =>
item.productId !== events.CartItemRemoved.data.productId
),
total: prev.total - events.CartItemRemoved.data.price
}));
}
if (events.OrderPlaced) {
setCartState({ items: [], total: 0 });
}
}, [events]);
return (
<div className="cart-widget">
<div className="cart-icon">
🛒
{cartState.items.length > 0 && (
<span className="cart-badge">{cartState.items.length}</span>
)}
</div>
<div className="cart-total">€{cartState.total.toFixed(2)}</div>
</div>
);
}
// Product Availability Component
function ProductAvailability({ productId }) {
const [availability, setAvailability] = useState(null);
const events = useBackendEvents(['InventoryChanged', 'InventoryReserved']);
useEffect(() => {
if (events.InventoryChanged?.data.productId === productId) {
setAvailability(events.InventoryChanged.data);
}
if (events.InventoryReserved?.data.productId === productId) {
setAvailability(prev => ({
...prev,
quantity: prev.quantity - events.InventoryReserved.data.quantity
}));
}
}, [events, productId]);
if (!availability) return <div>Verfügbarkeit wird geladen...</div>;
return (
<div className={`availability ${availability.quantity > 0 ? 'in-stock' : 'out-of-stock'}`}>
{availability.quantity > 0
? `${availability.quantity} auf Lager`
: 'Ausverkauft'
}
</div>
);
}Wie entscheiden Sie, welche Events lokal im Frontend behandelt werden und welche an das Backend propagiert werden? Client-side Event Handling optimiert die Benutzererfahrung durch sofortige UI-Updates bei vorhersagbaren Aktionen.
Hybrid Event Handling Strategie:
class HybridEventHandler {
constructor() {
this.optimisticUpdates = new Map();
this.eventQueue = [];
this.isOnline = navigator.onLine;
this.setupNetworkMonitoring();
}
setupNetworkMonitoring() {
window.addEventListener('online', () => {
this.isOnline = true;
this.flushEventQueue();
});
window.addEventListener('offline', () => {
this.isOnline = false;
});
}
async handleUserAction(action, data) {
const actionId = this.generateActionId();
// Sofortige UI-Aktualisierung (optimistic update)
const rollbackFunction = this.applyOptimisticUpdate(action, data);
this.optimisticUpdates.set(actionId, rollbackFunction);
try {
if (this.isOnline) {
// Backend-Call
const result = await this.sendToBackend(action, data, actionId);
// Optimistic Update bestätigen
this.confirmOptimisticUpdate(actionId, result);
} else {
// Offline-Modus: In Queue einreihen
this.eventQueue.push({ action, data, actionId, timestamp: Date.now() });
}
} catch (error) {
// Rollback bei Fehler
this.rollbackOptimisticUpdate(actionId);
this.showErrorMessage(action, error);
}
}
applyOptimisticUpdate(action, data) {
switch (action) {
case 'ADD_TO_CART':
return this.optimisticallyAddToCart(data);
case 'REMOVE_FROM_CART':
return this.optimisticallyRemoveFromCart(data);
case 'UPDATE_QUANTITY':
return this.optimisticallyUpdateQuantity(data);
default:
return () => {}; // No-op rollback
}
}
optimisticallyAddToCart(data) {
const cartWidget = document.querySelector('.cart-widget');
const currentCount = parseInt(cartWidget.dataset.itemCount || '0');
const newCount = currentCount + 1;
cartWidget.dataset.itemCount = newCount;
cartWidget.querySelector('.cart-badge').textContent = newCount;
cartWidget.classList.add('pending');
// Rollback-Funktion zurückgeben
return () => {
cartWidget.dataset.itemCount = currentCount;
cartWidget.querySelector('.cart-badge').textContent = currentCount;
cartWidget.classList.remove('pending', 'confirmed');
};
}
confirmOptimisticUpdate(actionId, backendResult) {
const rollback = this.optimisticUpdates.get(actionId);
this.optimisticUpdates.delete(actionId);
// UI-Feedback für erfolgreiche Bestätigung
const elements = document.querySelectorAll('.pending');
elements.forEach(el => {
el.classList.remove('pending');
el.classList.add('confirmed');
setTimeout(() => el.classList.remove('confirmed'), 500);
});
}
rollbackOptimisticUpdate(actionId) {
const rollbackFunction = this.optimisticUpdates.get(actionId);
if (rollbackFunction) {
rollbackFunction();
this.optimisticUpdates.delete(actionId);
}
// Fehler-UI anzeigen
const elements = document.querySelectorAll('.pending');
elements.forEach(el => {
el.classList.remove('pending');
el.classList.add('error');
setTimeout(() => el.classList.remove('error'), 2000);
});
}
async flushEventQueue() {
for (const queuedEvent of this.eventQueue) {
try {
await this.sendToBackend(
queuedEvent.action,
queuedEvent.data,
queuedEvent.actionId
);
} catch (error) {
console.error('Failed to sync queued event:', error);
}
}
this.eventQueue = [];
}
}Komplexe Microfrontend-Anwendungen benötigen koordinierte Zustandssynchronisation. Redux-ähnliche State-Management-Patterns lassen sich elegant mit Event-Streams verbinden.
Event-driven State Management:
// Zentraler State Store für alle Microfrontends
class MicrofrontendStateStore {
constructor() {
this.state = {
user: null,
cart: { items: [], total: 0 },
inventory: {},
orders: [],
ui: { loading: false, errors: [] }
};
this.subscribers = new Set();
this.eventHandlers = new Map();
this.setupEventHandlers();
this.connectToBackendEvents();
}
setupEventHandlers() {
this.eventHandlers.set('OrderPlaced', (event) => {
this.updateState(prevState => ({
...prevState,
cart: { items: [], total: 0 },
orders: [...prevState.orders, event.data]
}));
});
this.eventHandlers.set('CartItemAdded', (event) => {
this.updateState(prevState => ({
...prevState,
cart: {
items: [...prevState.cart.items, event.data],
total: prevState.cart.total + event.data.price
}
}));
});
this.eventHandlers.set('InventoryChanged', (event) => {
this.updateState(prevState => ({
...prevState,
inventory: {
...prevState.inventory,
[event.data.productId]: event.data.quantity
}
}));
});
this.eventHandlers.set('UserLoggedIn', (event) => {
this.updateState(prevState => ({
...prevState,
user: event.data.user,
cart: event.data.cart || prevState.cart
}));
});
}
connectToBackendEvents() {
window.microfrontendEventBus.subscribe('*', (event) => {
const handler = this.eventHandlers.get(event.eventType);
if (handler) {
handler(event);
}
});
}
updateState(updater) {
const newState = updater(this.state);
const hasChanged = !this.deepEqual(this.state, newState);
if (hasChanged) {
const previousState = this.state;
this.state = newState;
this.notifySubscribers(newState, previousState);
}
}
subscribe(callback) {
this.subscribers.add(callback);
// Aktuellen State sofort senden
callback(this.state);
return () => this.subscribers.delete(callback);
}
notifySubscribers(newState, previousState) {
this.subscribers.forEach(callback => {
try {
callback(newState, previousState);
} catch (error) {
console.error('Error in state subscriber:', error);
}
});
}
getState() {
return this.state;
}
// Action-Dispatcher für lokale State-Änderungen
dispatch(action) {
switch (action.type) {
case 'SET_LOADING':
this.updateState(state => ({
...state,
ui: { ...state.ui, loading: action.payload }
}));
break;
case 'ADD_ERROR':
this.updateState(state => ({
...state,
ui: {
...state.ui,
errors: [...state.ui.errors, action.payload]
}
}));
break;
case 'CLEAR_ERRORS':
this.updateState(state => ({
...state,
ui: { ...state.ui, errors: [] }
}));
break;
}
}
deepEqual(obj1, obj2) {
return JSON.stringify(obj1) === JSON.stringify(obj2);
}
}
// Globaler State Store
window.microfrontendStore = new MicrofrontendStateStore();
// React Hook für State-Integration
function useMicrofrontendState(selector) {
const [state, setState] = useState(() => {
const globalState = window.microfrontendStore.getState();
return selector ? selector(globalState) : globalState;
});
useEffect(() => {
const unsubscribe = window.microfrontendStore.subscribe((newState) => {
const selectedState = selector ? selector(newState) : newState;
setState(selectedState);
});
return unsubscribe;
}, [selector]);
return state;
}
// Beispiel-Komponente mit State-Integration
function OrderSummary() {
const cart = useMicrofrontendState(state => state.cart);
const user = useMicrofrontendState(state => state.user);
const handlePlaceOrder = async () => {
window.microfrontendStore.dispatch({
type: 'SET_LOADING',
payload: true
});
try {
const orderData = {
customerId: user.id,
items: cart.items,
totalAmount: cart.total
};
// Backend-Call über Event Bus
await window.hybridEventHandler.handleUserAction('PLACE_ORDER', orderData);
} catch (error) {
window.microfrontendStore.dispatch({
type: 'ADD_ERROR',
payload: 'Bestellung konnte nicht aufgegeben werden'
});
} finally {
window.microfrontendStore.dispatch({
type: 'SET_LOADING',
payload: false
});
}
};
return (
<div className="order-summary">
<h3>Bestellübersicht</h3>
{cart.items.map(item => (
<div key={item.productId} className="order-item">
{item.name} - €{item.price}
</div>
))}
<div className="order-total">
Gesamt: €{cart.total.toFixed(2)}
</div>
<button onClick={handlePlaceOrder}>
Bestellung aufgeben
</button>
</div>
);
}State Synchronization Patterns:
| Pattern | Konsistenz | Performance | Komplexität | Anwendungsfall |
|---|---|---|---|---|
| Shared State Store | Stark | Mittel | Hoch | Komplexe UI-Interaktionen |
| Event-only Sync | Eventual | Hoch | Niedrig | Lose gekoppelte MF |
| Hybrid Approach | Konfigurierbar | Optimal | Mittel | Production Systems |
Die Event-driven Microfrontend-Integration ermöglicht es, unabhängige Frontend-Module zu entwickeln, die trotzdem eine konsistente Benutzererfahrung bieten. Events fungieren als universelle Kommunikationsschicht zwischen Backend-Services und Frontend-Komponenten.
Was sind Ihre Erfahrungen mit Microfrontend-Architekturen? Welche Herausforderungen sehen Sie bei der Event-Synchronisation in verteilten Frontend-Systemen?