Architecture Micro-frontends : Modularité et scalabilité des applications web modernes

L'architecture micro-frontends révolutionne l'organisation des applications web complexes en appliquant les principes microservices au développement frontend. Cette approche modulaire permet aux équipes de développer, déployer et maintenir indépendamment des fonctionnalités distinctes.

Fondamentaux et philosophie micro-frontends Les micro-frontends décomposent les monolithes frontend

en unités autonomes possédant leurs propres cycles de développement, technologies et équipes dédiées. Cette indépendance technique élimine les goulots d'étranglement organisationnels et accélère drastiquement l'innovation produit. La séparation des responsabilités par domaine métier garantit une cohésion fonctionnelle forte tout en minimisant le couplage technique. Chaque micro-frontend peut évoluer selon ses besoins spécifiques sans impacter les autres composants de l'écosystème. L'intégration transparente au niveau utilisateur masque la complexité architecturale et préserve une expérience unifiée. Les techniques de composition dynamique permettent d'assembler les micro-frontends selon les besoins contextuels et les permissions utilisateur.

Stratégies d'intégration et composition L'orchestration des micro-frontends exploite plusieurs tec

hniques de composition, chacune adaptée à des contraintes spécifiques de performance, SEO et expérience utilisateur. Le choix de la stratégie d'intégration détermine largement l'architecture globale.

