DEV Community

Federico
Federico

Posted on

Architettura moderna dei frontend Leaflet-Angular App: Micro Frontend, Shell e Leaflet in Angular

Architettura moderna dei frontend: Micro Frontend, Shell e Leaflet in Angular

Introduzione

Nel contesto delle moderne applicazioni web, la crescente complessità e la necessità di scalabilità, manutenibilità e autonomia dei team hanno portato all'adozione di architetture distribuite anche nel frontend. I Micro Frontend rappresentano un paradigma architetturale che consente di suddividere un'applicazione frontend in moduli indipendenti, ciascuno gestito da un team dedicato, con il proprio ciclo di vita, stack tecnologico e pipeline di rilascio.

In questo articolo analizziamo come progettare un'architettura a micro frontend in Angular, integrando una shell principale con più MFE (Micro Frontend Applications), e focalizzandoci su un caso d'uso reale: la gestione di mappe interattive con Leaflet.


Perché scegliere i Micro Frontend?

Vantaggi principali:

  • Autonomia dei team
  • Deploy indipendenti
  • Scalabilità tecnica e organizzativa
  • Manutenibilità
  • Tecnologie eterogenee
  • Separazione dei domini funzionali (es. mappa, filtri, analytics, utente)

Architettura generale: Shell + MFE

Shell

La shell è l'applicazione contenitore che gestisce:

  • Il routing principale
  • Il layout condiviso (header, sidebar, footer)
  • L'integrazione dinamica dei MFE (via Webpack Module Federation)
  • Lo stato globale (NgRx, Redux, o soluzioni custom)
  • L'autenticazione e la gestione dei permessi (es. Keycloak)
  • La sessione utente condivisa

Micro Frontend (MFE)

Ogni MFE è un'applicazione autonoma, con:

  • Routing interno
  • Stato locale
  • Componenti e servizi propri
  • Pipeline CI/CD dedicata
  • Traduzione (file i18n)
  • Accesso alla sessione utente condivisa

Esempi di MFE in un'app geografica:

MFE Dominio funzionale
mfe-map Rendering mappa Leaflet, layer WMS, POI
mfe-filters Filtri geografici e tematici
mfe-details Dettagli POI, schede info, media
mfe-analytics Grafici, KPI, heatmap
mfe-user Profilo utente, permessi, ruoli

Integrazione tecnica con Angular e Module Federation

Configurazione base:

// webpack.config.js (esempio semplificato)
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        mfeMap: 'mfeMap@http://localhost:4201/remoteEntry.js',
        mfeFilters: 'mfeFilters@http://localhost:4202/remoteEntry.js',
      },
      shared: {
        '@angular/core': { singleton: true },
        '@angular/common': { singleton: true },
        '@angular/router': { singleton: true },
      },
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

Integrazione della libreria interna di widget

Per garantire coerenza visiva e riuso dei componenti UI, è consigliabile creare una libreria interna (es. @company/widgets) contenente:

  • Componenti UI riutilizzabili (Button, Card, Modal, Table)
  • Servizi comuni (NotificationService, ThemeService)
  • Pipe e direttive condivise

Setup della libreria

  1. Pubblicare la libreria su un registry privato (es. GitHub Packages, Verdaccio).
  2. Aggiungere come dipendenza npm in ogni MFE e nella Shell:

npm install @company/widgets

Configurazione delle porte locali

Durante lo sviluppo, ogni MFE e la Shell girano su una porta diversa per facilitare il caricamento remoto via Module Federation.

Queste porte possono essere configurate nel file angular.json o tramite serve custom nei workspace multipli.

Comunicazione tra Shell e MFE tramite Event Bus

Per una comunicazione decoupled e scalabile, si può implementare un Event Bus basato su RxJS o CustomEvent.

Soluzione con RxJS (Shared Event Bus)

// event-bus.service.ts
@Injectable({ providedIn: 'root' })
export class EventBusService {
  private subject$ = new Subject<{ type: string; payload?: any }>();

  emit(event: { type: string; payload?: any }) {
    this.subject$.next(event);
  }

