Skip to content

Angular, module federation

By Adrien Pessu

Posted in Blog, Angular, Micro-frontend

Les modules d'Angular qui sont très puissants. Ils vont même jusqu`à permettre de faire du micro frontend sur une application Angular.

La modularisation

Un module dans Angular, c'est une classe où sont déclarées des éléments (composants, directives, pipes, services, ...) qui sont en relation sur un même périmètre. Ce périmètre est le plus souvent un périmètre fonctionnel. Nous aurons donc. par exemple, un module "commande", un module "client" et un module "facture". Cette notion de module existe depuis Angular 2 (voir AngularJs si on considère que c'est le même Framework).

D'un point de vue architecture, nous aurons un module principal AppModule où seront importés d'autres sous modules. Nous pourons aussi déclarer des modules dans des sous modules, si bien sûr ça à du sens. D'ailleurs, quand on utilise une librarie externe conçue pour Angular ou qui possède un "wrapper" Angular. Il suffit de déclarer la librairie dans package.json et d'importer le module de cette librairie dans un de nos modules.

Prenons le module suivant :


import { NgModule }      from '@angular/core'; 
import { BrowserModule } from '@angular/platform-browser';  
import { AppComponent }  from './app.component';  

@NgModule ({ 
   imports:      [ BrowserModule ], 
   declarations: [ AppComponent ], 
   exports:      [ BrowserModule ], 
   bootstrap:    [ AppComponent ],
   providers:    [ AppService ]
}) 
export class AppModule { } 

  • Dans imports, nous aurons les modules que nous allons utiliser dans ce module.
  • Dans declarations, nous aurons les éléments que nous avons créés dans ce module.
  • Dans exports, nous aurons les éléments que nous avons créés dans ce module et qui seront utilisables à l'extérieur. C'est-à-dire, là où notre module sera importé.
  • Dans boostrap, nous aurons le composant racine qui sera inséré dans index.html
  • Dans providers, nous pourrons déclarer des services. Ils peuvent aussi être déclarés par une annotation dans le service lui-même.

D'un point de vue génération du livrable. Chaque module sera séparé dans des fichiers Javascript différents.

Chemin d'accès au module

Quand nous importons un module dans un autre module, nous y associons le plus souvent chemin (path) d'accès de l'URL.

Il faut donc que nous voyions ensemble quelques notions du routeur Angular, qui est d'ailleurs un module à déclarer dans import. Par exemple, la page principale de notre application de gestion sera /. La partie commande dans /commande, la partie facture dans /facture ,... Le module facture sera donc associé au chemin commençant par /facture. Nous associerons les composants du module facture que nous souhaitons voir, le composant d'édition des factures sera affiché sur le chemin /facture/edition

La route principale est /facture, la route enfant est /facture/edition.

Voici donc la déclaration des routes du module facture.

const routes: Routes = [
  {
    path: 'facture',
    component: FactureComponent, 
    children: [
      {
        path: 'edition', 
        component: EditionFactureComponent, 
      },
      {
        path: 'impression',
        component: ImpressionFactureComponent, 
      },
    ],
  },
];

Le lazy loading

Le lazy loading (difficile à traduire correctement) est un mécanisme qui permet de charger des éléments seulement quand on en a besoin (ou à un moment qui ne bloquerait pas l'application). Ici, cela nous permet de télécharger un module que lorsque l'on accède à un chemin de l'application. Dans notre exemple, l'utilisateur quand il accède à / ne devrait pas à avoir à télécharger le module facture alors qu'il n'a pas forcement l'intention d'y aller. Concrètement, les fichiers Javascript générés pour un module ne seront téléchargés par le navigateur que lors de l'accès à un chemin bien précis. Tout ceci pour permettre un premier affichage plus rapide des pages.

Voici la syntaxe pour déclarer le lazy loading d'un module :

const routes: Routes = [
  {
    path: 'facture',
    loadChildren: () => import('./facture/facture.module').then(m => m.FactureModule)
  },
  {
    path: 'commande',
    loadChildren: () => import('./commande/commande.module').then(m => m.CommandeModule)
  },
  {
    path: '',
    redirectTo: '',
    pathMatch: 'full'
  }
];

Module fédération

Dans "module federation", la notion de module n'est en fait pas tout à fait celle que nous venons de voir. Ici ce sont des modules "Webpack".

Webpack est un outil pour packager une application Javascript. Angular cli utilise Webpack pour packager notre application. Nous n'avons la plupart du temps pas besoin de configurer Webpack dans Angular. Lorsque Webpack package une application, le résultat est appelé build. Un module Webpack est une dépendance qui peut avoir plusieurs formes :

  • une instruction import (ES2015)
  • une instruction require (CommonJS)
  • une instruction define ou require (AMD)
  • une instruction @import dans un fichier css/sass/less.
  • L'URL d'une image dans uns feuille de style CSS url(...) ou un fichier HTML <img src=...>

Dans Webpack 5, le concept de module federation est le suivant. Chaque build est un container qui peut aussi importer un autre container. Un build associé à une application, donc une URL donnée, peut importer un autre build, donc une autre application située sur une autre URL.

Dans Angular, nous allons déclarer dans un module (Angular cette fois) d'une application (dite Shell) qu'elle va recevoir un module venant d'une application venant d'une autre URL. Toujours dans notre exemple, nous avons notre application hébergée à l'adresse : https://ma-gestion.com. Et le module de facturation hébergé à l'adresse https://facture.ma-gestion.com/bundle.js

Comme pour le lazy-loading, nous associerons le chemin facture au module facture, mais cette fois le module est à une autre adresse. Chaque module devra être indépendant, ils pourront accéder à l'état (state) de l'application Shell.

L'intérêt principal de ce mode de fonctionnement est d'adopter le principe de "Micro-frontend".

Micro-frontend

La tendance actuelle est de construire des applications web riche, c'est-à-dire, ayant beaucoup de fonctionnalités côté front, sous la forme de Single Page Application. Ces applications web font appel à une API découpé en microservice. Ces microservices sont des "petites" applications serveurs limitées à un domaine fonctionnel ou technique. Ceci permettant de limiter la charge sur un gros serveur et la répartir sur plusieurs serveurs, mais aussi de pouvoir mettre à jour un domaine sans impacter les autres.

Plutôt que de construire, une grosse application frontend, dites monolithiques, nous allons construire plusieurs petites applications frontend qui seront ensuite assemblées dans une application dite "Shell". Le micro-frontend permettra de livrer une nouvelle version d'une partie de l'application sans livrer l'application en entier. Dans une organisation découpée en "feature" team, chaque équipe sera indépendante pour livrer une nouvelle application, ce qui permet de livrer plus rapidement sans bloquer les autres équipes. L'organisation sera alors alignée avec l'architecture de l'application (voir loi de Conway).

Conclusion

Les modules permettent d'organiser le code de notre application. Adopter cette organisation dès le début du développement, nous permettra de faire évoluer notre application en fonction de la taille de celle-ci. Nous commencerons avec plusieurs "petits" modules. Quand un module devient trop important pour être toujours charger au premier affichage, nous pourrons en faire un module lazy-loadé. Ensuite, si un module est suffisamment important pour être développé par une équipe indépendante, nous pourrons alors sortir ce module de l'application et utiliser le mécanisme de module federation.

Dans un prochain article, nous verrons verra la construction pas à pas d'une application Angular avec module fédération. A suivre...