// Shell Application - Orchestrateur principal class MicrofrontendShell { constructor() { this.applications = new Map(); this.router = new MicrofrontendRouter(); this.eventBus = new EventBus(); this.sharedState = new SharedStateManager(); this.loadingStates = new Map(); this.initializeShell(); } async initializeShell() { // Configuration des micro-frontends disponibles this.microfrontends = { 'auth': { url: '/auth/remoteEntry.js', scope: 'auth', module: './AuthApp', routes: ['/login', '/register', '/profile'], permissions: ['guest'], preload: true }, 'dashboard': { url: '/dashboard/remoteEntry.js', scope: 'dashboard', module: './DashboardApp', routes: ['/dashboard/'], permissions: ['user', 'admin'], preload: false }, 'ecommerce': { url: '/shop/remoteEntry.js', scope: 'ecommerce', module: './ShopApp', routes: ['/products', '/cart', '/checkout'], permissions: ['user'], preload: false }, 'admin': { url: '/admin/remoteEntry.js', scope: 'admin', module: './AdminApp', routes: ['/admin/'], permissions: ['admin'], preload: false } }; // Préchargement des micro-frontends critiques await this.preloadCriticalMicrofrontends(); // Configuration du routing intelligent this.setupIntelligentRouting(); // Initialisation des communications inter-applications this.initializeEventSystem(); console.log('Microfrontend shell initialized', { applications: Object.keys(this.microfrontends), preloaded: this.getPreloadedApplications() }); } // Chargement dynamique des micro-frontends async loadMicrofrontend(name) { if (this.applications.has(name)) { return this.applications.get(name); } const config = this.microfrontends[name]; if (!config) { throw new Error(Microfrontend ${name} not configured); } // Vérification des permissions if (!this.hasPermission(config.permissions)) { throw new Error(Insufficient permissions for ${name}); } this.loadingStates.set(name, 'loading'); try { // Chargement du module distant avec timeout const container = await this.loadRemoteContainer(config.url, config.scope); const factory = await container.get(config.module); const module = factory(); // Initialisation du micro-frontend const application = await this.initializeMicrofrontend(module, config); this.applications.set(name, application); this.loadingStates.set(name, 'loaded'); // Émission d'événement pour les listeners this.eventBus.emit('microfrontend:loaded', { name, application }); console.log(Microfrontend ${name} loaded successfully); return application; } catch (error) { this.loadingStates.set(name, 'error'); console.error(Failed to load microfrontend ${name}:, error); throw error; } } // Chargement de conteneur distant avec retry logic async loadRemoteContainer(url, scope, retries = 3) { for (let attempt = 1; attempt <= retries; attempt++) { try { // Injection dynamique du script await this.injectScript(url); // Vérification de la disponibilité du scope if (!window[scope]) { throw new Error(Scope ${scope} not available); } // Initialisation du conteneur await window[scope].init({ shared: { react: { singleton: true, version: '18.3.1' }, 'react-dom': { singleton: true, version: '18.3.1' }, 'react-router-dom': { singleton: true }, '@shared/components': { singleton: true }, '@shared/utils': { singleton: true } } }); return window[scope]; } catch (error) { console.warn(Attempt ${attempt}/${retries} failed for ${url}:, error); if (attempt === retries) { throw new Error(Failed to load remote container after ${retries} attempts); } // Backoff exponentiel await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000) ); } } } // Injection sécurisée des scripts distants async injectScript(url) { return new Promise((resolve, reject) => { // Vérification de la whitelist des URLs autorisées if (!this.isUrlAllowed(url)) { reject(new Error(URL not whitelisted: ${url})); return; } // Éviter les doublons const existingScript = document.querySelector(script[src=&quot;${url}&quot;]); if (existingScript) { resolve(); return; } const script = document.createElement('script'); script.src = url; script.type = 'text/javascript'; script.async = true; // Sécurité CSP script.crossOrigin = 'anonymous'; script.integrity = this.getScriptIntegrity(url); script.onload = () => { console.log(Script loaded: ${url}); resolve(); }; script.onerror = (error) => { document.head.removeChild(script); reject(new Error(Script load failed: ${url})); }; // Timeout pour éviter les blocages setTimeout(() => { if (!script.dataset.loaded) { document.head.removeChild(script); reject(new Error(Script load timeout: ${url})); } }, 10000); document.head.appendChild(script); }); } // Routage intelligent avec lazy loading setupIntelligentRouting() { this.router.configure({ routes: Object.entries(this.microfrontends).flatMap(([name, config]) => config.routes.map(route => ({ path: route, microfrontend: name, loader: () => this.loadMicrofrontend(name), preload: config.preload })) ), // Gestion des routes non trouvées fallback: { component: this.NotFoundComponent, microfrontend: 'shell' }, // Middleware de navigation beforeNavigation: async (to, from) => { // Analytics de navigation this.trackNavigation(from, to); // Préchargement prédictif await this.predictivePreload(to); // Vérification des permissions return this.checkNavigationPermissions(to); } }); // Écoute des changements de route this.router.onRouteChange(async (route) => { const microfrontendName = this.getMicrofrontendForRoute(route); if (microfrontendName) { try { const application = await this.loadMicrofrontend(microfrontendName); await this.mountMicrofrontend(application, route); } catch (error) { console.error('Route navigation failed:', error); this.router.navigateToError(error); } } }); } // Système de communication inter-applications initializeEventSystem() { // Bus d'événements global this.eventBus.configure({ // Middleware pour les événements middleware: [ this.logEventMiddleware, this.authenticationMiddleware, this.validationMiddleware ], // Gestion des erreurs d'événements errorHandler: (error, event) => { console.error('Event handling error:', error, event); this.reportError(error, { context: 'event-system', event }); } }); // Événements système standard this.eventBus.on('user:login', this.handleUserLogin.bind(this)); this.eventBus.on('user:logout', this.handleUserLogout.bind(this)); this.eventBus.on('cart:update', this.handleCartUpdate.bind(this)); this.eventBus.on('notification:show', this.handleNotification.bind(this)); // Synchronisation d'état entre micro-frontends this.eventBus.on('state:sync', (event) => { this.sharedState.sync(event.key, event.value, event.source); }); } // Préchargement prédictif basé sur les patterns utilisateur async predictivePreload(currentRoute) { const predictions = await this.analyzePredictivePatterns(currentRoute); const preloadPromises = predictions .filter(prediction => prediction.confidence > 0.7) .slice(0, 3) // Limite à 3 prédictions .map(prediction => { const microfrontend = this.getMicrofrontendForRoute(prediction.route); if (microfrontend && !this.applications.has(microfrontend)) { return this.loadMicrofrontend(microfrontend).catch(error => { console.warn(Predictive preload failed for ${microfrontend}:, error); }); } }) .filter(Boolean); if (preloadPromises.length > 0) { console.log(Predictive preloading ${preloadPromises.length} microfrontends); await Promise.allSettled(preloadPromises); } } // Gestion des permissions et sécurité hasPermission(requiredPermissions) { const userPermissions = this.sharedState.get('user.permissions') || ['guest']; return requiredPermissions.some(permission => userPermissions.includes(permission) ); } checkNavigationPermissions(route) { const microfrontend = this.getMicrofrontendForRoute(route); if (!microfrontend) return true; const config = this.microfrontends[microfrontend]; return this.hasPermission(config.permissions); } // Nettoyage et optimisation mémoire async unmountMicrofrontend(name) { const application = this.applications.get(name); if (!application) return; try { // Appel du lifecycle de nettoyage if (application.unmount) { await application.unmount(); } // Suppression des event listeners this.eventBus.removeAllListeners(${name}:*); // Nettoyage du state local this.sharedState.clearNamespace(name); this.applications.delete(name); console.log(Microfrontend ${name} unmounted successfully); } catch (error) { console.error(Error unmounting microfrontend ${name}:, error); } } } // Gestionnaire d'état partagé optimisé class SharedStateManager { constructor() { this.state = new Map(); this.subscribers = new Map(); this.history = []; this.maxHistorySize = 100; // Persistence locale this.loadFromStorage(); // Synchronisation avec le localStorage window.addEventListener('beforeunload', () => { this.saveToStorage(); }); } // Gestion d'état avec notification des changements set(key, value, source = 'unknown') { const previousValue = this.state.get(key); const hasChanged = !this.deepEqual(previousValue, value); if (!hasChanged) return; this.state.set(key, value); // Historique pour debugging this.history.push({ timestamp: Date.now(), action: 'set', key, value, previousValue, source }); if (this.history.length > this.maxHistorySize) { this.history.shift(); } // Notification des abonnés this.notifySubscribers(key, value, previousValue, source); } get(key) { return this.state.get(key); } subscribe(key, callback, options = {}) { if (!this.subscribers.has(key)) { this.subscribers.set(key, new Set()); } const subscription = { callback, options, id: ${key}${Date.now()}${Math.random()} }; this.subscribers.get(key).add(subscription); // Appel immédiat si demandé if (options.immediate && this.state.has(key)) { callback(this.state.get(key), undefined, 'immediate'); } // Fonction de désabonnement return () => { this.subscribers.get(key)?.delete(subscription); }; } // Synchronisation entre micro-frontends sync(key, value, source) { this.set(key, value, source); // Propagation aux autres instances window.postMessage({ type: 'SHAREDSTATESYNC', key, value, source }, window.location.origin); } notifySubscribers(key, value, previousValue, source) { const keySubscribers = this.subscribers.get(key); if (!keySubscribers) return; keySubscribers.forEach(subscription => { try { subscription.callback(value, previousValue, source); } catch (error) { console.error(Subscriber error for key &quot;${key}&quot;:, error); } }); // Notification pour les patterns wildcard this.notifyWildcardSubscribers(key, value, previousValue, source); } notifyWildcardSubscribers(key, value, previousValue, source) { for (const [pattern, subscribers] of this.subscribers.entries()) { if (pattern.includes('*') && this.matchesPattern(key, pattern)) { subscribers.forEach(subscription => { try { subscription.callback(value, previousValue, source); } catch (error) { console.error(Wildcard subscriber error for pattern &quot;${pattern}&quot;:, error); } }); } } } matchesPattern(key, pattern) { const regex = new RegExp( '^' + pattern.replace(/\/g, '.').replace(/\?/g, '.') + '$' ); return regex.test(key); } deepEqual(a, b) { if (a === b) return true; if (a == null || b == null) return a === b; if (typeof a !== typeof b) return false; if (typeof a !== 'object') return a === b; if (Array.isArray(a) !== Array.isArray(b)) return false; const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; for (const key of keysA) { if (!keysB.includes(key)) return false; if (!this.deepEqual(a[key], b[key])) return false; } return true; } // Persistence et récupération saveToStorage() { try { const serializedState = JSON.stringify(Object.fromEntries(this.state)); localStorage.setItem('microfrontendsharedstate', serializedState); } catch (error) { console.warn('Failed to save shared state:', error); } } loadFromStorage() { try { const serializedState = localStorage.getItem('microfrontendsharedstate'); if (serializedState) { const parsedState = JSON.parse(serializedState); this.state = new Map(Object.entries(parsedState)); } } catch (error) { console.warn('Failed to load shared state:', error); this.state = new Map(); } } clearNamespace(namespace) { const keysToDelete = []; for (const key of this.state.keys()) { if (key.startsWith(${namespace}.)) { keysToDelete.push(key); } } keysToDelete.forEach(key => this.state.delete(key)); } }

