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 },
},
}),
],
};
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
- Pubblicare la libreria su un registry privato (es. GitHub Packages, Verdaccio).
- 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.
| Modulo | Porta | URL remoto |
|---|---|---|
| Shell | 4200 | http://localhost:4200/ |
| mfe-map | 4201 | http://localhost:4201/remoteEntry.js |
| mfe-filters | 4202 | http://localhost:4202/remoteEntry.js |
| mfe-details | 4203 | http://localhost:4203/remoteEntry.js |
| mfe-analytics | 4204 | http://localhost:4204/remoteEntry.js |
| mfe-user | 4205 | http://localhost:4205/remoteEntry.js |
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)
);
}
}
Utilizzo
// MFE emette evento
this.eventBus.emit({ type: 'poiSelected', payload: poi });
// Shell ascolta
this.eventBus.on('poiSelected').subscribe(poi => {
this.detailsService.loadDetails(poi);
});
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',
},
}
Importazione nei MFE
Nel webpack.config.js del MFE:
remotes: {
shell: 'shell@http://localhost:4200/remoteEntry.js'
}
Nel codice Angular del MFE:
import('shell/AuthService').then(({ AuthService }) => {
const token = AuthService.getToken();
if (AuthService.isAuthenticated()) {
// procedi con chiamate protette
}
});
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"
}
Utilizzo nei template
<h1>{{ 'WELCOME' | translate }}</h1>
<button>{{ 'LABEL' | translate }}</button>
Cambio lingua runtime
La Shell può gestire il cambio lingua e notificare i MFE tramite EventBus o servizio condiviso:
this.translate.use('it');
Oppure
this.eventBus.emit({ type: 'localeChanged', payload: 'it' });
I MFE ascoltano e aggiornano:
this.eventBus.on('localeChanged').subscribe(locale => {
this.translate.use(locale);
});
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);
})
);
}
}
Top comments (0)