Un micro frontend est un morceau d’UI construit, testé et livré indépendamment par une équipe, puis composé avec les autres à l’exécution, dans le navigateur. Le mot qui compte est « indépendamment » : si deux parties doivent être livrées ensemble, ce ne sont pas des micro frontends mais des modules d’une même application — aussi séparés que soient leurs dossiers.
Cette architecture résout un problème organisationnel, pas technique : plusieurs équipes autonomes qui veulent livrer à des rythmes différents sans se bloquer mutuellement. Le prix se paie en complexité à l’exécution : versions des dépendances, échecs d’intégration, cohérence visuelle, tests, observabilité.
- Les micro frontends résolvent un problème d’organisation (des équipes qui livrent indépendamment), pas un problème technique.
- Le prix est réel : plus de JavaScript, des versions de dépendances à garder alignées, des tests end-to-end plus difficiles.
- Pour Angular avec esbuild, l’option recommandée est Native Federation. Web Components et iframes conviennent quand on mélange des frameworks ou qu’on veut une isolation totale.
- Les contrats entre applications (routes, événements, versions) comptent plus que la technologie. Chaque application chargée à distance a besoin d’un fallback.
- Avec une seule équipe, un monorepo aux frontières claires est presque toujours plus simple.
La plupart des applications Angular, même grandes, sont mieux servies par une seule application dans un monorepo, avec des frontières claires et du lazy loading. Les micro frontends sont la bonne réponse à quelques situations précises, et une source coûteuse de problèmes sinon. Cette page vous aide à les distinguer et, si besoin, à les construire correctement avec Native Federation.
Ce que c’est et ce que ce n’est pas
Un système de micro frontends a trois propriétés, toutes obligatoires :
- Build séparé — chaque partie a son propre pipeline et son propre
package.json(ou au moins sa propre cible de build). - Déploiement séparé — l’équipe Catalog publie une nouvelle version sans que le shell ou l’équipe Cart n’ait à intervenir.
- Composition à l’exécution — un shell (host) ne découvre que dans le navigateur quelle version de chaque remote charger, généralement à partir d’un manifest.
BUILD — PAS un micro frontend
libs/catalog ─┐
libs/cart ─┼─► ng build shell ─► un seul déploiement
libs/reviews ─┘
une version, une release, des contrats vérifiés par le compilateur
EXÉCUTION — micro frontends
catalog → build → deploy → catalog…/remoteEntry.json ─┐
cart → build → deploy → cart…/remoteEntry.json ─┼─► le shell les compose
reviews → build → deploy → reviews…/remoteEntry.json ─┘ dans le navigateur
N versions, N releases, contrats vérifiés seulement à l’exécution| Composition | Comment ça marche | Est-ce un micro frontend ? |
|---|---|---|
| Bibliothèques dans un monorepo | le shell importe libs/catalog ; un seul build, un seul déploiement | non — c’est au build (voir Monorepo) |
Lazy routes, @defer | des chunks séparés, mais produits par le même build | non — la même application |
| Native / Module Federation | les remotes publient des modules ES + des métadonnées ; le shell les charge depuis le manifest et partage les dépendances | oui |
| Web Components | chaque équipe livre un bundle qui définit des balises ; le shell ne fait que les utiliser | oui (voir Angular Elements) |
| iframes | des documents séparés, dans un frame | oui — l’isolation la plus forte |
Vraies raisons, fausses raisons
Les raisons qui justifient le prix concernent presque toutes les personnes et la livraison :
- Des équipes autonomes avec des cadences différentes. L’équipe checkout livre plusieurs fois par jour, celle du catalogue une fois par semaine, et une release commune bloque les deux : une régression dans le catalogue arrête un correctif de paiement urgent.
- Les très grandes organisations. Des dizaines de développeurs frontend sur le même produit, plusieurs domaines métier, chacun avec une responsabilité claire (loi de Conway : l’architecture finit de toute façon par refléter l’organisation).
- La migration progressive (strangler fig). Un nouveau shell « enveloppe » l’ancienne application (AngularJS, un Angular plus ancien, un autre framework) ; les écrans migrent un par un, et le vieux code rétrécit jusqu’à disparaître — sans réécriture « big bang ».
- Des produits séparés sous une seule expérience : acquisitions, unités métier, portails qui regroupent des applications existantes.
Les fausses raisons sonnent bien en présentation, mais ont des solutions moins chères :
| « On veut des MFE pour… » | Pourquoi ça ne tient pas | Ce qui le résout vraiment |
|---|---|---|
| scalabilité / performance | les MFE ajoutent des octets, des requêtes et une négociation de versions au démarrage | lazy routes, @defer, SSR, optimisations |
| un code propre et modulaire | la modularité est une propriété du code, pas du déploiement | bibliothèques + règles d’import (frontières, structure) |
| des builds plus rapides | N pipelines, ce sont N endroits qui peuvent casser | build incrémental et cache (Nx affected) dans un monorepo |
| chaque équipe choisit son propre framework | la diversité technologique est un coût permanent : un design system dupliqué, des personnes difficiles à faire passer d’une équipe à l’autre | à n’accepter que lorsque c’est inévitable (migration, acquisition) |
Si vous ne pouvez pas nommer deux équipes qui se bloquent aujourd’hui au moment de la release, vous n’avez pas le problème que résolvent les micro frontends.
Le prix
Chacun des coûts ci-dessous apparaît en pratique ; la différence entre un système sain et un système fragile, c’est d’avoir un plan dès le premier jour.
| Coût | Ce qui se passe concrètement | Comment le maîtriser |
|---|---|---|
| Versions partagées | un remote construit avec un autre Angular ne peut pas utiliser le runtime du shell ; les montées de version majeure doivent être coordonnées | la même version partout, un calendrier de montées de version commun, une vérification en CI |
| Intégration à l’exécution | un export renommé dans un remote casse le shell en production, pas à la compilation | exposes traité comme une API publique (semver), des tests de contrat, un fallback par remote |
| Bundles dupliqués | tout ce qui n’est pas partagé est téléchargé N fois ; tout ce qui est partagé est construit séparément, donc n’est plus tree-shaké par application | un analyseur de bundle, ne partager que ce qui a du sens |
| Design system | cinq équipes, cinq versions des boutons | le design system comme paquet partagé + tokens CSS, géré par une équipe plateforme |
| Routing | qui possède l’URL, que se passe-t-il sur un lien direct vers un remote | le shell possède les routes de premier niveau, chaque remote exporte des Routes pour son préfixe |
| Auth et session | le token doit atteindre tous les remotes, le refresh ne doit avoir lieu qu’une fois | un cookie HttpOnly ou un Service d’auth venant d’une bibliothèque partagée, en singleton (refresh d’auth) |
| Tests E2E | chaque équipe est au vert isolément, le bug apparaît dans la composition | e2e sur « shell + les versions de production des autres » |
| Observabilité | « erreur en production » — de quel remote, de quelle version ? | la version de chaque remote dans chaque log, des source maps par remote |
| Développement local | vous démarrez cinq serveurs pour travailler sur un seul | un manifest par environnement : le shell local charge les remotes déployés, vous ne lancez que ce que vous modifiez |
Native Federation pas à pas
Native Federation implémente le modèle Module Federation sur des standards du web : les remotes sont de simples modules ES, reliés par une import map. Pour Angular 22, le paquet est @angular-architects/native-federation 22.x — la version suit le major.minor d’Angular — et il reste sur le builder esbuild du CLI (@angular/build:application), qu’il enveloppe. Le projet a déménagé vers l’organisation github.com/native-federation. Le mécanisme, pas à pas :
- Build. Chaque remote produit des modules ES et un
remoteEntry.json: ce qu’il expose (./routes→ le fichier généré) et les dépendances qu’il partage, avecversion,requiredVersion,singleton,strictVersion. - Démarrage du shell.
main.tsappelleinitFederation()avant Angular : il lit le manifest (nom → URL), télécharge les métadonnées de chaque remote, négocie les versions et écrit l’import map (aveces-module-shims). - Bootstrap. Seulement ensuite,
import('./bootstrap')démarre Angular. C’est pourquoi le schematic déplace le bootstrap dans un fichier séparé :@angular/coredoit être résolu via l’import map, il faut donc qu’elle existe d’abord. - Navigation.
loadRemoteModule('catalog', './routes')est, en substance, unimport()dynamique d’une URL sur le serveur de l’équipe Catalog. Quand le code du remote faitimport '@angular/core', l’import map l’envoie vers la copie du shell : un seul Angular, un seul injector racine.
# la version de l’adaptateur suit le major.minor d’Angular (22.2.x pour Angular 22.2)
npm i -D @angular-architects/native-federation
ng g @angular-architects/native-federation:init \
--project catalog --port 4201 --type remote
ng g @angular-architects/native-federation:init \
--project shell --port 4200 --type dynamic-host
# Ce que change le schematic :
# • build/serve → @angular-architects/native-federation:build,
# qui délègue à @angular/build:application (esbuild)
# • main.ts → initFederation(...) puis import('./bootstrap');
# l’ancien bootstrap se retrouve dans bootstrap.ts
# • génère federation.config.mjs (+ public/federation.manifest.json sur dynamic-host)
# • ajoute es-module-shims aux polyfills (import maps avec scopes)
# • avec --webcomponent : bootstrap sous forme de custom element (createApplication + define)// catalog/federation.config.mjs — un remote, tel que généré par la 22.x
import {
withNativeFederation,
fromPackageJson,
} from '@angular-architects/native-federation/config';
export default withNativeFederation({
name: 'catalog',
exposes: {
// l’API publique du remote
'./routes': './projects/catalog/src/app/catalog.routes.ts',
},
shared: fromPackageJson({
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
build: 'package',
})
// tout @angular/core, entry points secondaires compris → une seule copie
.patch(['@angular/core'], { includeSecondaries: { keepAll: true } }),
skip: ['rxjs/ajax', 'rxjs/fetch', 'rxjs/testing', 'rxjs/webSocket'],
features: { denseChunking: true },
});// shell/src/main.ts — s’exécute AVANT Angular :
// lit le manifest, télécharge les fichiers remoteEntry.json, négocie les versions,
// écrit l’import map — et seulement ensuite charge bootstrap.ts (et @angular/core)
import { initFederation } from '@angular-architects/native-federation';
initFederation('federation.manifest.json')
.then((nf) => import('./bootstrap').then((m) => m.bootstrap(nf)))
.catch((err) => console.error(err));- Pour Angular 22, Native Federation v4 revient dans le paquet principal
@angular-architects/native-federation(pour Angular 20–21, il y avait le pont-v4). - Le schematic génère
federation.config.mjsavecfromPackageJson(...);shareAlletsharerestent disponibles. loadRemoteModuleimporté directement depuis le paquet est marqué@deprecated: utilisez celui du résultat deinitFederation(), comme dansapp.routes.ts.
// La forme courte des anciens tutoriels existe encore en 22.x,
// mais elle est marquée @deprecated :
import { loadRemoteModule } from '@angular-architects/native-federation';
const route = {
path: 'catalog',
loadChildren: () =>
loadRemoteModule('catalog', './routes').then((m) => m.CATALOG_ROUTES),
};
// La raison, d’après le d.ts : elle dépend d’une instance globale et du DERNIER initFederation,
// elle est donc fragile dans les tests et avec plusieurs hosts.
// Recommandé : loadRemoteModule issu du résultat d’initFederation (voir app.routes.ts).L’exemple ci-dessous reproduit exactement ce flux. Les serveurs sont simulés, mais chaque « remote » est un vrai composant Angular, placé dans son propre chunk et chargé avec un import() dynamique — le même mécanisme que loadRemoteModule utilise en dessous. Le catalogue et le panier ne communiquent que par des événements sur window ; les avis ne parlent à personne.
Dépendances partagées et versions
Pour chaque paquet de shared, un remote déclare quatre choses : version (avec quoi il a été construit), requiredVersion (ce qu’il accepte ; 'auto' = la plage de package.json, par exemple ^22.2.0), singleton (une seule instance pour tous) et strictVersion (refuse une version hors de la plage).
Pourquoi Angular doit être un singleton. @angular/core n’est pas une bibliothèque de fonctions pures : c’est le runtime qui fait le rendu, l’injector, la Change Detection. Les templates compilés appellent les instructions du runtime du build avec lequel ils ont été compilés, donc un composant d’une copie ne peut pas être rendu par une autre. Même pour une bibliothèque ordinaire, deux copies donnent deux classes différentes : inject(AuthService) dans le remote demande un autre token que celui enregistré par le shell, et votre « singleton » a soudain deux instances.
Ce qui se passe quand les versions ne correspondent pas dépend de l’outil :
| Situation (singleton: true) | Native Federation (orchestrator v4) | Module Federation (webpack) |
|---|---|---|
la version partagée satisfait requiredVersion | le remote utilise la copie partagée ; la version du host a priorité | utilise le singleton |
ne satisfait pas, strictVersion: true | le remote obtient sa propre copie, dans les scopes de l’import map | erreur au chargement : Unsatisfied version … of shared singleton module … |
ne satisfait pas, strictVersion: false | utilise quand même la version partagée + un avertissement | utilise le singleton + console.warn |
Pour Angular, « sa propre copie » ne sauve pas la situation : le remote aurait un autre runtime, donc ses composants ne peuvent pas entrer dans l’arbre du shell. Un remote sur une autre version majeure ne s’intègre qu’en isolation — comme Web Component avec son propre bootstrap, ou dans un iframe. Si vous préférez le découvrir au démarrage plutôt qu’à la navigation, l’orchestrator a l’option strict.strictExternalCompatibility, qui lève une erreur à l’initialisation au lieu de créer des scopes.
// Trois façons d’écrire `shared` dans federation.config.mjs
// 1. le défaut en 22.x : tout ce qui est dans dependencies de package.json, mêmes règles
shared: fromPackageJson({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
// 2. le style classique (toujours exporté)
shared: { ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }) },
// 3. explicite, paquet par paquet
shared: share({
'@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
// a de l’état → singleton
'@ngrx/signals': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
// utilitaire sans état : chaque remote peut avoir sa propre copie
'date-fns': { singleton: false },
}),
// requiredVersion: 'auto' → la plage de package.json (p. ex. "^22.2.0")
// autoShareScope({ level: 'minor' }) → ne partage qu’entre remotes
// de la même ligne de versions (p. ex. "ng22.2") Le shell (host) fournit @angular/core en version 22.2.2, qui a priorité dans le scope global.
| Remote | construit avec | requiredVersion | singleton | strictVersion |
|---|---|---|---|---|
| catalog | ||||
| cart | ||||
| reviews |
@angular/core téléchargées : 2un runtime Angular supplémentaire{
"imports": {
"@angular/core": "https://shell.example/core-22.2.2.js"
},
"scopes": {
"https://cart.example/": {
"@angular/core": "https://cart.example/core-22.1.4.js"
}
}
}- Idéal : exactement la même version d’Angular dans le shell et dans tous les remotes.
^donne de la tolérance entre mineures, mais le code compilé en AOT utilise des instructions internes du runtime — testez la combinaison, ne vous fiez pas à semver. - Le host gagne : à chaque nouvelle mineure, le shell peut donc passer en premier, tant que les plages des remotes l’acceptent. Une nouvelle majeure exige une montée de version synchronisée, ou d’isoler le remote en retard.
- Singleton + strictVersion pour le framework et tout ce qui a de l’état : Angular, router, stores, design system, bibliothèque d’auth. Les utilitaires sans état peuvent rester non-singleton.
autoShareScope()lie le partage à une ligne de versions (par exempleng22.2), pour ne pas mélanger accidentellement des builds incompatibles.
Module Federation, Web Components, iframes
Module Federation (webpack)
@angular-architects/module-federation a une version pour Angular 22, mais c’est la variante « classique », sur webpack : ng add fait passer le projet du builder esbuild à @angular-builders/custom-webpack (à partir d’Angular 22 ; jusqu’à la 21 il utilisait ngx-build-plus), et il vous avertit explicitement que, pour les nouveaux projets, l’alternative est Native Federation. Il existe aussi un builder rspack, marqué expérimental. Ne le choisissez que si vous étendez un système Module Federation existant ou devez partager le runtime avec des remotes déjà construits sur webpack.
// Module Federation classique (webpack) — @angular-architects/module-federation 22.x
const {
shareAll,
withModuleFederationPlugin,
} = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
name: 'catalog',
exposes: {
'./routes': './projects/catalog/src/app/catalog.routes.ts',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});Web Components
Chaque remote démarre sa propre application avec createApplication() et définit une balise ; le shell se contente de la poser sur la page. Cela fonctionne entre versions majeures d’Angular différentes et entre frameworks, car rien n’est partagé. C’est exactement son coût : chaque bundle apporte son propre runtime (≈ 33 kB transférés pour un élément simple, mesurés sur la page Angular Elements), il n’y a pas de DI commune, et la communication passe par des attributs et des événements. Cela se combine bien avec la federation : un remote sur une autre majeure est exposé comme Web Component et chargé quand même via loadRemoteModule.
// Un remote sur une AUTRE version majeure d’Angular (ou un autre framework) : intégré en isolation,
// comme Web Component avec son propre runtime.
//
// remote — federation.config.mjs : exposes: { './Bootstrap': './src/bootstrap.ts' }
// remote — bootstrap.ts :
// createApplication(appConfig).then(({ injector }) =>
// customElements.define('mfe-legacy', createCustomElement(App, { injector })));
//
// shell — une page qui charge le bundle (son effet : define), puis utilise la balise
import { CUSTOM_ELEMENTS_SCHEMA, Component, inject, input, resource } from '@angular/core';
import { NATIVE_FEDERATION } from '../app.config';
@Component({
selector: 'app-legacy-page',
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
@if (bundle.hasValue()) {
<mfe-legacy [attr.customer-id]="customerId()"></mfe-legacy>
} @else if (bundle.error()) {
<p role="alert">
Le module legacy n’est pas disponible.
<button (click)="bundle.reload()">Réessayer</button>
</p>
}
`,
})
export class LegacyPage {
private readonly nf = inject(NATIVE_FEDERATION);
readonly customerId = input.required<string>();
protected readonly bundle = resource({
loader: () => this.nf.loadRemoteModule('legacy', './Bootstrap'),
});
}iframes
L’isolation la plus forte : un autre document, un autre JavaScript, un autre CSS ; les erreurs, les fuites mémoire et les styles restent dans le frame, et avec sandbox vous pouvez exécuter du code auquel vous ne faites pas confiance. Vous la payez en intégration : la hauteur se synchronise via postMessage, un dialog ne peut pas sortir du frame, les deep links et l’historique du navigateur doivent être reliés à la main, le focus et l’accessibilité se compliquent, et chaque frame charge toute l’application. Adapté au code tiers, aux applications legacy que vous ne pouvez pas modifier et aux outils internes.
<!-- L’isolation la plus forte : un autre document, un autre JS, un autre CSS ;
les erreurs restent dans le frame -->
<iframe
src="https://billing.exemplu.ro/embed?customer=42"
title="Facturation"
sandbox="allow-scripts allow-same-origin allow-forms"
loading="lazy"
style="width: 100%; border: 0"
></iframe>
<!-- Le prix : la hauteur se communique via postMessage, un dialog ne peut pas sortir
du frame, les deep links et l’historique du navigateur se synchronisent à la main,
et chaque frame charge toute l’application. -->Contrats, communication, fallback
Le contrat d’un remote, c’est tout ce qui en dépend de l’extérieur : les clés de exposes, ce qu’elles exportent, les paramètres de route qu’elles lisent, les événements qu’elles émettent et les dépendances partagées. Traitez-le comme une API publique avec semver : un changement incompatible signifie une nouvelle clé (./routes-v2), et l’ancienne reste jusqu’à ce que le shell migre.
La communication, dans l’ordre où elle vaut la peine d’être essayée :
- L’URL — paramètres de route et query params. Gratuite, accessible par lien direct, elle survit à un rafraîchissement, et le remote n’importe rien d’un autre remote.
- Des événements typés sur
window, depuis un minuscule paquetcontracts(types +emit/listen). N’oubliez pas celui qui démarre tard : il doit demander l’état. - Quelques petits Services, partagés en singletons — auth, utilisateur courant, feature flags — depuis une bibliothèque incluse dans
shared.
À éviter : un Store global commun (le même Store NgRx, ou le même Service qui détient tout l’état, utilisé par chaque remote). Tout changement de la forme de l’état devient un changement coordonné entre équipes — exactement le couplage que vous vouliez supprimer. Chaque remote garde son propre état ; seuls des événements et des URL circulent entre eux (voir aussi State Management).
// 1. L’URL est le premier canal : gratuite, accessible par lien direct, survit à un rafraîchissement.
// /catalog/audio?sort=price → le remote lit l’URL, pas un Store partagé.
// (withComponentInputBinding() va dans le SHELL : les routes du remote
// s’exécutent dans le routeur du shell)
export class ProductList {
readonly category = input<string>(); // depuis :category
readonly sort = input<'price' | 'rating'>('rating'); // depuis ?sort=
}
// 2. La navigation entre remotes passe aussi par l’URL :
// le remote n’importe rien d’un autre remote.
this.router.navigate(['/cart'], { queryParams: { added: product.id } });Fallback. N’importe quel remote peut manquer : un déploiement raté, un CDN en panne, une erreur dans le manifest d’un environnement. Deux situations, deux solutions :
- Une route entière : laissez
loadChildrenéchouer et transformez l’erreur en redirection avecwithNavigationErrorHandler. Un loader rejeté n’est pas mis en cache par le routeur, donc la navigation suivante réessaie le remote. - Un remote dans une page (un widget, un slot) : un composant du shell charge le remote avec
resource, avec un timeout, un fallback etreload()pour « Réessayer » — comme dans le premier exemple.
// Une ROUTE de remote EN ÉCHEC : laissez loadChildren échouer et redirigez.
// Un loader rejeté n’est PAS mis en cache par le routeur → la navigation suivante réessaie.
import { ApplicationConfig, inject } from '@angular/core';
import {
NavigationError,
RedirectCommand,
Router,
provideRouter,
withNavigationErrorHandler,
} from '@angular/router';
import type { NativeFederationResult } from '@angular-architects/native-federation';
import { routes } from './app.routes';
export const appConfig = (nf: NativeFederationResult): ApplicationConfig => ({
providers: [
provideRouter(
routes(nf),
withNavigationErrorHandler((e: NavigationError) => {
const router = inject(Router);
const target = `/indisponibil?url=${encodeURIComponent(e.url)}`;
return new RedirectCommand(router.parseUrl(target));
}),
),
],
});
// ✗ Le piège : un fallback renvoyé par le loader est un SUCCÈS pour le routeur.
// Il est mis en cache sur la route jusqu’au rechargement de la page, donc « Réessayer »
// n’atteint plus jamais le remote :
// loadComponent: () => nf.loadRemoteModule(...).then(...).catch(() => RemoteDown)Quand OUI, quand NON
La décision dépend de l’organisation plus que du code. Le score ci-dessous applique les règles de cette page : ce n’est pas une formule magique, mais il vous force à répondre aux questions qui comptent et vous montre pourquoi une recommandation ressort.
- + 3 équipes : des frontières dans le code (bibliothèques, règles d’import) suffisent généralement
- + une release commune est acceptable
- + à partir de ~4 équipes autonomes, l’autonomie commence à compter
- − sans vrai besoin de déploiements indépendants, les MFE ne justifient pas leur prix
- + même version d’Angular → @angular/core peut être un singleton partagé
- − sans plateforme, sans pipelines par équipe ni monitoring : la complexité des MFE arrive en production
- Plusieurs équipes autonomes (environ 4–5 ou plus) sur le même produit, avec des cadences de release différentes, qui se bloquent aujourd’hui.
- Le déploiement indépendant est obligatoire (contractuel, réglementaire, rythmes différents) → Native Federation avec la même version d’Angular.
- Migration progressive depuis une ancienne application → un nouveau shell + strangler fig ; les anciens écrans en Web Components ou iframes jusqu’à leur disparition.
- Des produits séparés sous la même marque, avec une responsabilité claire par domaine métier.
- Vous avez déjà les fondations : une équipe plateforme pour le shell et le design system, une CI/CD par équipe, un monitoring par version de remote.
- Une équipe, ou quelques équipes qui peuvent livrer ensemble → une seule application dans un monorepo, avec des lazy routes et des frontières.
- Le problème est la performance →
@defer, lazy loading, SSR ; les MFE aggravent les choses. - Le problème est un code enchevêtré → bibliothèques, règles d’import, architecture ; les MFE ne font que déplacer l’enchevêtrement entre dépôts.
- Vous voulez simplement réutiliser des composants entre applications → une bibliothèque publiée, ou partagée dans le monorepo.
- Un widget pour des pages tierces → un seul Web Component (Angular Elements), pas une architecture.
- Personne ne peut prendre en charge le shell, les contrats et les montées de version communes → reportez jusqu’à ce que ce soit le cas.
Pièges fréquents
- La bibliothèque « partagée » qui n’est pas dans
shared→ chaque remote l’embarque séparément, doncprovidedIn: 'root'produit deux instances (deux classes, deux tokens). Incluez les bibliothèques avec état danssharedet vérifiezremoteEntry.json. singleton: falsesur@angular/coreou un remote sur une autre majeure → deux runtimes Angular. La même version partout ; pour les exceptions, l’isolation (Web Component, iframe).strictVersion: falsepour que ça « passe » → le message disparaît de la console, le problème reste et apparaît en production. Corrigez les versions, pas le flag.- Un fallback renvoyé par
loadComponent(.catch(() => RemoteDown)) → le routeur le met en cache sur la route jusqu’au rafraîchissement, et « Réessayer » n’atteint jamais le remote. Laissez le loader échouer, ou utilisez un composant host avecreload(). - Pas de timeout → un
remoteEntry.jsonqui ne répond pas laisse la page en « chargement » pour toujours. Mettez un timeout sur chaque chargement de remote. - Un seul manifest pour tous les environnements → le staging charge les remotes de production (ou l’inverse). Un manifest par environnement, généré au déploiement.
- Un Store global commun entre remotes → des releases en lockstep, un monolithe distribué. État local + événements + URL.
- Un remote qui importe du code directement d’un autre remote → une dépendance cachée entre déploiements. Tout passe par
libs/contractsou par l’URL. - Des routes absolues écrites dans un remote (
/products/42) → elles cassent quand le shell le monte sous un autre préfixe. Naviguez relativement à la route active. - Du CSS global dans les remotes → les styles d’une équipe cassent ceux d’une autre. Pas de styles globaux dans les remotes ; des tokens CSS issus du design system ;
ShadowDompour les Web Components. - Des erreurs sans contexte → vous ne savez pas quel remote, dans quelle version, a planté. Attachez le nom et la version du remote à chaque erreur remontée (voir gestion des erreurs).
Aide-mémoire
// REMOTE — federation.config.mjs
withNativeFederation({
name: 'catalog',
exposes: { './routes': './src/app/catalog.routes.ts' }, // API publique, versionnée
shared: fromPackageJson({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
});
// SHELL — public/federation.manifest.json (un par environnement : dev / staging / prod)
// { "catalog": "https://catalog.exemplu.ro/remoteEntry.json" }
// SHELL — main.ts
initFederation('federation.manifest.json')
.then((nf) => import('./bootstrap').then((m) => m.bootstrap(nf)));
// SHELL — routes
const catalog = {
path: 'catalog',
loadChildren: () =>
nf.loadRemoteModule<{ CATALOG_ROUTES: Routes }>('catalog', './routes')
.then((m) => m.CATALOG_ROUTES),
};
// VERSIONS : la même version d’Angular partout ; le host a priorité.
// incompatible + strictVersion → copie propre dans les scopes (NF)
// "Unsatisfied version …" (webpack MF)
// incompatible, sans strictVersion → utilise la version partagée + avertissement
// COMMUNICATION : URL → événements typés (libs/contracts) → petits Services partagés.
// Pas de Stores communs.
// ÉCHEC : route → withNavigationErrorHandler + redirection
// slot → composant host avec timeout + resource.reload()
// AUTRE MAJEURE / AUTRE FRAMEWORK → Web Component avec son propre runtime, ou iframe