Architettura MFE
Modello generale
@pzeta/mfe-contracts implementa il pattern Shell + Microfrontend (MFE): un'applicazione host (la "shell") carica dinamicamente moduli applicativi indipendenti, ciascuno con il proprio ciclo di vita, routing isolato e UI montata in uno slot DOM dedicato.
┌─────────────────────────────────────────────────────────────────┐
│ SHELL (host Vue) │
│ │
│ ┌──────────────┐ ┌─────────────────────────────────────┐ │
│ │ Sidebar nav │ │ MFE slot │ │
│ │ buildMenu... │ │ <div ref="mfeContainer" /> │ │
│ │ checkPerms.. │ │ │ │
│ └──────────────┘ │ ┌───────────────────────────────┐ │ │
│ │ │ MFE Vue app (isolated router) │ │ │
│ ┌──────────────┐ │ │ provide(MFE_CONTEXT_KEY, ctx) │ │ │
│ │ MFELoader │ │ │ └─ useMfeContext() in views │ │ │
│ │ - cache │───►│ └───────────────────────────────┘ │ │
│ │ - CSS auto │ └─────────────────────────────────────┘ │
│ │ - ESM import │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
▲
│ ESM dynamic import
│
┌─────────────┴─────────────┐
│ MFE bundle (remoto) │
│ - default: MFEModule │
│ - getManifest() │
│ - mount() / unmount() │
└───────────────────────────┘
Componenti del modello
Shell
Applicazione host che:
- conosce la lista dei MFE registrati (
MFEConfig[]), - usa
MFELoaderper caricarli ESM-dinamicamente, - costruisce la navigazione globale con
buildMenuFromManifest/buildSettingsMenuFromManifests, - valuta i permessi UI con
checkPermissions/ direttivav-can, - istanzia il
MFEContexte lo passa almount()di ciascun MFE attivo.
Microfrontend (MFE)
Modulo ESM autonomo che esporta (default) un oggetto MFEModule:
export default {
getManifest(): MFEManifest { /* ... */ },
async mount(container, context) { /* ... */ },
async unmount() { /* ... */ },
} satisfies MFEModule
Ogni MFE è un'applicazione Vue completa: ha il proprio createApp, il proprio router (in memory history, non browser), i propri stili. La shell non sa nulla del suo interno: vede solo il manifest e i tre lifecycle hook.
MFEContext
Bridge fornito dalla shell al MFE in fase di mount(). Contiene:
basePath— prefisso URL del MFE nella shellonNavigate(path)— callback per navigazione coordinatahttp— client HTTP con autenticazione già gestitaeventBus— bus globale per comunicazione cross-MFEtheme— gestione tema condivisa (light/dark/system)getUser()/getToken()— utente e token correnti
Vedi MFEContext per il contratto completo.
Lifecycle di un MFE
┌──────────────────────────────────────────────────────────────┐
│ 1. Shell.startup │
│ └─ MFELoader.loadRemote({ name, url, scope, module }) │
│ ├─ valida URL (https:/http:) │
│ ├─ inietta CSS sibling (se esiste) │
│ ├─ dynamic ESM import(url) │
│ ├─ valida MFEModule (getManifest/mount/unmount) │
│ └─ chiama optional onLoad() │
│ │
│ 2. Shell costruisce menu con manifest.routes │
│ │
│ 3. Utente naviga su una route del MFE │
│ └─ Shell chiama mfe.mount(container, ctx) │
│ └─ MFE: createApp + provide(MFE_CONTEXT_KEY, ctx) │
│ + memoryHistory(ctx.basePath) │
│ │
│ 4. (opzionale) Shell cambia rotta → mfe.onRouteChange(path) │
│ │
│ 5. Utente esce dal MFE │
│ └─ Shell chiama mfe.onBeforeUnmount?() poi mfe.unmount() │
│ └─ MFE: app.unmount() + cleanup router │
└──────────────────────────────────────────────────────────────┘
Isolation del routing
Il punto chiave per evitare conflitti tra shell router e MFE router:
// Dentro il MFE
const router = createRouter({
history: createMemoryHistory(context.basePath), // ⚠️ memory, NON web
routes,
})
createMemoryHistory è obbligatorio: il browser ha un solo History API, già occupato dalla shell. Il MFE deve usare una history "in memoria" e sincronizzare la navigazione via context.onNavigate().
router.afterEach((to) => {
context.onNavigate(to.path)
})
La shell intercetta la callback e aggiorna l'URL nel browser (via il proprio router).
Comunicazione cross-MFE
Tre canali, in ordine di accoppiamento crescente:
| Canale | Quando | Esempio |
|---|---|---|
| EventBus | Notifiche fire-and-forget | ctx.eventBus.emit('user-created', { id }) |
| HTTP shared | Dati persistenti via API | ctx.http.get('/api/users') |
Componenti esposti (manifest.components) | Riuso UI cross-MFE | <UserCard :user="u" /> da MFE A in MFE B |
Da evitare: dipendenze dirette tra MFE (import statici, type sharing). Tutto passa per il MFEContext o per il manifest.
Permission model
I permessi sono UI-only: nascondono/disabilitano elementi della UI in base ai permessi dell'utente, ma non sono un gate di sicurezza. La sicurezza reale è server-side.
- Manifest-level:
manifest.permissionsindica i permessi richiesti per accedere al MFE - Route-level:
route.permissionfiltra le voci nel menu - Element-level: direttiva
v-can="'modulo:resource:write'"nasconde/disabilita componenti
Vedi checkPermissions e createCanDirective.
Sezione "settings"
Le route possono essere classificate in due sezioni tramite meta.section:
'app'(default) — rail di navigazione principale'settings'— rail "Impostazioni" della shell
{
path: '/utenti',
label: 'Gestione utenti',
meta: { section: 'settings', order: 10 },
}
La shell aggrega tutte le route settings dei MFE registrati con buildSettingsMenuFromManifests e le monta sotto /settings/{manifest.name}/....
Vincoli e best practice
- Usare
createMemoryHistorynei MFE - Sincronizzare nav con
context.onNavigate() - Tutti gli HTTP call via
context.http(auth gestita dalla shell) - Validare
manifestricevuto da fonti esterne convalidateManifest - Cleanup totale in
unmount()(app.unmount, router, listener, intervals)
- Importare staticamente codice da altri MFE
- Usare
createWebHistoryocreateWebHashHistorydentro un MFE - Bypassare
context.httpper autenticazione custom - Fidarsi dei permessi client come gate di sicurezza
- Lasciare side-effects globali dopo
unmount()