Webpack Module Federation et configuration Module Federation transforme l'approche de partage de code entre applications en permettant l'import dynamique de modules distants.

Cette technologie native Webpack 5 simplifie drastiquement l'architecture micro-frontends.

// Configuration Webpack pour le Shell Principal const ModuleFederationPlugin = require('@module-federation/webpack'); const path = require('path'); module.exports = { mode: 'development', entry: './src/index.js', target: 'web', devServer: { port: 3000, historyApiFallback: true, hot: true, headers: { 'Access-Control-Allow-Origin': '', }, }, resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'], alias: { '@shared': path.resolve(_dirname, 'src/shared'), } }, module: { rules: [ { test: /\.m?js$/, type: 'javascript/auto', resolve: { fullySpecified: false, }, }, { test: /\.(css|s[ac]ss)$/, use: ['style-loader', 'css-loader', 'postcss-loader'], }, { test: /\.(ts|tsx|js|jsx)$/, exclude: /nodemodules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-react', '@babel/preset-env'], plugins: ['@babel/plugin-transform-runtime'] }, }, }, ], }, plugins: [ new ModuleFederationPlugin({ name: 'shell', filename: 'remoteEntry.js', // Modules exposés par le shell exposes: { './SharedComponents': './src/shared/components', './EventBus': './src/shared/eventBus', './StateManager': './src/shared/stateManager', './Router': './src/shared/router', './Theme': './src/shared/theme' }, // Applications distantes consommées remotes: { auth: 'auth@http://localhost:3001/remoteEntry.js', dashboard: 'dashboard@http://localhost:3002/remoteEntry.js', ecommerce: 'ecommerce@http://localhost:3003/remoteEntry.js', admin: 'admin@http://localhost:3004/remoteEntry.js' }, // Dépendances partagées avec versioning shared: { react: { singleton: true, strictVersion: true, requiredVersion: '^18.3.1', eager: true }, 'react-dom': { singleton: true, strictVersion: true, requiredVersion: '^18.3.1', eager: true }, 'react-router-dom': { singleton: true, requiredVersion: '^6.0.0' }, '@emotion/react': { singleton: true }, '@emotion/styled': { singleton: true }, 'framer-motion': { singleton: true } } }), // Plugin de nettoyage des bundles new (require('clean-webpack-plugin').CleanWebpackPlugin)(), // Génération du manifest pour le monitoring new (require('webpack-manifest-plugin').WebpackManifestPlugin)({ fileName: 'microfrontend-manifest.json', publicPath: '/', generate: (seed, files, entrypoints) => { const manifestFiles = files.reduce((manifest, file) => { manifest[file.name] = file.path; return manifest; }, seed); const entrypointFiles = entrypoints.main.filter( fileName => !fileName.endsWith('.map') ); return { files: manifestFiles, entrypoints: entrypointFiles, version: process.env.BUILDVERSION || 'development', buildTime: new Date().toISOString() }; } }) ], optimization: { splitChunks: { chunks: 'async', cacheGroups: { default: false, vendor: { name: 'vendor', chunks: 'initial', test: /[\\/]nodemodules[\\/]/, priority: 10, enforce: true } } } } }; // Configuration pour un micro-frontend (Auth) module.exports = { mode: 'development', entry: './src/index.js', target: 'web', devServer: { port: 3001, historyApiFallback: true, hot: true, headers: { 'Access-Control-Allow-Origin': '', }, }, plugins: [ new ModuleFederationPlugin({ name: 'auth', filename: 'remoteEntry.js', // Modules exposés exposes: { './AuthApp': './src/AuthApp', './LoginForm': './src/components/LoginForm', './RegisterForm': './src/components/RegisterForm', './UserProfile': './src/components/UserProfile' }, // Consommation des modules partagés du shell remotes: { shell: 'shell@http://localhost:3000/remoteEntry.js' }, // Partage des dépendances avec le shell shared: { react: { singleton: true, eager: false }, 'react-dom': { singleton: true, eager: false }, 'react-router-dom': { singleton: true }, } }), ], // Optimisations spécifiques au micro-frontend optimization: { splitChunks: { chunks: 'async', minSize: 20000, maxSize: 244000, cacheGroups: { default: { minChunks: 2, priority: -20, reuseExistingChunk: true }, vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: -10, chunks: 'all' } } } } }; // Composant d'intégration React pour Module Federation const AuthApp = React.lazy(() => import('auth/AuthApp')); const DashboardApp = React.lazy(() => import('dashboard/DashboardApp')); function MicrofrontendLoader({ name, fallback = <div>Loading...</div>, onError }) { const [hasError, setHasError] = useState(false); const [retryCount, setRetryCount] = useState(0); const maxRetries = 3; const handleError = useCallback((error, errorInfo) => { console.error(Microfrontend ${name} failed to load:, error, errorInfo); setHasError(true); if (onError) { onError(error, errorInfo, retryCount); } // Auto-retry avec backoff if (retryCount < maxRetries) { setTimeout(() => { setHasError(false); setRetryCount(prev => prev + 1); }, Math.pow(2, retryCount) * 1000); } }, [name, onError, retryCount, maxRetries]); const ErrorFallback = useCallback(({ error, resetErrorBoundary }) => ( <div className="microfrontend-error"><h3>Une erreur est survenue dans {name}</h3><details><summary>Détails techniques</summary><pre>{error.message}</pre></details><button onClick={resetErrorBoundary}> Réessayer ({maxRetries - retryCount} tentatives restantes) </button></div> ), [name, retryCount, maxRetries]); if (hasError && retryCount >= maxRetries) { return <ErrorFallback error={new Error(Failed to load ${name})} />; } return ( <ErrorBoundary FallbackComponent={ErrorFallback} onError={handleError} resetKeys={[retryCount]} ><Suspense fallback={fallback}> {name === 'auth' && <AuthApp />} {name === 'dashboard' && <DashboardApp />} </Suspense></ErrorBoundary> ); }

