Chapitre 14
Chapitre 14 — Micro-Frontends
Chapitre 14 — Micro-Frontends
Micro-Frontends — Cours complet
1. Pourquoi les micro-frontends ?
Le problème des monolithes front-end
Une application front-end monolithique typique (ex : dashboard e-commerce) :
- Codebase unique : 500 000+ lignes, 2000+ composants.
- Équipe unique : 15 développeurs sur le même repo → conflits de merge, PR longues.
- Release unique : une seule pipeline, déploiement complet à chaque fois.
- Technologie unique : tout le monde doit utiliser React (impossible d'expérimenter avec Vue ou Svelte).
- Scaling limité : le bundle grossit, le build ralentit.
Avantages des micro-frontends
| Problème monolithe | Solution micro-frontends |
|---|---|
| Codebase massive | Codebase divisée par domaine |
| Conflits de merge | Repos indépendants |
| Release lente | Déploiement indépendant |
| Technologie unique | Choix tech par équipe |
| Bundle lourd | Chargement à la demande |
Quand adopter les micro-frontends ?
- Équipe de 5+ développeurs front-end.
- Application avec domaines métiers clairement séparés (catalogue, panier, compte).
- Cycles de release différents par domaine.
- Migration progressive vers une nouvelle stack.
- N'utilisez PAS si : petite équipe, application simple, pas de contrainte de scaling.
2. Stratégies d'intégration
2.1 iframes
<iframe src="http://micro-app-panier.example.com"></iframe>
Problèmes :
- Pas de communication native entre iframes (nécessite
postMessage). - Pas de SEO (contenu non indexé).
- Pas de responsive design intégré.
- Rechargement complet à chaque navigation.
- Accessibilité dégradée.
- Performances (iframe = page entière à charger).
À éviter sauf cas très spécifiques (intégration de contenu tiers non modifiable).
2.2 Web Components
class MonWidget extends HTMLElement {
connectedCallback() {
this.innerHTML = `<div>Mon widget</div>`;
this.addEventListener('click', () => { ... });
}
}
customElements.define('mon-widget', MonWidget);
<mon-widget data-product-id="123"></mon-widget>
Avantages :
- Standard du Web (natif, pas de framework).
- Framework-agnostic (fonctionne avec React, Vue, Angular).
- CSS isolé (Shadow DOM).
Inconvénients :
- Pas de SSR simple.
- Bundle plus lourd (polyfills pour vieux navigateurs).
- Interopérabilité limitée avec les frameworks (propriétés React vs attributs HTML).
- Pas de gestion d'état intégrée.
2.3 Module Federation (Webpack 5)
La solution la plus populaire en 2024.
Principe : Un bundle peut exposer des modules (Remote) qu'un autre bundle peut consommer (Host).
// webpack.config.js (Remote - app panier)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'panier',
filename: 'remoteEntry.js',
exposes: {
'./Cart': './src/Cart',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true },
},
}),
],
};
// webpack.config.js (Host - shell)
new ModuleFederationPlugin({
name: 'shell',
remotes: {
panier: 'panier@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
});
// Host : utilisation du remote
const Cart = React.lazy(() => import('panier/Cart'));
function App() {
return (
<Suspense fallback={<div>Chargement panier...</div>}>
<Cart />
</Suspense>
);
}
Avantages :
- Intégration profonde (partage de librairies, contexte React).
- Chargement à la demande (lazy loading).
- Déploiement indépendant.
- Écosystème mature.
Inconvénients :
- Webpack uniquement (pas Vite sans adaptateur).
- Complexité de configuration.
- Gestion des versions partagées (singleton conflicts).
- Bundle size si mauvais partage.
2.4 SingleSPA
Framework orchestrateur qui permet de combiner plusieurs frameworks dans une même page.
// root-config
import { registerApplication, start } from 'single-spa';
registerApplication({
name: 'app-vue',
app: () => import('app-vue'), // Module Federation ou import distant
activeWhen: '/vue',
});
registerApplication({
name: 'app-react',
app: () => import('app-react'),
activeWhen: '/react',
});
start();
Avantages :
- Multi-framework natif.
- Routing géré par l'orchestrateur.
- Cycle de vie standardisé (mount, unmount, bootstrap).
Inconvénients :
- Complexité (concepts d'orchestration).
- Overhead de communication.
- Écosystème moins actif que Module Federation.
- Courbe d'apprentissage.
2.5 Podium (FINN/DB)
Développé par FINN (Norvège) et adopté par la DB (Allemagne).
// Serveur Podium
const podlet = new Podlet({
name: 'header',
version: '1.0.0',
pathname: '/header',
content: '/header/content',
fallback: '/header/fallback',
});
Avantages :
- SSR natif.
- Layout server client (Podium server assemble les fragments).
- Versioning explicite.
- CSS isolation via Shadow DOM.
Inconvénients :
- Écosystème très petit.
- Nécessite un serveur Podium.
- Moins adapté au CSR pur.
3. Communication entre micro-frontends
3.1 Événements personnalisés (Custom Events)
// Micro-frontend A : émet
window.dispatchEvent(
new CustomEvent('product:selected', { detail: { productId: '123' } })
);
// Micro-frontend B : écoute
window.addEventListener('product:selected', (event) => {
const { productId } = event.detail;
// Ajouter au panier
});
Avantages : simple, découplé, natif. Inconvénients : pas de typage, pas évident à débugger.
3.2 Shared State (Global Store)
// shared-store.ts (package partagé)
type Event = { type: string; payload: unknown };
class SharedStore {
private listeners: Map<string, Set<(payload: unknown) => void>> = new Map();
emit(type: string, payload: unknown) {
this.listeners.get(type)?.forEach(fn => fn(payload));
}
on(type: string, fn: (payload: unknown) => void) {
if (!this.listeners.has(type)) {
this.listeners.set(type, new Set());
}
this.listeners.get(type)!.add(fn);
}
off(type: string, fn: (payload: unknown) => void) {
this.listeners.get(type)?.delete(fn);
}
}
export const store = new SharedStore();
Avantages : typé, testable, centralisé. Inconvénients : couplage fort au store, dépendance partagée.
3.3 GlobalThis / Window
// Éviter — pas de typage, pollution globale
window.__myApp = window.__myApp || {};
window.__myApp.user = { id: 1, name: 'John' };
3.4 URL comme bus de communication
// Micro-frontend A
const params = new URLSearchParams(window.location.search);
params.set('productId', '123');
window.history.pushState({}, '', `?${params}`);
// Micro-frontend B
window.addEventListener('popstate', () => {
const productId = new URLSearchParams(window.location.search).get('productId');
});
Avantages : partagé, bookmarkable, pas de dépendance. Inconvénients : limité aux données sérialisables en URL.
4. Isolation CSS
Shadow DOM
class MonWidget extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot!.innerHTML = `
<style>
.btn { background: blue; color: white; }
</style>
<button class="btn">Mon bouton</button>
`;
}
}
- Isolation totale du CSS.
- Pas de fuite de styles.
- Les styles du parent ne s'appliquent pas à l'intérieur du Shadow DOM (et vice versa).
Inconvénient : composants par framework non compatibles Shadow DOM (React ne rend pas dans shadowRoot nativement).
CSS Custom Properties
/* Shell : définir les tokens */
:root {
--color-primary: #0070f3;
--spacing-unit: 8px;
--font-family: 'Inter', sans-serif;
}
/* Micro : utiliser les tokens */
.my-component {
color: var(--color-primary);
margin: var(--spacing-unit);
}
Avantage : partage de design tokens sans couplage CSS. Inconvénient : pas d'isolation réelle (le micro peut modifier les variables globales).
Scoped Styles (CSS Modules, CSS-in-JS)
// app-header/src/Header.module.css
.header { background: black; }
.logo { color: white; }
import styles from './Header.module.css';
export function Header() {
return (
<header className={styles.header}>
<span className={styles.logo}>MonApp</span>
</header>
);
}
- Noms de classe hashés → pas de collision.
- Simple, performant.
- Ne protège pas contre les styles globaux (reset, typographie).
Naming Convention (BEM)
.mfe-header__logo { ... }
.mfe-cart__button { ... }
Avantage : simple, compréhensible par tous. Inconvénient : pas d'isolation réelle (convention non enforce).
5. Routage
Routage dans le Shell
Le Shell (application hôte) gère le routing global et active/désactive les micros.
// Shell : app-routing.tsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { lazy, Suspense } from 'react';
// Les micros sont chargés à la demande
const ProductCatalog = lazy(() => import('catalog/App'));
const Cart = lazy(() => import('cart/App'));
const UserAccount = lazy(() => import('account/App'));
const router = createBrowserRouter([
{ path: '/', element: <Home /> },
{ path: '/catalog/*', element: <ProductCatalog /> },
{ path: '/cart/*', element: <Cart /> },
{ path: '/account/*', element: <UserAccount /> },
]);
Chaque micro gère son propre sous-routage :
// catalog/src/App.tsx
import { Routes, Route, useNavigate } from 'react-router-dom';
export default function CatalogApp() {
// Le micro utilise le routeur du shell via la base /catalog
return (
<Routes>
<Route index element={<ProductList />} />
<Route path=":slug" element={<ProductDetail />} />
<Route path="search" element={<SearchResults />} />
</Routes>
);
}
Navigation inter-micros
// Shell : fournit un navigateur partagé
// shared/services/navigation.ts
export function navigate(to: string) {
window.history.pushState({}, '', to);
window.dispatchEvent(new PopStateEvent('popstate'));
}
// Micro catalogue
import { navigate } from '@shared/navigation';
function ProductCard({ product }: { product: Product }) {
return (
<button onClick={() => navigate(`/cart?add=${product.id}`)}>
Ajouter au panier
</button>
);
}
6. Testing des micro-frontends
Tests unitaires (par micro)
Chaque micro a ses propres tests (vitest, jest) — indépendants et isolés.
Tests d'intégration (integration testing)
Tester la communication entre micros :
describe('Communication inter-micros', () => {
it('should add product to cart when event is emitted', () => {
const store = new SharedStore();
const addToCart = vi.fn();
store.on('cart:add', addToCart);
store.emit('cart:add', { productId: '123' });
expect(addToCart).toHaveBeenCalledWith({ productId: '123' });
});
});
Tests end-to-end (Cypress, Playwright)
Tester le Shell et les micros ensemble dans un navigateur.
describe('Micro-frontends intégrés', () => {
beforeEach(() => {
cy.visit('http://shell.local:3000');
});
it('should navigate from catalog to cart', () => {
cy.get('[data-testid="product-card"]').first().click();
cy.get('[data-testid="add-to-cart"]').click();
cy.get('[data-testid="cart-count"]').should('contain', '1');
});
});
Stratégies
- Isolation : chaque micro testé indépendamment avec ses mocks.
- Convergence : tests d'intégration sur le protocole de communication.
- E2E : parcours critiques dans l'application assemblée.
- Contract testing : vérifier que les contrats d'interface sont respectés.
7. Déploiement
Déploiement indépendant
Chaque micro a sa propre pipeline CI/CD :
# .github/workflows/catalog-deploy.yml
on:
push:
branches: [main]
paths:
- 'micros/catalog/**' # Déclenché uniquement pour le micro catalogue
jobs:
deploy-catalog:
steps:
- build
- test
- deploy
- Versioning : chaque micro versionné indépendamment.
- URL : chaque micro déployé sur une URL distincte.
- Rollback : possible sans impacter les autres micros.
Versioning des remotes
// Shell : version flexible
remotes: {
catalog: 'catalog@http://cdn.example.com/catalog/v1.2.0/remoteEntry.js',
cart: 'cart@http://cdn.example.com/cart/v2.0.1/remoteEntry.js',
}
Stratégie de déploiement
- Déployer le remote d'abord.
- Mettre à jour la référence dans le host (ou utiliser la version courante).
- Déployer le host si nécessaire.
8. Pattern App Shell
Le App Shell est l'architecture recommandée pour les micro-frontends :
┌─────────────────────────────────────┐
│ App Shell (hôte) │
│ ├─ Header (micro) │
│ ├─ Navigation (micro) │
│ ├─ Content Area (micro actif) │
│ │ ├─ /catalog → Catalog Micro │
│ │ ├─ /cart → Cart Micro │
│ │ └─ /account → Account Micro │
│ └─ Footer (micro) │
└─────────────────────────────────────┘
Le Shell est responsable de :
- L'authentification (SSO).
- Le routing global.
- L'injection des micros.
- Le thème et les tokens de design.
- Les services partagés (logger, analytics, store).
9. Choisir sa stratégie
| Stratégie | Quand l'utiliser | Points forts | Limites |
|---|---|---|---|
| Module Federation | Équipe React, déploiement indépendant | Mature, performant, lazy loading | Webpack only |
| SingleSPA | Multi-framework, orchestration | Multi-framework, cycles de vie | Complexe |
| Web Components | Intégration dans un écosystème varié | Standard, framework-agnostic | SSR, bundle |
| Podium | SSR natif, serveur d'assemblage | SSR, versioning | Petit écosystème |
| iframes | Contenu tiers (jamais pour vos apps) | Isolation totale | SEO, perf, UX |
10. Erreurs classiques
- Micros trop petits : chaque micro doit correspondre à un domaine métier, pas à un composant.
- Micros trop gros : si un micro a 10 pages, il redevient un monolithe.
- Pas de contrat clair : les interfaces entre micros doivent être documentées et versionnées.
- Partage excessif : trop de dépendances partagées → couplage fort, conflits de versions.
- Pas de monitoring : sans tracing distribué, impossible de savoir quel micro cause un bug.
- Ignorer le CSS : les fuites de styles sont le problème #1 en production.
- Déploiement synchronisé : si on doit déployer tous les micros ensemble, on perd l'avantage.
- Pas de feature flags : nécessaire pour les migrations progressives.
11. Conclusion
Les micro-frontends sont une réponse architecturale puissante à la complexité des grandes applications web. Module Federation est la solution la plus mature en 2024 pour les équipes React. L'adoption doit être motivée par des besoins concrets (taille d'équipe, indépendance des releases) et non par la mode. Une bonne architecture micro-frontends repose sur des contrats clairs, une isolation CSS robuste et une stratégie de communication bien définie.