Getting Started
Quick Start
Esempio end-to-end per implementare un MFE Vue, esporre il manifest, caricarlo dalla shell e usare i composables CRUD.
1. Crea il MFE
Entry point del MFE
// src/mfe-entry.ts
import { createApp, type App } from 'vue'
import { createRouter, createMemoryHistory, type Router } from 'vue-router'
import type {
MFEModule,
MFEManifest,
MFEContext,
} from '@pzeta/mfe-contracts'
import { MFE_CONTEXT_KEY } from '@pzeta/mfe-contracts'
import AppRoot from './App.vue'
import routes from './router/routes'
let app: App | null = null
let router: Router | null = null
const mfeModule: MFEModule = {
getManifest(): MFEManifest {
return {
name: 'crm-module',
version: '1.0.0',
description: 'Gestione clienti, ticket e pipeline CRM',
icon: 'pi-building',
routes: [
{
path: '/crm/dashboard',
label: 'Dashboard',
icon: 'pi-chart-line',
category: 'CRM',
permission: 'crm.dashboard.read',
meta: { requiresAuth: true, order: 1 },
},
{
path: '/crm/clienti',
label: 'Clienti',
icon: 'pi-users',
category: 'CRM',
permission: 'crm.clienti.read',
meta: { order: 2 },
},
{
path: '/templates',
label: 'Template CRM',
icon: 'pi-file',
meta: { section: 'settings', order: 1 },
},
],
permissions: ['crm.read'],
}
},
async mount(container: HTMLElement, context: MFEContext) {
router = createRouter({
history: createMemoryHistory(context.basePath),
routes,
})
router.afterEach((to) => {
context.onNavigate(to.path)
})
app = createApp(AppRoot)
app.use(router)
app.provide(MFE_CONTEXT_KEY, context)
app.mount(container)
},
async unmount() {
app?.unmount()
app = null
router = null
},
onRouteChange(path) {
router?.push(path).catch(() => {/* race con unmount: ignora */})
},
}
export default mfeModule
Configurazione Vite (build come libreria ESM)
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: 'src/mfe-entry.ts',
formats: ['es'],
fileName: 'index',
},
rollupOptions: {
external: ['vue', 'vue-router'],
output: {
// ATTENZIONE: usa globals SOLO se la shell li espone come UMD
// Per ESM lascia esterni e import-mapped via shell
},
},
},
})
2. Carica il MFE dalla shell
// shell/main.ts
import { MFELoader, type MFEConfig } from '@pzeta/mfe-contracts'
const loader = new MFELoader()
const config: MFEConfig = {
name: 'crm-module',
url: 'https://mfes.example.com/crm/index.js',
scope: 'crm',
module: './module',
}
const mfe = await loader.loadRemote(config)
const manifest = await mfe.getManifest()
console.log('MFE caricato:', manifest.name, manifest.version)
Costruisci il menu dalla shell
import {
buildMenuFromManifest,
buildSettingsMenuFromManifests,
} from '@pzeta/mfe-contracts/helpers'
const manifests = [manifest /*, manifest2, ... */]
const appMenu = manifests.flatMap((m) => buildMenuFromManifest(m, 'app'))
const settingsMenu = buildSettingsMenuFromManifests(manifests)
Monta il MFE in uno slot DOM
<!-- shell/MfeSlot.vue -->
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import type { MFEModule, MFEContext } from '@pzeta/mfe-contracts'
const props = defineProps<{
mfe: MFEModule
context: MFEContext
}>()
const container = ref<HTMLElement | null>(null)
onMounted(async () => {
if (container.value) {
await props.mfe.mount(container.value, props.context)
}
})
onBeforeUnmount(async () => {
await props.mfe.onBeforeUnmount?.()
await props.mfe.unmount()
})
</script>
<template>
<div ref="container" class="mfe-slot" />
</template>
3. Componenti CRUD nel MFE
Esempio di vista lista clienti che usa useCrudResource + useCrudView:
<script setup lang="ts">
import {
useMfeContext,
useCrudResource,
useCrudViewFromResource,
} from '@pzeta/mfe-contracts/composables'
interface Cliente { id: string; ragioneSociale: string; piva: string }
interface CreateClienteRequest { ragioneSociale: string; piva: string }
const ctx = useMfeContext()
const resource = useCrudResource<Cliente, CreateClienteRequest>({
resourceName: 'clienti',
loadItems: async () => (await ctx.http.get<Cliente[]>('/api/clienti')).data,
createItem: async (p) => (await ctx.http.post<Cliente>('/api/clienti', p)).data,
updateItem: async (id, p) =>
(await ctx.http.put<Cliente>(`/api/clienti/${id}`, p)).data,
deleteItem: async (id) => { await ctx.http.delete(`/api/clienti/${id}`) },
})
const view = useCrudViewFromResource(resource, {
singularName: 'cliente',
getDisplayName: (c) => c.ragioneSociale,
})
await resource.load()
</script>
<template>
<div>
<button @click="view.handleCreate">Nuovo cliente</button>
<ul>
<li v-for="c in resource.items.value" :key="c.id">
{{ c.ragioneSociale }}
<button @click="view.handleEdit(c.id)">Modifica</button>
<button @click="view.handleDelete(c.id)">Elimina</button>
</li>
</ul>
</div>
</template>
4. Permessi inline con v-can
// shell/main.ts
import { createCanDirective } from '@pzeta/mfe-contracts/helpers'
import { computed } from 'vue'
const userPermissions = computed(() => currentUser.value?.permissions ?? [])
app.directive('can', createCanDirective(() => userPermissions.value))
<template>
<!-- Nasconde se l'utente non ha il permesso -->
<button v-can="'crm.clienti.delete'" @click="onDelete">
Elimina
</button>
<!-- Disabilita invece di nascondere -->
<button v-can.disable="'crm.clienti.export'" @click="onExport">
Esporta CSV
</button>
<!-- OR logic: basta uno -->
<a v-can="['crm.clienti.read', 'crm.admin']" :href="`/clienti/${id}`">
Apri
</a>
</template>
Prossimi passi
- Architettura MFE — modello completo
- Types — tutti i contratti tipizzati
- Composables —
useCrudView,useDetailForm,useDialog, ... - Helpers — validazione manifest, menu, permessi, stili