Nelle grandi organizzazioni enterprise, la transizione dalle architetture monolitiche ai microservizi ha risolto molti problemi lato backend, introducendo team indipendenti, deploy disaccoppiati e maggiore scalabilità. Tuttavia, troppo spesso il frontend rimane un enorme monolite.
Oggi esploreremo una soluzione architetturale avanzata: l'utilizzo di Angular e Webpack Module Federation combinato con un paradigma di rilascio strettamente legato al backend. Scopriremo come e perché pacchettizzare i nostri Micro Frontend (MFE) direttamente all'interno di file .jar (es. usando Spring Boot).
Il Problema Architetturale e il Pattern "End-to-End"
Il concetto di Micro Frontend serve proprio a dividere l'applicazione UI in domini di business, permettendo a team separati di sviluppare, testare e rilasciare la propria porzione di interfaccia in totale autonomia.
Ma dove "vive" questo frontend in produzione? Molti team optano per hostare i MFE su CDN o bucket S3 (AWS). Questo va bene, ma in scenari enterprise complessi o in infrastrutture legacy/ibride, l'approccio migliore è spesso il pattern Self-Contained Systems (SCS) o Backend for Frontend (BFF).
In questo modello, il team responsabile del dominio "Ordini" gestisce un unico repository contenente sia il microservizio Java che il modulo Angular. Quando la pipeline CI/CD genera l'artefatto, viene creato un singolo file .jar contenente sia le logiche di business che i file statici compilati del frontend.
1. Configurazione di Angular (Module Federation)
Per abilitare la federazione dei moduli in Angular, il pacchetto standard de facto è @angular-architects/module-federation. Una volta installato nel progetto del nostro MFE "Ordini", configuriamo il webpack.config.js per esporre il modulo.
// webpack.config.js (Remote MFE)
const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
name: 'ordersMfe',
exposes: {
'./OrdersModule': './src/app/orders/orders.module.ts',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});
2. Pacchettizzazione del Frontend nel JAR (Maven)
Il vero "trucco" architetturale avviene durante la build del backend. Utilizziamo il frontend-maven-plugin per delegare a Maven l'installazione delle dipendenze npm e la build di Angular. Successivamente, copiamo la cartella dist/ all'interno delle risorse pubbliche del JAR.
<!-- pom.xml -->
<build>
<plugins>
<!-- 1. Build Angular -->
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<executions>
<execution>
<id>npm build</id>
<goals><goal>npm</goal></goals>
<configuration>
<arguments>run build:mfe</arguments>
</configuration>
</execution>
</executions>
</plugin>
<!-- 2. Copia i file generati nelle resources di Spring Boot -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-frontend</id>
<phase>generate-resources</phase>
<goals><goal>copy-resources</goal></goals>
<configuration>
<outputDirectory>${project.build.outputDirectory}/public/orders-ui</outputDirectory>
<resources>
<resource>
<directory>${project.basedir}/frontend/dist/orders-mfe</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
3. Configurazione Lato Server (Spring Boot)
A questo punto, all'avvio del nostro JAR (es. sulla porta 8081), Spring Boot servirà automaticamente i file statici. Il file d'ingresso cruciale per l'orchestrazione è remoteEntry.js.
C'è però un dettaglio vitale: se il frontend Angular sfrutta l'History API del browser per il routing interno del modulo (es. /orders/history), il server backend riceverà richieste per percorsi che non conosce e restituirà un errore 404. È necessario implementare un Resource Handler per reindirizzare le richieste non mappate ai file statici del frontend, preservando lo stato lato client.
4. L'Orchestrazione (Shell App)
Infine, l'applicazione contenitore (la "Shell", solitamente deployata separatamente o sul gateway) caricherà a runtime il modulo esposto dal nostro JAR. Poiché ogni JAR risiede nel proprio ecosistema, il manifesto di federazione caricherà il modulo dinamicamente via URL.
// main.ts (Shell Application)
import { loadManifest } from '@angular-architects/module-federation';
// Carica la mappa dei micro frontend a runtime (può provenire da una API REST)
loadManifest({
"ordersMfe": "https://api.tuodominio.com/orders-service/public/orders-ui/remoteEntry.js"
})
.catch(err => console.error('Manifest load error', err))
.then(() => import('./bootstrap'));
Pro e Contro di questo Approccio
I Vantaggi:
- Deploy Atomico: Modifiche ad un'API backend e alla sua UI vengono rilasciate insieme. Niente più problemi di desincronizzazione tra client e server durante il deploy.
- Cross-Origin Resource Sharing (CORS): Poiché l'UI del dominio viene scaricata dallo stesso server dell'API del dominio, le comunicazioni relative a quel dominio sono naturally same-origin.
- Team Autonomi: Il team "Ordini" decide il proprio ciclo di rilascio senza bloccare il team "Utenti".
Gli Svantaggi:
- Accoppiamento: Il ciclo di vita del frontend è vincolato ai tempi di build del backend Java, che di solito sono più lenti rispetto a una semplice pipeline npm + CDN.
- Cache e CDN: Servire asset statici da un server Tomcat/Undertow (all'interno del JAR) è meno performante rispetto a servirli tramite una CDN globale edge, richiedendo accorgimenti infrastrutturali aggiuntivi (es. Cloudflare proxy).
In conclusione, integrare Micro Frontend in architetture JAR è una scelta estremamente potente per contesti enterprise fortemente "domain-driven", in cui i benefici della separazione organizzativa superano i trade-off legati all'infrastruttura di erogazione.