Testing et qualité dans un écosystème distribué Les tests dans une architecture micro-frontends

nécessitent des stratégies multi-niveaux couvrant les tests unitaires, d'intégration et end-to-end. L'orchestration des tests across les équipes devient critique.

// Framework de tests d'intégration micro-frontends import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { jest } from '@jest/globals'; class MicrofrontendTestEnvironment { constructor() { this.mockedRemotes = new Map(); this.eventBus = null; this.sharedState = null; this.shell = null; } // Configuration de l'environnement de test async setup() { // Mock des modules distants this.mockRemoteModules(); // Initialisation du shell de test this.shell = new MicrofrontendShell(); await this.shell.initializeShell(); // Configuration de l'état partagé de test this.sharedState = this.shell.sharedState; this.eventBus = this.shell.eventBus; // État initial pour les tests this.sharedState.set('user.permissions', ['user']); this.sharedState.set('user.profile', { id: 'test-user', email: '[email protected]', name: 'Test User' }); } // Mock sophistiqué des modules distants mockRemoteModules() { // Mock de l'application d'authentification const AuthAppMock = jest.fn(() => ({ mount: jest.fn(), unmount: jest.fn(), navigate: jest.fn(), getState: jest.fn(() => ({ user: null })) })); // Mock de l'application dashboard const DashboardAppMock = jest.fn(() => ({ mount: jest.fn(), unmount: jest.fn(), updateData: jest.fn(), getMetrics: jest.fn(() => ({ visitors: 1000 })) })); // Configuration des mocks globaux global._webpackinit_sharing = jest.fn(); global.webpacksharescopes__ = { default: {} }; window.auth = { init: jest.fn(), get: jest.fn().mockImplementation((module) => { if (module === './AuthApp') { return Promise.resolve(AuthAppMock); } return Promise.reject(new Error(Module ${module} not found)); }) }; window.dashboard = { init: jest.fn(), get: jest.fn().mockImplementation((module) => { if (module === './DashboardApp') { return Promise.resolve(DashboardAppMock); } return Promise.reject(new Error(Module ${module} not found)); }) }; this.mockedRemotes.set('auth', AuthAppMock); this.mockedRemotes.set('dashboard', DashboardAppMock); } // Tests d'intégration des communications async testInterAppCommunication() { describe('Inter-App Communication', () => { test('should handle user login event across apps', async () => { // Simulation de la connexion utilisateur const userLoginData = { id: 'user-123', email: '[email protected]', permissions: ['user'] }; // Émission de l'événement de connexion this.eventBus.emit('user:login', userLoginData); // Vérification de la propagation dans l'état partagé await waitFor(() => { expect(this.sharedState.get('user.profile')).toEqual(userLoginData); }); // Vérification de la réaction des micro-frontends mockés const authApp = this.mockedRemotes.get('auth'); expect(authApp).toHaveBeenCalled(); }); test('should synchronize state changes between microfrontends', async () => { const testData = { count: 42, timestamp: Date.now() }; // Modification d'état depuis un micro-frontend this.sharedState.sync('test.data', testData, 'dashboard'); // Vérification de la synchronisation expect(this.sharedState.get('test.data')).toEqual(testData); // Simulation de la réception par un autre micro-frontend const subscribers = []; const unsubscribe = this.sharedState.subscribe('test.data', (value) => { subscribers.push(value); }); this.sharedState.sync('test.data', { ...testData, count: 43 }, 'auth'); expect(subscribers).toContainEqual({ ...testData, count: 43 }); unsubscribe(); }); }); } // Tests de performance et charge async testPerformance() { describe('Performance Tests', () => { test('should load microfrontends within performance budget', async () => { const startTime = performance.now(); // Chargement simulé de multiples micro-frontends const loadPromises = ['auth', 'dashboard'].map(name => this.shell.loadMicrofrontend(name) ); await Promise.all(loadPromises); const endTime = performance.now(); const loadTime = endTime - startTime; // Vérification du budget performance (< 3s) expect(loadTime).toBeLessThan(3000); console.log(Microfrontends loaded in ${loadTime.toFixed(2)}ms); }); test('should handle concurrent navigation efficiently', async () => { const navigationTimes = []; // Simulation de navigations concurrentes for (let i = 0; i < 10; i++) { const startTime = performance.now(); await this.shell.router.navigate(/test-route-${i}); const endTime = performance.now(); navigationTimes.push(endTime - startTime); } const avgNavigationTime = navigationTimes.reduce((a, b) => a + b, 0) / navigationTimes.length; // Vérification de la performance de navigation expect(avgNavigationTime).toBeLessThan(100); // < 100ms en moyenne }); }); } // Tests de résilience et error handling async testResilience() { describe('Resilience Tests', () => { test('should gracefully handle microfrontend load failures', async () => { // Simulation d'échec de chargement window.fakeApp = { init: jest.fn().mockRejectedValue(new Error('Network error')), get: jest.fn() }; let caughtError = null; try { await this.shell.loadMicrofrontend('fakeApp'); } catch (error) { caughtError = error; } expect(caughtError).toBeInstanceOf(Error); expect(caughtError.message).toContain('fakeApp'); // Vérification que l'application shell reste fonctionnelle expect(this.shell.eventBus).toBeDefined(); expect(this.shell.sharedState).toBeDefined(); }); test('should isolate errors between microfrontends', async () => { // Simulation d'erreur dans un micro-frontend const errorMessage = 'Simulated microfrontend error'; this.eventBus.emit('error:microfrontend', { source: 'dashboard', error: new Error(errorMessage) }); // Vérification que les autres micro-frontends continuent à fonctionner expect(() => { this.eventBus.emit('user:profile:update', { name: 'Updated Name' }); }).not.toThrow(); // Vérification du state des autres applications expect(this.sharedState.get('user.profile')).toBeDefined(); }); }); } // Nettoyage après les tests cleanup() { // Nettoyage des mocks this.mockedRemotes.clear(); // Reset du DOM document.body.innerHTML = ''; // Nettoyage des timers jest.clearAllTimers(); // Reset des modules mockés jest.resetModules(); } } // Contract Testing pour les APIs inter-microfrontends class ContractTestSuite { constructor() { this.contracts = new Map(); } // Définition des contrats d'interface defineContract(microfrontendName, contract) { this.contracts.set(microfrontendName, { ...contract, version: contract.version || '1.0.0', lastUpdated: new Date().toISOString() }); } // Validation des contrats async validateContract(microfrontendName, implementation) { const contract = this.contracts.get(microfrontendName); if (!contract) { throw new Error(No contract defined for ${microfrontendName}); } const violations = []; // Validation des méthodes exposées if (contract.methods) { for (const method of contract.methods) { if (typeof implementation[method.name] !== 'function') { violations.push(Missing method: ${method.name}); } } } // Validation des événements émis if (contract.events) { // Test d'émission d'événements const eventPromises = contract.events.map(event => this.testEventEmission(implementation, event) ); const eventResults = await Promise.allSettled(eventPromises); eventResults.forEach((result, index) => { if (result.status === 'rejected') { violations.push(Event emission failed: ${contract.events[index].name}); } }); } // Validation du state shape if (contract.stateShape) { const state = implementation.getState?.(); if (state) { const shapeViolations = this.validateStateShape(state, contract.stateShape); violations.push(...shapeViolations); } } return { valid: violations.length === 0, violations, testedAt: new Date().toISOString() }; } validateStateShape(state, expectedShape, path = '') { const violations = []; for (const [key, expectedType] of Object.entries(expectedShape)) { const fullPath = path ? ${path}.${key} : key; const actualValue = state[key]; if (actualValue === undefined) { violations.push(Missing state property: ${fullPath}); continue; } if (typeof expectedType === 'string') { if (typeof actualValue !== expectedType) { violations.push(Type mismatch at ${fullPath}: expected ${expectedType}, got ${typeof actualValue}); } } else if (typeof expectedType === 'object') { const nestedViolations = this.validateStateShape(actualValue, expectedType, fullPath); violations.push(...nestedViolations); } } return violations; } }

