Front-end angularforall.com

- Native Federation Angular : sans Webpack

Angular Native-Federation Module-Federation Micro-Frontends Esbuild Import-Maps Angular-19 Shared-Dependencies Architecture Monorepo Rxjs Loadremotemodule Manifest Deploiement
Native Federation Angular : sans Webpack

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).

L'idée fondatrice : reproduire l'API de Module Federation (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));
Pourquoi un manifest JSON ? Contrairement à Module Federation où les URLs des remotes sont hardcodées dans le bundle, le manifest est chargé à l'exécution. Vous pouvez changer l'URL d'un remote en prod sans rebuild du shell.

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;
        }
    }
}

Optimiser les dépendances partagées

La performance d'une architecture micro-frontend dépend critiquement de la déduplication des dépendances. Sans configuration soigneuse, vous risquez de télécharger plusieurs versions d'Angular, ce qui ruine totalement les gains attendus.

Stratégies de partage

// federation.config.js — 3 stratégies selon le besoin
const { withNativeFederation, share, shareAll } = require('@angular-architects/native-federation/config');

module.exports = withNativeFederation({
    name: 'mfe-billing',
    exposes: { './Routes': './src/app/billing.routes.ts' },

    // Stratégie 1 : shareAll() — tout package node_modules est shared (défaut)
    shared: {
        ...shareAll({
            singleton: true,
            strictVersion: true,    // erreur si versions incompatibles
            requiredVersion: 'auto', // lit la version depuis package.json
        }),

        // Stratégie 2 : override sélectif d'un package
        '@angular/material': {
            singleton: true,
            strictVersion: false,  // tolère les versions différentes
            requiredVersion: '^19.0.0',
        },

        // Stratégie 3 : ne PAS partager (deps spécifiques au remote)
        'lodash-es': { singleton: false },
    },
});

Comprendre singleton et strictVersion

Option Effet Quand l'activer
singleton: true Une seule instance globale en mémoire Obligatoire pour Angular, RxJS, frameworks
singleton: false Chaque remote a sa propre instance Utilitaires sans état (lodash, date-fns)
strictVersion: true Erreur si versions incompatibles Production — éviter bugs silencieux
strictVersion: false Tolère les versions différentes Phase de migration uniquement
⚠️ Piège classique : deux remotes utilisant @angular/core 19.0 et @angular/core 19.1 en singleton: true + strictVersion: true produiront une erreur au démarrage. Alignez les versions dans tous vos package.json ou utilisez un monorepo Nx.

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
        ]);
    }
}
Cas concret : une banque peut activer le module « investissement » uniquement pour les clients premium, sans déployer une nouvelle version du shell. La config arrive via API, les routes sont enregistrées à la volée.

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;
">
Checklist déploiement production :
  • 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.json et federation.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
Conseil de migration : profitez-en pour passer aux routes standalone (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.json via healthcheck
Aller plus loin : Native Federation v2 (prévu été 2026) introduit le support natif des Web Components fédérés et l'auto-discovery des remotes via DNS. L'équipe Angular Architects travaille également sur l'intégration directe au CLI Angular pour atteindre le statut de fonctionnalité officielle.

Partager