Micro-frontends
Arquitetura
Frontend
Escalabilidade
React
Module Federation

Micro-frontend: quando e perché adottarlo in progetti scalabili

Micro-frontend: quando e perché adottarlo in progetti scalabili

Nel 2025, con applicazioni web sempre più complesse, l'architettura micro-frontend è diventata una soluzione matura per scalare sia lo sviluppo che la manutenzione di applicazioni di grandi dimensioni. Esploriamo quando e come implementare questa architettura in modo efficiente.

Fondamenti di micro-frontend

1. Concetti fondamentali

// Exemplo de configuração de Module Federation // webpack.config.js const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin'); module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'container', remotes: { marketing: 'marketing@http://localhost:8081/remoteEntry.js', dashboard: 'dashboard@http://localhost:8082/remoteEntry.js', auth: 'auth@http://localhost:8083/remoteEntry.js', }, shared: ['react', 'react-dom'], }), ], };

2. Architettura tipica

L'architettura è organizzata attorno a un'applicazione contenitore che carica, su richiesta, i micro-frontend di ciascun dominio, ad esempio uno per il marketing, un altro per la dashboard e un altro per autenticazione. Ciascuno di questi micro-frontend utilizza uno strato comune di componenti condivisi, garantendo coerenza visiva e comportamentale senza associare i team.

Quando adottare i microfrontend

1. Indicatori di necessità

interface ProjectAssessment { teamSize: number; numberOfFeatures: number; deploymentFrequency: number; technicalDebt: { monolithComplexity: number; buildTime: number; testCoverage: number; }; teamAutonomy: { independent: boolean; technicalDecisions: boolean; deploymentControl: boolean; }; } function shouldAdoptMicroFrontends( assessment: ProjectAssessment ): RecommendationReport { const score = calculateAdoptionScore(assessment); return { recommendation: score > 0.7 ? 'Adopt' : 'Evaluate Further', reasons: analyzeFactors(assessment), risks: identifyRisks(assessment), nextSteps: generateRoadmap(assessment), }; }

2. Analisi dei compromessi

AspettoVantaggiSfide
ScalabilitàSquadre indipendentiComplessità iniziale
PrestazioniRicarica su richiestaSovraccarico di esecuzione
ManutenzioneCodice isolatoCoerenza tra le app
DistribuireIndipendenteCoordinamento necessario
SviluppoAutonomiaCurva di apprendimento

Implementazione con la Federazione dei Moduli

1. Applicazione contenitore

// src/App.tsx import { lazy, Suspense } from 'react'; import { Router, Route, Switch } from 'react-router-dom'; const MarketingApp = lazy(() => import('./remotes/MarketingApp')); const DashboardApp = lazy(() => import('./remotes/DashboardApp')); const AuthApp = lazy(() => import('./remotes/AuthApp')); export default function App() { return ( <Router> <Suspense fallback={<LoadingSpinner />}> <Switch> <Route path="/auth" component={AuthApp} /> <Route path="/dashboard" component={DashboardApp} /> <Route path="/" component={MarketingApp} /> </Switch> </Suspense> </Router> ); } // src/bootstrap.tsx import { createRoot } from 'react-dom/client'; import App from './App'; const mount = (el: HTMLElement) => { const root = createRoot(el); root.render(<App />); }; const devRoot = document.querySelector('#root'); if (devRoot) { mount(devRoot); } export { mount };

2. Applicazioni remote

// marketing/src/bootstrap.tsx import { createRoot } from 'react-dom/client'; import { createMemoryHistory, createBrowserHistory } from 'history'; import App from './App'; interface MountOptions { defaultHistory?: typeof createMemoryHistory; initialPath?: string; onNavigate?: (location: any) => void; } const mount = ( el: HTMLElement, { defaultHistory, initialPath, onNavigate, }: MountOptions = {} ) => { const history = defaultHistory?.({ initialEntries: [initialPath || '/'], }) || createBrowserHistory(); if (onNavigate) { history.listen(onNavigate); } const root = createRoot(el); root.render(<App history={history} />); return { onParentNavigate({ pathname: nextPathname }) { const { pathname } = history.location; if (pathname !== nextPathname) { history.push(nextPathname); } }, }; }; if (process.env.NODE_ENV === 'development') { const devRoot = document.querySelector('#_marketing-dev-root'); if (devRoot) { mount(devRoot, { defaultHistory: createBrowserHistory }); } } export { mount };

3. Condivisione dello stato

// shared/store/index.ts import create from 'zustand'; interface SharedState { user: User | null; theme: 'light' | 'dark'; setUser: (user: User | null) => void; setTheme: (theme: 'light' | 'dark') => void; } const useSharedStore = create<SharedState>((set) => ({ user: null, theme: 'light', setUser: (user) => set({ user }), setTheme: (theme) => set({ theme }), })); export { useSharedStore }; // shared/events/index.ts type EventCallback = (data: any) => void; class EventBus { private events: Map<string, Set<EventCallback>>; constructor() { this.events = new Map(); } subscribe(event: string, callback: EventCallback) { if (!this.events.has(event)) { this.events.set(event, new Set()); } this.events.get(event)!.add(callback); return () => { this.events.get(event)?.delete(callback); }; } publish(event: string, data: any) { this.events.get(event)?.forEach((callback) => { callback(data); }); } } export const eventBus = new EventBus();

