Architecturez vos micro-frontends Angular sans Webpack avec Native Federation : Import Maps, esbuild, shared deps et migration depuis Module Federation.
Pourquoi Native Federation arrive maintenant
Pendant des années, Module Federation (Webpack 5) a été la solution incontournable pour construire des architectures micro-frontends en Angular. Le pattern est simple : plusieurs équipes développent et déploient indépendamment leurs sous-applications, le shell les charge dynamiquement à l'exécution.
Mais depuis Angular 17, l'équipe Angular a abandonné Webpack par défaut au profit d'esbuild et du nouveau Application Builder. Conséquence : Module Federation, qui dépend intrinsèquement de Webpack, devenait incompatible avec les projets modernes. C'est de ce vide qu'est né Native Federation, créé et maintenu par Manfred Steyer (Angular Architects).
loadRemoteModule, shared) mais en utilisant les Import Maps, un standard W3C supporté par tous les navigateurs modernes. Aucune dépendance bundler-spécifique.
Cas d'usage typiques
- Plateformes multi-équipes : finance + commerce + admin développés et déployés indépendamment
- Migration progressive : moderniser une app legacy module par module sans tout réécrire
- White-label SaaS : chaque client charge dynamiquement ses widgets personnalisés
- Plugins et extensions : autoriser des partenaires à brancher des modules tiers à votre app
Concepts clés : Import Maps et shared deps
Avant de coder, comprenons les 3 mécanismes fondamentaux exploités par Native Federation.
1. Import Maps : la magie du standard W3C
Un Import Map est un objet JSON injecté dans la page qui mappe des noms de modules vers des URLs. Le navigateur résout alors les imports ES dynamiquement, sans bundler à l'exécution.
<!-- index.html — exemple d'Import Map simplifié -->
<script type="importmap">
{
"imports": {
"@angular/core": "/shared/angular-core-19.0.0.js",
"@angular/common":"/shared/angular-common-19.0.0.js",
"rxjs": "/shared/rxjs-7.8.1.js",
"mfe-billing/": "https://billing.example.com/"
}
}
</script>
Désormais, quand un module fait import { signal } from '@angular/core', le navigateur va chercher /shared/angular-core-19.0.0.js. Aucun bundler n'intervient à l'exécution.
2. Shared dependencies : économiser la bande passante
Sans déduplication, chaque micro-frontend embarquerait sa propre copie d'Angular (~300 KB gzippé). Native Federation extrait ces dépendances partagées dans l'Import Map global, permettant aux 5 micro-frontends d'utiliser la même instance d'Angular en mémoire.
3. Remote modules : composants chargés à la demande
Chaque équipe expose une liste de modules dans son federation.config.js. Le shell les charge dynamiquement via loadRemoteModule(), exactement comme avec Module Federation.
| Comparaison | Module Federation (Webpack 5) | Native Federation |
|---|---|---|
| Bundler requis | Webpack uniquement | esbuild, Vite, Rollup ou Webpack |
| Mécanisme runtime | Code Webpack injecté | Import Maps standards W3C |
| Compatible Angular 17+ | ❌ Nécessite custom-webpack | ✅ Out of the box |
| Bundle shell size | ~30 KB de runtime | ~5 KB de polyfill (es-module-shims) |
| API publique | loadRemoteModule() |
Quasi identique (drop-in) |
Installation et configuration shell + remote
Native Federation s'installe via une schematic Angular CLI officielle. Aucune configuration Webpack à toucher.
Étape 1 : créer le workspace
// Créer un workspace vide avec 2 projets : shell et billing (le remote)
ng new my-platform --no-create-application
cd my-platform
ng generate application shell --routing --style=scss
ng generate application mfe-billing --routing --style=scss
Étape 2 : ajouter Native Federation aux deux projets
// Installer le package
npm install @angular-architects/native-federation
// Lancer la schematic pour chaque projet
ng add @angular-architects/native-federation --project shell --port 4200 --type dynamic-host
ng add @angular-architects/native-federation --project mfe-billing --port 4201 --type remote
La schematic modifie automatiquement angular.json, crée federation.config.js dans chaque projet et ajoute un fichier bootstrap.ts avec un import dynamique.
Étape 3 : configurer le remote (mfe-billing)
// projects/mfe-billing/federation.config.js
const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');
module.exports = withNativeFederation({
name: 'mfe-billing',
// Modules exposés par ce remote, accessibles depuis le shell
exposes: {
'./Component': './projects/mfe-billing/src/app/billing/billing.component.ts',
'./Routes': './projects/mfe-billing/src/app/billing/billing.routes.ts',
},
// Dépendances partagées avec le shell (singleton requis pour Angular)
shared: {
...shareAll({
singleton: true,
strictVersion: true, // versions exactes obligatoires
requiredVersion: 'auto',
}),
},
skip: [
'rxjs/ajax', // exclusions optionnelles
'rxjs/testing',
'rxjs/webSocket',
],
});
Étape 4 : configurer le shell (dynamic-host)
// projects/shell/src/assets/federation.manifest.json
// Manifest qui liste les remotes connus du shell — chargé au démarrage
{
"mfe-billing": "http://localhost:4201/remoteEntry.json",
"mfe-catalog": "http://localhost:4202/remoteEntry.json",
"mfe-admin": "http://localhost:4203/remoteEntry.json"
}
// projects/shell/src/main.ts — bootstrap modifié par la schematic
import { initFederation } from '@angular-architects/native-federation';
// initFederation() charge le manifest puis bootstrap l'application
initFederation('/assets/federation.manifest.json')
.catch(err => console.error('Erreur federation:', err))
.then(_ => import('./bootstrap'))
.catch(err => console.error('Erreur bootstrap:', err));
Charger un micro-frontend distant
Une fois le shell et le remote configurés, charger un module distant ressemble exactement à du lazy-loading Angular classique.
Lazy-loading via le router
// projects/shell/src/app/app.routes.ts
import { Routes } from '@angular/router';
import { loadRemoteModule } from '@angular-architects/native-federation';
export const routes: Routes = [
{ path: '', loadComponent: () => import('./home/home.component').then(m => m.HomeComponent) },
// Route qui charge le remote 'mfe-billing' et son module './Routes'
{
path: 'billing',
loadChildren: () =>
loadRemoteModule('mfe-billing', './Routes').then(m => m.BILLING_ROUTES),
},
// Variante : charger un composant unique
{
path: 'catalog',
loadComponent: () =>
loadRemoteModule('mfe-catalog', './Component').then(m => m.CatalogComponent),
},
];
Charger un remote dynamiquement (sans router)
// Exemple : afficher un widget tiers à la demande
import { Component, ViewContainerRef, inject } from '@angular/core';
import { loadRemoteModule } from '@angular-architects/native-federation';
@Component({
selector: 'app-dashboard',
standalone: true,
template: `<button (click)="loadWidget()">Charger widget météo</button>
<ng-container #host></ng-container>`,
})
export class DashboardComponent {
private vcr = inject(ViewContainerRef);
async loadWidget() {
// Le module n'est téléchargé QUE quand l'utilisateur clique
const module = await loadRemoteModule('mfe-weather', './Widget');
this.vcr.createComponent(module.WeatherWidget);
}
}
Gérer les erreurs de chargement
// remote-loader.service.ts — gestion robuste des erreurs
import { Injectable } from '@angular/core';
import { loadRemoteModule } from '@angular-architects/native-federation';
@Injectable({ providedIn: 'root' })
export class RemoteLoaderService {
async load<T>(remoteName: string, exposedModule: string, fallback?: T): Promise<T> {
try {
return await loadRemoteModule(remoteName, exposedModule) as T;
} catch (err) {
console.error(`Remote ${remoteName} indisponible:`, err);
// En cas d'échec réseau, on retourne un fallback ou un module dégradé
if (fallback) return fallback;
throw err;
}
}
}
Routing dynamique multi-team
L'un des cas réels les plus puissants : découvrir les routes à l'exécution selon les droits utilisateur ou la configuration backend, sans rebuild du shell.
// dynamic-routes.service.ts — récupérer les routes depuis l'API
import { inject, Injectable } from '@angular/core';
import { Router, Routes } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { loadRemoteModule } from '@angular-architects/native-federation';
interface RemoteRouteConfig {
path: string;
remoteName: string;
exposedModule: string;
label: string;
roles: string[];
}
@Injectable({ providedIn: 'root' })
export class DynamicRoutesService {
private http = inject(HttpClient);
private router = inject(Router);
async loadUserRoutes(userRoles: string[]): Promise<void> {
// 1. Récupérer la config des routes depuis le backend
const configs = await this.http
.get<RemoteRouteConfig[]>('/api/user/routes')
.toPromise() ?? [];
// 2. Filtrer selon les rôles de l'utilisateur connecté
const allowed = configs.filter(c => c.roles.some(r => userRoles.includes(r)));
// 3. Construire dynamiquement les routes Angular
const dynamicRoutes: Routes = allowed.map(c => ({
path: c.path,
loadChildren: () =>
loadRemoteModule(c.remoteName, c.exposedModule).then(m => m.ROUTES),
data: { label: c.label },
}));
// 4. Réinitialiser le router avec les nouvelles routes
this.router.resetConfig([
...this.router.config,
...dynamicRoutes,
{ path: '**', redirectTo: '' }, // catch-all en dernier
]);
}
}
Déploiement et CDN en production
Native Federation produit un build esbuild standard. Chaque remote génère un dossier avec un remoteEntry.json (manifest), des modules ES, et un Import Map dédié.
Stratégie de déploiement recommandée
// Structure typique en production
// CDN/Bucket :
// /shell/ → l'app shell (ng build shell)
// /mfe-billing/v2.4/ → version 2.4 du remote billing
// /mfe-billing/v2.5/ → version 2.5 (nouveau déploiement)
// /mfe-catalog/v1.8/ → version 1.8 du remote catalog
// federation.manifest.json — pointe vers les versions actives
{
"mfe-billing": "https://cdn.example.com/mfe-billing/v2.5/remoteEntry.json",
"mfe-catalog": "https://cdn.example.com/mfe-catalog/v1.8/remoteEntry.json"
}
Rollback instantané sans rebuild
Pour annuler un déploiement défectueux, il suffit de modifier federation.manifest.json côté serveur :
// AVANT (déploiement v2.5 cassé)
"mfe-billing": "https://cdn.example.com/mfe-billing/v2.5/remoteEntry.json"
// APRÈS (rollback vers v2.4) — modification d'UN fichier JSON, c'est tout
"mfe-billing": "https://cdn.example.com/mfe-billing/v2.4/remoteEntry.json"
CSP (Content Security Policy)
<!-- index.html — autoriser les remotes dans la CSP -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' https://cdn.example.com 'wasm-unsafe-eval';
style-src 'self' 'unsafe-inline' https://cdn.example.com;
connect-src 'self' https://cdn.example.com https://api.example.com;
">
- Versionner chaque remote dans son URL (
/mfe-name/v1.2.3/) - Configurer Cache-Control long sur les
chunks.*.mjs(immutable) - Cache-Control court (60s) sur
remoteEntry.jsonetfederation.manifest.json - CSP stricte mentionnant explicitement le domaine CDN
- Healthcheck monitoring de chaque
remoteEntry.json - Fallback UI en cas d'échec de chargement remote (cf. RemoteLoaderService)
Migrer depuis Module Federation Webpack
Si vous avez déjà une plateforme sur Module Federation Webpack, la migration vers Native Federation est largement automatisée par une schematic dédiée.
// 1. Désinstaller l'ancien plugin custom-webpack
npm uninstall @angular-architects/module-federation @angular-builders/custom-webpack
// 2. Mettre à jour Angular vers v17+ avec le builder esbuild
ng update @angular/cli@latest @angular/core@latest
// 3. Ajouter Native Federation
ng add @angular-architects/native-federation
// 4. Migrer les configs (la schematic convertit webpack.config.js → federation.config.js)
ng generate @angular-architects/native-federation:migrate
Différences à connaître
// AVANT — webpack.config.js (Module Federation)
const { withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
name: 'mfe-billing',
exposes: { './Module': './src/app/billing/billing.module.ts' },
shared: { '@angular/core': { singleton: true, strictVersion: true } },
});
// APRÈS — federation.config.js (Native Federation, quasi identique)
const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');
module.exports = withNativeFederation({
name: 'mfe-billing',
exposes: { './Routes': './src/app/billing/billing.routes.ts' }, // standalone routes
shared: { ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }) },
});
// L'API loadRemoteModule() reste IDENTIQUE — pas de changement côté shell
app.routes.ts) plutôt qu'aux NgModules. Native Federation s'intègre naturellement avec l'architecture standalone d'Angular 17+.
Conclusion et bonnes pratiques
Native Federation représente l'avenir des micro-frontends Angular dans l'écosystème post-Webpack. En s'appuyant sur les Import Maps standards et le builder esbuild officiel, il combine la flexibilité de Module Federation avec les performances et la simplicité d'une stack moderne.
Les bonnes pratiques à graver :
- Manifest dynamique : ne hardcodez jamais les URLs des remotes dans le bundle
- Versionnez vos remotes dans l'URL pour permettre rollback instantané
- singleton + strictVersion pour Angular et RxJS — toujours
- Alignez les versions entre tous les projets (Nx workspace recommandé)
- Fallback UI pour chaque remote pouvant être indisponible
- Monitoring du
remoteEntry.jsonvia healthcheck