Monitoring et observabilité distribuée La surveillance d'une architecture micro-frontends nécessi

te une approche holistique couvrant les performances, erreurs, et expérience utilisateur à travers tous les micro-frontends déployés.

// Système de monitoring distribué pour micro-frontends class DistributedMonitoring { constructor() { this.metrics = new Map(); this.traces = []; this.errors = []; this.userJourneys = new Map(); this.performanceObserver = null; this.initializeMonitoring(); } initializeMonitoring() { // Observer des Core Web Vitals this.setupCoreWebVitalsMonitoring(); // Monitoring des erreurs globales this.setupErrorMonitoring(); // Tracking des interactions utilisateur this.setupUserInteractionTracking(); // Monitoring des ressources réseau this.setupNetworkMonitoring(); // Health checks des micro-frontends this.setupHealthChecks(); } setupCoreWebVitalsMonitoring() { // Largest Contentful Paint new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { this.recordMetric('LCP', entry.startTime, { microfrontend: this.getCurrentMicrofrontend(), element: entry.element?.tagName, url: entry.url }); } }).observe({ entryTypes: ['largest-contentful-paint'] }); // First Input Delay new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { const fid = entry.processingStart - entry.startTime; this.recordMetric('FID', fid, { microfrontend: this.getCurrentMicrofrontend(), eventType: entry.name }); } }).observe({ entryTypes: ['first-input'] }); // Cumulative Layout Shift let clsValue = 0; new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { if (!entry.hadRecentInput) { clsValue += entry.value; } } this.recordMetric('CLS', clsValue, { microfrontend: this.getCurrentMicrofrontend() }); }).observe({ entryTypes: ['layout-shift'] }); } setupErrorMonitoring() { // Erreurs JavaScript window.addEventListener('error', (event) => { this.recordError({ type: 'javascript', message: event.message, filename: event.filename, line: event.lineno, column: event.colno, stack: event.error?.stack, microfrontend: this.getCurrentMicrofrontend(), timestamp: Date.now() }); }); // Erreurs de promesses non gérées window.addEventListener('unhandledrejection', (event) => { this.recordError({ type: 'unhandled-promise', message: event.reason?.message || 'Unhandled promise rejection', stack: event.reason?.stack, microfrontend: this.getCurrentMicrofrontend(), timestamp: Date.now() }); }); // Erreurs de chargement de ressources window.addEventListener('error', (event) => { if (event.target !== window) { this.recordError({ type: 'resource-load', element: event.target.tagName, source: event.target.src || event.target.href, microfrontend: this.getCurrentMicrofrontend(), timestamp: Date.now() }); } }, true); } setupUserInteractionTracking() { // Tracking des clics avec contexte micro-frontend document.addEventListener('click', (event) => { this.recordUserInteraction({ type: 'click', element: event.target.tagName, text: event.target.textContent?.slice(0, 50), microfrontend: this.getCurrentMicrofrontend(), timestamp: Date.now(), coordinates: { x: event.clientX, y: event.clientY } }); }); // Tracking des navigations const originalPushState = history.pushState; history.pushState = (...args) => { this.recordNavigation({ from: window.location.pathname, to: args[2], microfrontend: this.getCurrentMicrofrontend(), timestamp: Date.now(), type: 'programmatic' }); originalPushState.apply(history, args); }; // Tracking du temps passé par section this.setupTimeTraking(); } setupNetworkMonitoring() { // Observation des requêtes réseau new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { if (entry.name.includes('remoteEntry.js')) { this.recordMicrofrontendLoad({ url: entry.name, loadTime: entry.responseEnd - entry.requestStart, size: entry.transferSize || 0, cached: entry.transferSize === 0, timestamp: Date.now() }); } } }).observe({ entryTypes: ['navigation', 'resource'] }); // Monitoring des WebSocket connections pour les micro-frontends this.setupWebSocketMonitoring(); } setupHealthChecks() { setInterval(() => { this.performHealthChecks(); }, 30000); // Toutes les 30 secondes } async performHealthChecks() { const healthData = { timestamp: Date.now(), shell: { status: 'healthy', memory: performance.memory ? { used: performance.memory.usedJSHeapSize, total: performance.memory.totalJSHeapSize, limit: performance.memory.jsHeapSizeLimit } : null, activeMicrofrontends: this.getActiveMicrofrontends() }, microfrontends: {} }; // Health check pour chaque micro-frontend actif for (const [name, app] of this.getActiveMicrofrontends()) { try { const health = await this.checkMicrofrontendHealth(name, app); healthData.microfrontends[name] = health; } catch (error) { healthData.microfrontends[name] = { status: 'unhealthy', error: error.message, lastSeen: this.getLastSeenTime(name) }; } } this.recordHealthCheck(healthData); // Alertes pour les micro-frontends en échec this.checkHealthAlerts(healthData); } async checkMicrofrontendHealth(name, app) { const startTime = Date.now(); try { // Test de ping si disponible if (app.ping) { await app.ping(); } // Vérification de l'état const state = app.getState?.() || {}; return { status: 'healthy', responseTime: Date.now() - startTime, state: Object.keys(state), lastActivity: this.getLastActivity(name) }; } catch (error) { return { status: 'degraded', error: error.message, responseTime: Date.now() - startTime }; } } // Analytics avancées pour l'expérience utilisateur generateUserJourneyAnalytics() { const journeys = Array.from(this.userJourneys.values()); return { totalJourneys: journeys.length, averageJourneyTime: this.calculateAverageJourneyTime(journeys), mostCommonPaths: this.findMostCommonPaths(journeys), dropoffPoints: this.identifyDropoffPoints(journeys), microfrontendUsage: this.analyzeMicrofrontendUsage(journeys), conversionFunnels: this.analyzeConversionFunnels(journeys) }; } analyzeMicrofrontendUsage(journeys) { const usage = {}; journeys.forEach(journey => { journey.interactions.forEach(interaction => { const mf = interaction.microfrontend; if (!usage[mf]) { usage[mf] = { visits: 0, totalTime: 0, interactions: 0, errors: 0 }; } usage[mf].visits++; usage[mf].totalTime += interaction.duration || 0; usage[mf].interactions++; if (interaction.errors) { usage[mf].errors += interaction.errors.length; } }); }); // Calcul des métriques dérivées Object.keys(usage).forEach(mf => { const data = usage[mf]; data.averageTime = data.totalTime / data.visits; data.errorRate = data.errors / data.interactions; data.engagementScore = this.calculateEngagementScore(data); }); return usage; } // Dashboard temps réel createRealTimeDashboard() { return { getCurrentMetrics: () => ({ activeUsers: this.userJourneys.size, activeMicrofrontends: this.getActiveMicrofrontends().size, errorRate: this.calculateErrorRate(), averageLoadTime: this.calculateAverageLoadTime(), memoryUsage: performance.memory?.usedJSHeapSize || 0 }), getErrorSummary: () => { const recentErrors = this.errors.filter( error => Date.now() - error.timestamp < 300000 // 5 minutes ); return { total: recentErrors.length, byMicrofrontend: this.groupErrorsByMicrofrontend(recentErrors), byType: this.groupErrorsByType(recentErrors), critical: recentErrors.filter(error => error.severity === 'critical') }; }, getPerformanceTrends: () => { const metrics = ['LCP', 'FID', 'CLS']; const trends = {}; metrics.forEach(metric => { const recentData = this.getRecentMetrics(metric, 3600000); // 1 hour trends[metric] = { current: this.getLatestMetric(metric), trend: this.calculateTrend(recentData), p95: this.calculatePercentile(recentData, 0.95) }; }); return trends; } }; } // Export des données pour analyse exportMonitoringData(timeRange = 3600000) { // 1 hour par défaut const cutoff = Date.now() - timeRange; return { exportedAt: new Date().toISOString(), timeRange: timeRange, metrics: this.metrics, errors: this.errors.filter(error => error.timestamp >= cutoff), traces: this.traces.filter(trace => trace.timestamp >= cutoff), userJourneys: Array.from(this.userJourneys.values()) .filter(journey => journey.startTime >= cutoff), healthChecks: this.healthChecks.filter(check => check.timestamp >= cutoff) }; } } // Instance globale de monitoring export const distributedMonitoring = new DistributedMonitoring(); // Hook React pour l'utilisation du monitoring export function useDistributedMonitoring() { const [metrics, setMetrics] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const updateMetrics = () => { const dashboard = distributedMonitoring.createRealTimeDashboard(); setMetrics({ current: dashboard.getCurrentMetrics(), errors: dashboard.getErrorSummary(), performance: dashboard.getPerformanceTrends() }); setIsLoading(false); }; // Mise à jour initiale updateMetrics(); // Mise à jour périodique const interval = setInterval(updateMetrics, 5000); return () => clearInterval(interval); }, []); return { metrics, isLoading }; }