Modelli di progettazione

1. Composizione della domanda

// container/src/components/MicroFrontend.tsx interface MicroFrontendProps { name: string; host: string; history?: History; } function MicroFrontend({ name, host, history }: MicroFrontendProps) { useEffect(() => { const scriptId = `micro-frontend-script-${name}`; const renderMicroFrontend = () => { // @ts-ignore window[`render${name}`]( `${name}-container`, history ); }; if (document.getElementById(scriptId)) { renderMicroFrontend(); return; } fetch(`${host}/asset-manifest.json`) .then((res) => res.json()) .then((manifest) => { const script = document.createElement('script'); script.id = scriptId; script.crossOrigin = ''; script.src = `${host}${manifest.files['main.js']}`; script.onload = () => { renderMicroFrontend(); }; document.head.appendChild(script); }); return () => { // @ts-ignore window[`unmount${name}`]?.(`${name}-container`); }; }, []); return <div id={`${name}-container`} />; }

2. Comunicazione tra app

// shared/communication/index.ts type MessageHandler = (data: any) => void; class MessageBus { private handlers: Map<string, Set<MessageHandler>>; constructor() { this.handlers = new Map(); window.addEventListener('message', (event) => { const { type, data } = event.data; this.handlers.get(type)?.forEach((handler) => { handler(data); }); }); } subscribe(type: string, handler: MessageHandler) { if (!this.handlers.has(type)) { this.handlers.set(type, new Set()); } this.handlers.get(type)!.add(handler); return () => { this.handlers.get(type)?.delete(handler); }; } publish(type: string, data: any) { window.postMessage({ type, data }, '*'); } } export const messageBus = new MessageBus();

Migliori pratiche

1. Prestazioni

// shared/performance/index.ts interface PerformanceMetrics { ttfb: number; fcp: number; lcp: number; fid: number; cls: number; } class PerformanceMonitor { private metrics: Map<string, PerformanceMetrics>; constructor() { this.metrics = new Map(); this.setupObservers(); } private setupObservers() { new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { this.recordMetric(entry); } }).observe({ entryTypes: ['web-vital'] }); } private recordMetric(entry: PerformanceEntry) { const appName = this.getAppName(); if (!this.metrics.has(appName)) { this.metrics.set(appName, { ttfb: 0, fcp: 0, lcp: 0, fid: 0, cls: 0, }); } const metrics = this.metrics.get(appName)!; metrics[entry.name as keyof PerformanceMetrics] = entry.value; } }

2. Test

// shared/testing/index.ts import { render } from '@testing-library/react'; import { createMemoryHistory } from 'history'; interface TestConfig { route?: string; initialState?: any; mocks?: any[]; } function renderWithRouter( ui: React.ReactElement, { route = '/', initialState = {}, mocks = [], }: TestConfig = {} ) { const history = createMemoryHistory({ initialEntries: [route], }); return { ...render(ui), history, }; } export { renderWithRouter };

Sfide e soluzioni

1. Coerenza visiva

// shared/design-system/index.ts import { createTheme, ThemeProvider } from '@mui/material/styles'; const baseTheme = createTheme({ palette: { primary: { main: '#1976d2', }, secondary: { main: '#dc004e', }, }, typography: { fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', }, components: { MuiButton: { styleOverrides: { root: { textTransform: 'none', }, }, }, }, }); export { baseTheme, ThemeProvider };

2. Monitoraggio

// shared/monitoring/index.ts interface MonitoringConfig { appName: string; version: string; environment: string; } class ApplicationMonitor { constructor(private config: MonitoringConfig) { this.setupErrorBoundary(); this.setupPerformanceMonitoring(); this.setupUserMonitoring(); } private setupErrorBoundary() { window.addEventListener('error', (event) => { this.logError({ type: 'uncaught', error: event.error, location: event.filename, line: event.lineno, column: event.colno, }); }); } private logError(error: any) { console.error('[MFE Error]', { ...error, app: this.config.appName, version: this.config.version, environment: this.config.environment, timestamp: new Date().toISOString(), }); } }

Conclusione

L'adozione di micro-frontend offre vantaggi significativi per progetti scalabili:

  1. Scalabilità organizzativa: team autonomi e indipendenti
  2. Flessibilità tecnica: libertà di scegliere le tecnologie
  3. Distribuzione indipendente: rischi ridotti e maggiore velocità
  4. Manutenibilità: codice più organizzato e isolato
  5. Evoluzione graduale: possibilità di migrazione incrementale

Passaggi successivi

  1. Valuta la reale necessità del tuo progetto
  2. Iniziare con un progetto pilota in un'area non critica
  3. Stabilire standard e governance
  4. Investire in automazione e strumenti
  5. Monitorare e regolare continuamente

Stai pensando di adottare micro-frontend nel tuo progetto? Condividi le tue domande ed esperienze nei commenti qui sotto!

Leggi anche