  on(type: string): Observable<any> {
    return this.subject$.asObservable().pipe(
      filter(event => event.type === type),
      map(event => event.payload)
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Utilizzo

// MFE emette evento
this.eventBus.emit({ type: 'poiSelected', payload: poi });

// Shell ascolta
this.eventBus.on('poiSelected').subscribe(poi => {
  this.detailsService.loadDetails(poi);
});
Enter fullscreen mode Exit fullscreen mode

Condivisione della sessione Keycloak tra Shell e MFE

Soluzioni per la gestione della sessione utente con Keycloak

Shared Auth Service come singleton

Una delle soluzioni più robuste per la gestione della sessione utente in un'architettura Micro Frontend è la definizione di un modulo auth-lib nella Shell, che espone un AuthService singleton contenente lo stato della sessione e i metodi di autenticazione.

Caratteristiche del modulo auth-lib

  • Stato della sessione utente (token, profilo, ruoli)
  • Metodi login(), logout(), isAuthenticated(), getToken()
  • Rinnovo automatico del token (es. con Keycloak JS Adapter)
  • EventEmitter o Subject per notificare i MFE su cambiamenti di sessione

Esportazione via Module Federation
Nel webpack.config.js della Shell:

// webpack.config.js
shared: {
  '@my-org/auth-lib': {
    singleton: true,
    strictVersion: true,
    requiredVersion: 'auto',
  },
}
Enter fullscreen mode Exit fullscreen mode

Importazione nei MFE
Nel webpack.config.js del MFE:

remotes: {
  shell: 'shell@http://localhost:4200/remoteEntry.js'
}
Enter fullscreen mode Exit fullscreen mode

Nel codice Angular del MFE:

import('shell/AuthService').then(({ AuthService }) => {
  const token = AuthService.getToken();
  if (AuthService.isAuthenticated()) {
    // procedi con chiamate protette
  }
});
Enter fullscreen mode Exit fullscreen mode
Vantaggi
  • Centralizzazione della logica di sessione
  • Singleton garantito tra Shell e MFE
  • Decoupling funzionale: i MFE non gestiscono direttamente Keycloak
  • Facile estensione: supporto a ruoli, permessi, refresh token, ecc.

Asset multimediali (immagini, icone, font)

  • Ogni MFE può servire i propri asset statici tramite il proprio assets/ locale.
  • Per asset condivisi (es. logo, icone del design system), è consigliabile in una libreria condivisa.

Traduzioni i18n

Gestione delle traduzioni i18n con ngx-translate

Una delle possibili soluzioni è il servizio ngx-translate

File di traduzione

Ogni MFE può avere i propri file assets/i18n/en.json, it.json, ecc.

Esempio it.json:

{
  "LABEL": "Etichetta",
  "WELCOME": "Benvenuto"
}
Enter fullscreen mode Exit fullscreen mode
Utilizzo nei template
<h1>{{ 'WELCOME' | translate }}</h1>
<button>{{ 'LABEL' | translate }}</button>
Enter fullscreen mode Exit fullscreen mode
Cambio lingua runtime

La Shell può gestire il cambio lingua e notificare i MFE tramite EventBus o servizio condiviso:

this.translate.use('it');
Enter fullscreen mode Exit fullscreen mode

Oppure

this.eventBus.emit({ type: 'localeChanged', payload: 'it' });
Enter fullscreen mode Exit fullscreen mode

I MFE ascoltano e aggiornano:

this.eventBus.on('localeChanged').subscribe(locale => {
  this.translate.use(locale);
});
Enter fullscreen mode Exit fullscreen mode

Questa architettura consente una gestione distribuita e coerente delle traduzioni, con supporto a localizzazione dinamica, modularità e autonomia dei team.

Architettura consigliata per chiamate API nei MFE

Facade per ogni MFE

Ogni Micro Frontend dovrebbe avere un proprio servizio o facade che incapsula le chiamate al backend relative al suo dominio funzionale. Questo approccio garantisce:

  • Isolamento del dominio (es. PoiService, UserService, AnalyticsService)
  • Testabilità e mocking facilitato
  • Manutenibilità e coerenza interna

Interceptor condiviso

Un HttpInterceptor comune, definito nella Shell o in una libreria condivisa, può essere utilizzato da tutti i MFE per:

  • Inserire il token JWT nella Authorization header
  • Gestire gli errori globali (es. 401 → redirect, 403 → access denied, 500 → fallback UI)
  • Loggare o tracciare le chiamate (es. OpenTelemetry, Sentry)
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = localStorage.getItem('kc_token');
    const authReq = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` }
    });

    return next.handle(authReq).pipe(
      catchError(err => {
        if (err.status === 401) {
          // redirect alla Shell per login
        }
        return throwError(() => err);
      })
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)