Déploiement et CI/CD pour micro-frontends Le déploiement d'une architecture micro-frontends néces

site des pipelines sophistiqués gérant l'indépendance des équipes tout en assurant la cohérence globale du système.

# Pipeline CI/CD pour le Shell Principal name: Shell Application CI/CD on: push: branches: [main, develop] paths: ['shell/'] pull_request: branches: [main] paths: ['shell/'] env: NODEVERSION: '18' SHELLPORT: 3000 jobs: test-shell: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODEVERSION }} cache: 'npm' cache-dependency-path: shell/package-lock.json - name: Install dependencies run: | cd shell npm ci - name: Run tests run: | cd shell npm run test:coverage - name: Upload coverage uses: codecov/codecov-action@v3 with: file: shell/coverage/lcov.info flags: shell integration-tests: runs-on: ubuntu-latest needs: test-shell services: # Mock services pour les micro-frontends auth-mock: image: nginx:alpine ports: - 3001:80 volumes: - ./test/mocks/auth:/usr/share/nginx/html dashboard-mock: image: nginx:alpine ports: - 3002:80 volumes: - ./test/mocks/dashboard:/usr/share/nginx/html steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODEVERSION }} cache: 'npm' - name: Install dependencies run: | cd shell npm ci - name: Start Shell Application run: | cd shell npm run build npm run serve & sleep 10 - name: Run integration tests run: | cd shell npm run test:integration - name: Run E2E tests run: | npx playwright test --config=shell/playwright.config.js contract-validation: runs-on: ubuntu-latest needs: integration-tests steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Validate contracts run: | # Téléchargement des contrats des micro-frontends curl -L https://contracts.microfrontends.com/auth/latest > contracts/auth.json curl -L https://contracts.microfrontends.com/dashboard/latest > contracts/dashboard.json # Validation des contrats avec le shell npm run validate:contracts deploy-shell: runs-on: ubuntu-latest needs: [test-shell, integration-tests, contract-validation] if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODEVERSION }} cache: 'npm' - name: Build for production run: | cd shell npm ci npm run build:prod env: REACTAPPVERSION: ${{ github.sha }} REACTAPPBUILDTIME: ${{ github.event.headcommit.timestamp }} - name: Deploy to CDN run: | # Upload vers S3/CloudFront aws s3 sync shell/dist/ s3://microfrontend-shell-prod/ --delete aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONTDISTRIBUTION_ID }} --paths "/" - name: Update deployment registry run: | curl -X POST https://registry.microfrontends.com/deployments \ -H "Authorization: Bearer ${{ secrets.REGISTRYTOKEN }}" \ -H "Content-Type: application/json" \ -d '{ "service": "shell", "version": "${{ github.sha }}", "timestamp": "${{ github.event.headcommit.timestamp }}", "buildUrl": "${{ github.serverurl }}/${{ github.repository }}/actions/runs/${{ github.runid }}" }' # Pipeline pour un micro-frontend individuel (Auth) name: Auth Microfrontend CI/CD on: push: branches: [main, develop] paths: ['microfrontends/auth/'] pullrequest: branches: [main] jobs: test-auth: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' cache: 'npm' cache-dependency-path: microfrontends/auth/package-lock.json - name: Install dependencies run: | cd microfrontends/auth npm ci - name: Run unit tests run: | cd microfrontends/auth npm run test:coverage - name: Contract testing run: | cd microfrontends/auth npm run test:contract - name: Build microfrontend run: | cd microfrontends/auth npm run build env: MFVERSION: ${{ github.sha }} deploy-auth: runs-on: ubuntu-latest needs: test-auth if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 - name: Build for production run: | cd microfrontends/auth npm ci npm run build:prod env: MFVERSION: ${{ github.sha }} NODEENV: production - name: Deploy with blue-green strategy run: | # Déploiement vers l'environnement de staging aws s3 sync microfrontends/auth/dist/ s3://mf-auth-staging-${{ github.sha }}/ # Test de smoke sur la nouvelle version curl -f https://mf-auth-staging-${{ github.sha }}.s3.amazonaws.com/remoteEntry.js # Basculement vers la production aws s3 sync s3://mf-auth-staging-${{ github.sha }}/ s3://mf-auth-prod/ --delete # Invalidation du cache CDN aws cloudfront create-invalidation --distribution-id ${{ secrets.AUTHMFDISTRIBUTION_ID }} --paths "/" - name: Publish contract run: | # Publication du contrat d'interface curl -X POST https://contracts.microfrontends.com/auth \ -H "Authorization: Bearer ${{ secrets.REGISTRYTOKEN }}" \ -H "Content-Type: application/json" \ -d @microfrontends/auth/contract.json # Orchestrateur de déploiement pour l'écosystème complet name: Ecosystem Deployment Orchestrator on: workflowdispatch: inputs: deploymenttype: description: 'Type of deployment' required: true default: 'rolling' type: choice options: - rolling - blue-green - canary components: description: 'Components to deploy (comma-separated)' required: true default: 'shell,auth,dashboard' type: string jobs: orchestrate-deployment: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Parse deployment configuration id: config run: | echo "components=$(echo '${{ github.event.inputs.components }}' | tr ',' ' ')" >> $GITHUBOUTPUT echo "deploymenttype=${{ github.event.inputs.deploymenttype }}" >> $GITHUBOUTPUT - name: Validate deployment compatibility run: | # Vérification de la compatibilité entre versions ./scripts/validate-compatibility.sh ${{ steps.config.outputs.components }} - name: Rolling deployment if: steps.config.outputs.deploymenttype == 'rolling' run: | for component in ${{ steps.config.outputs.components }}; do echo "Deploying $component with rolling strategy" ./scripts/deploy-rolling.sh $component # Health check après chaque déploiement ./scripts/health-check.sh $component if [ $? -ne 0 ]; then echo "Deployment failed for $component, rolling back" ./scripts/rollback.sh $component exit 1 fi done - name: Blue-green deployment if: steps.config.outputs.deploymenttype == 'blue-green' run: | # Déploiement simultané vers l'environnement vert for component in ${{ steps.config.outputs.components }}; do ./scripts/deploy-blue-green.sh $component & done wait # Attendre tous les déploiements # Tests d'intégration sur l'environnement vert ./scripts/integration-tests.sh green if [ $? -eq 0 ]; then # Basculement du trafic ./scripts/switch-traffic.sh blue green else echo "Integration tests failed, keeping blue environment" exit 1 fi - name: Canary deployment if: steps.config.outputs.deploymenttype == 'canary' run: | # Déploiement canary avec augmentation progressive du trafic for component in ${{ steps.config.outputs.components }}; do ./scripts/deploy-canary.sh $component # Monitoring des métriques pendant 5 minutes ./scripts/monitor-canary.sh $component 300 if [ $? -eq 0 ]; then ./scripts/promote-canary.sh $component else ./scripts/rollback-canary.sh $component fi done - name: Update service registry run: | # Mise à jour du registre des services for component in ${{ steps.config.outputs.components }}; do ./scripts/register-service.sh $component ${{ github.sha }} done - name: Notify teams run: | # Notification aux équipes du succès du déploiement ./scripts/notify-deployment.sh "${{ steps.config.outputs.components }}" success

Outils et écosystème recommandés L'écosystème micro-frontends mature rapidement avec des outils

spécialisés optimisant le développement, déploiement et monitoring des architectures distribuées. - Module Federation : Partage de code natif Webpack 5 - Single-SPA : Framework d'orchestration micro-frontends mature - Bit : Plateforme de composants distribuée avec versioning - Qiankun : Solution micro-frontends basée sur Single-SPA - Nx : Monorepo tooling pour micro-frontends à grande échelle L'architecture micro-frontends révolutionne le développement d'applications web complexes en permettant l'indépendance technique des équipes tout en maintenant une expérience utilisateur cohérente. Cette approche constitue un investissement stratégique pour les organisations nécessitant une scalabilité technique et organisationnelle maximale.