La fédération de modules (Module Federation) est un concept introduit par Webpack 5 qui permet à plusieurs applications ou micro-frontends de charger et consommer du code les uns des autres de manière dynamique, sans avoir à les regrouper dans un bundle monolithique. Pour les équipes Angular travaillant sur des applications d'envergure, cette approche résout un problème fondamental : comment maintenir une architecture modulaire tout en permettant à plusieurs équipes de déployer indépendamment sans créer des dépendances figées sur la branche principale. La fédération de modules change le paradigme en transformant votre application en un ensemble de micro-frontends qui communiquent à travers des contrats clairs, chacun avec son propre cycle de vie de déploiement.
Comprendre la fédération de modules en Angular
Avant de plonger dans l'implémentation, il est crucial de comprendre les acteurs clés. Une application hôte (host ou shell) charge dynamiquement des modules distants (remote modules) au moment de l'exécution. Chaque module distant expose des composants, services ou logiques métier via un point d'entrée déclaré dans sa configuration webpack. Cette approche diffère radicalement du lazy loading classique : au lieu de charger des chunks à partir d'une même origin, vous téléchargez du code depuis des origins différentes, potentiellement déployées sur des serveurs différents. La magie opère grâce au fichier remoteEntry.js généré pour chaque module distant, qui expose les exports disponibles et gère les dépendances partagées intelligemment.
Configuration Webpack et shared dependencies
La configuration de Module Federation se fait via le plugin ModuleFederationPlugin de Webpack. La clé du succès réside dans la section 'shared' : vous déclarez quelles dépendances (Angular, RxJS, lodash, etc.) doivent être partagées entre l'hôte et les modules distants. Si vous déclarez Angular comme shared, Webpack utilisera une seule instance d'Angular pour toute l'application, évitant les doublons et les conflits de version. Voici un exemple simplifié pour un module hôte : dans webpack.config.js, vous configurez 'exposes' pour les modules locaux et 'remotes' pour pointer vers les modules distants. Pour un module distant (par exemple, dashboard-module), vous déclarez 'name', 'filename' (remoteEntry.js) et 'exposes' (les composants à partager). La syntaxe est déclarative et lisible une fois qu'on la maîtrise.
Exemple concret : architecture à deux niveaux
Imaginez une plateforme SaaS avec un shell applicatif central et plusieurs modules métier déployés indépendamment. Le shell charge un header partagé, une barre de navigation, et dynamiquement les modules selon les droits de l'utilisateur. Chaque module (Facturation, CRM, Analytics) exporte un composant racine et ses routes. Le shell configure les remotes dans webpack, et à l'exécution, il charge le module distant uniquement quand l'utilisateur y accède. Grâce au shared Angular, les deux applications partagent une seule instance du framework, réduisant la taille totale du bundle. Le module distant reste complètement indépendant : il peut utiliser sa propre version de NgRx, ses propres styles, et se déployer sans toucher au shell.
Gestion des versions et compatibilité
Un piège courant est de négliger la gestion des versions de dépendances partagées. Si le shell utilise Angular 18 et qu'un module distant compile avec Angular 17, des incompatibilités subtiles peuvent surgir au runtime. Webpack 5 offre des options pour gérer cela : vous pouvez définir une plage de versions acceptable (par exemple, '@angular/core': { singleton: true, strictVersion: false, requiredVersion: '^18.0.0' }). Le mode 'strictVersion' force une correspondance exacte, tandis que 'singleton' garantit qu'une seule instance existe dans toute l'application. Une bonne pratique est de documenter clairement les versions attendues de chaque dépendance partagée et d'utiliser le versioning sémantique de manière cohérente. Tester avec des versions légèrement décalées dans un environnement intermédiaire révèle souvent des incompatibilités cachées avant la production.
Routing et navigation entre modules
Intégrer le routing Angular avec Module Federation demande une approche réfléchie. Le shell définit les routes principales et charge les modules distants comme enfants de certaines routes. Chaque module distant expose ses propres routes, que le shell enregistre dynamiquement via loadChildren avec un import dynamique. Par exemple, le shell peut avoir une route '/dashboard' qui charge le module Dashboard, lequel expose ses sous-routes internes. La communication entre modules passe généralement par des services partagés ou des event buses (comme RxJS Subject) pour maintenir l'indépendance. Évitez de passer des références de composants ou de services non partagés entre modules ; privilégiez les contrats d'interface clairs et les données sérialisables.
Pièges courants et comment les éviter
Le premier piège est de partager trop de dépendances ou trop peu. Partager trop crée des dépendances implicites difficiles à gérer ; partager trop peu gonfle les bundles. Testez avec vos métriques réelles de bundle size pour trouver l'équilibre. Le deuxième piège est d'oublier que le remoteEntry.js doit être accessible en production : si votre CDN n'est pas bien configuré ou si une version est supprimée, le module distant ne chargera pas. Mettez en place une stratégie de versioning des artefacts et de cleanup. Le troisième piège concerne les styles et les CSS : si deux modules appliquent des styles globaux conflictuels, le rendu peut être imprévisible. Isolez les styles au niveau des composants avec ViewEncapsulation.ShadowDom ou utilisez des conventions de nommage strictes (BEM, CSS Modules). Enfin, la gestion des erreurs de chargement est souvent négligée : un module distant inaccessible doit afficher un fallback gracieux, pas une page blanche.
Monitoring et déploiement
En production, vous avez besoin de visibilité sur l'état des modules distants. Implémentez un health check qui vérifie régulièrement que chaque remoteEntry.js est accessible et valide. Loggez les erreurs de chargement de module avec le contexte complet (version attendue, URL du module, stack trace). Les outils comme Sentry ou DataDog peuvent instrumenter les erreurs de Module Federation. Côté déploiement, une stratégie de versioning clair (par exemple, /assets/dashboard-module@1.2.3/remoteEntry.js) permet de maintenir plusieurs versions en parallèle et de basculer rapidement en cas de problème. Les déploiements bleus-verts ou canary des modules distants réduisent les risques.
Conclusion opérationnelle
Module Federation est un outil puissant pour les équipes Angular construisant des applications à grande échelle où l'indépendance de déploiement est critique. La clé est une compréhension solide de la gestion des dépendances partagées, une documentation claire des contrats entre modules, et une discipline rigoureuse sur le versioning. Commencez avec une architecture simple (shell + 1 ou 2 modules distants), validez votre approche de partage de dépendances, puis évoluez progressivement. Les outils et conventions que vous établissez maintenant (nommage des modules, versioning, monitoring) determineront votre succès à long terme.