🏗️ Arquitectura y Patrones
Objetivo: Entender cuándo y cómo usar cada tipo de arquitectura, packages compartidos y patrones de comunicación en nuestro stack Vue.js 3 + Monorepo.
⚡ Principios de Arquitectura
Section titled “⚡ Principios de Arquitectura”🧩 Filosofía del Monorepo
Section titled “🧩 Filosofía del Monorepo”¿Por qué Monorepo?
- Reutilización real: Packages compartidos entre microfrontends (Types, Utils, vue-utils)
- Consistencia de versiones: Mismo Vue, TypeScript, Vite en todos los proyectos
- Refactoring cross-project: Cambios en tipos compartidos se propagan automáticamente
- Developer Experience: Un
git clone, unpnpm instally todo funciona
¿Por qué pnpm?
- Workspaces nativos: Gestión de dependencies entre packages
- Disk efficiency: Hard links reducen espacio en disco significativamente
- Lock file determinista: Builds reproducibles en cualquier máquina
- Catalog feature: Centralizamos versiones de dependencies críticas
🏗️ Stack Decisions
Section titled “🏗️ Stack Decisions”¿Por qué Pinia?
- Composition API nativo: Integración perfecta con Vue 3
- TypeScript first: Inferencia automática de tipos
- DevTools: Debugging superior vs state management manual
- Modularidad: Cada microfrontend puede tener sus propios stores
¿Por qué Vite?
- Hot Module Reload: Desarrollo ultrarrápido
- Tree shaking: Bundles optimizados automáticamente
- ES modules nativo: Mejor performance que bundlers tradicionales
- Plugin ecosystem: Integración perfecta con Vue + TypeScript
🎯 Arquitectura de Separación
Section titled “🎯 Arquitectura de Separación”- Separación de responsabilidades: Cada microfrontend tiene un propósito específico
- Reutilización inteligente: Solo compartir lo que realmente aporta valor
- Comunicación explícita: Interfaces claras entre componentes
- Independencia controlada: Autonomía sin fragmentación
🎯 Decision Tree Rápida
Section titled “🎯 Decision Tree Rápida”| ¿Se ejecuta en ASP.NET? | ¿Usa window.__params? | ¿Es reutilizable? | Solución |
|---|---|---|---|
| ✅ SÍ | ✅ SÍ | ❌ NO | Microfrontend Integrado |
| ❌ NO | ❌ NO | ❌ NO | Microfrontend Independiente |
| ❌ N/A | ❌ N/A | ✅ SÍ (con Vue) | Librería Vue (vue-*) |
| ❌ N/A | ❌ N/A | ✅ SÍ (sin Vue) | Librería Utilidades |
📁 Monorepo y Workspace
Section titled “📁 Monorepo y Workspace”🏗️ Estructura Organizacional
Section titled “🏗️ Estructura Organizacional”proyecto-vue/├── 📁 @apps/ # Microfrontends│ ├── 📁 flight-list/ # Listado de vuelos (Integrado)│ ├── 📁 flight-review/ # Revisión de reservas (Integrado)│ └── 📁 admin-panel/ # Panel administrativo (Independiente)├── 📁 packages/ # Packages compartidos│ ├── 📁 types/ # Tipos compartidos│ ├── 📁 utils/ # Utilidades generales│ ├── 📁 vue-utils/ # Utilidades específicas de Vue│ └── 📁 vue-modal/ # Componentes compartidos└── 📄 pnpm-workspace.yaml # Configuración del workspace⚙️ Gestión de Dependencies
Section titled “⚙️ Gestión de Dependencies”Catalog Strategy:
catalog: vue: ^3.5.13 typescript: ~5.8.3 pinia: ^2.3.0 vite: ^7.0.0Ventajas del Catalog:
- Versiones consistentes entre todos los proyectos
- Actualizaciones centralizadas
- Reducción de conflictos de dependencias
- Bundle size optimizado
🔄 Workflows del Workspace
Section titled “🔄 Workflows del Workspace”# Scripts por proyectopnpm app-flight-list dev # Desarrollo de microfrontendpnpm lib-utils build # Build de libreríapnpm lib-vue-modal dev # Desarrollo con watch
# Scripts globalespnpm build # Build todos los proyectospnpm builddev # Build desarrollo (sourcemaps)pnpm lint # Lint todo el workspace🔗 Microfrontends: Integrados vs Independientes
Section titled “🔗 Microfrontends: Integrados vs Independientes”1. 🏢 Microfrontends Integrados
Section titled “1. 🏢 Microfrontends Integrados”Características:
- Se ejecutan dentro de aplicaciones ASP.NET existentes
- Reciben configuración via
window.__params - No tienen navegación propia (single page dentro del host)
- Optimizados para integración seamless
Ejemplo Real: flight-list
// window.__params recibido desde ASP.NETinterface FlightListParams { flightSearchRequest: { isPackage: boolean; mode: number; tripMode: number; startingFromAirport: string; returningFromAirport: string; startingFromDateTime: string; returningFromDateTime: string; adults: number; kids: number; agekids: null | number[]; site: string; quoteList: boolean; quoteFlight: boolean; cacheTimeout: number; culture: string; fareMode: number; simpleFlightQuotes: boolean; step: boolean; currency: string; channelId: number; CriteriaFilter: { StopTypes: string[]; FlightTypes: string[]; DepartureTimeOfDay: string[]; ArrivalTimeOfDay: string[]; Airlines: string[]; }; CriteriaSort: { SortBy: number; SortDirection: number; }; QuoteTokenFQS: string; B2B2CToken: string; CarrierCodesList: string[]; NonStop: boolean; }; config: { baseUrl: string; showPoints: boolean; language: string; reviewPath: string; lottieUrl: string; };}Ejemplo Real: flight-review
// window.__params recibido desde ASP.NETinterface FlightReviewParams { config: { baseUrl: string; showPoints: boolean; language: string; currency: string; listPath: string; checkoutPath: string; lottieUrl: string; }; reviewData: { tripMode: string; startingFromAirport: string; returningFromAirport: string; checkIn: string; checkOut: string; adults: number; kids: number; ageKids: string; carrierCodesList: string[]; nonStop: boolean; revalidateToken: string; detailstoken: string; totalAmount: number; flightDetails: string; // JSON serializado con detalles del vuelo };}Cuándo usar:
- ✅ Reemplazar secciones específicas de aplicaciones ASP.NET existentes
- ✅ Necesitas datos del contexto del host (usuario, sesión, configuración)
- ✅ Navegación controlada por la aplicación host
- ✅ UI/UX consistente con el host
2. 🚀 Microfrontends Independientes
Section titled “2. 🚀 Microfrontends Independientes”Características:
- SPAs autónomos con su propia navegación
- No dependen de
window.__params - Manejo completo de rutas y estado
- Comunicación únicamente via APIs
Ejemplo: admin-panel
// main.ts - No config.window.tsimport { configEnv } from "@/config.env";
const app = await i18n(createApp(App), configEnv.I18Path, configEnv.I18Version);
app.provide("configEnv", configEnv);// Sin window.__paramsapp.use(createPinia());app.use(router); // Router completoapp.mount("#app");Cuándo usar:
- ✅ Aplicaciones administrativas o de gestión
- ✅ Funcionalidades completamente independientes
- ✅ Diferentes audiencias o niveles de acceso
- ✅ Despliegue independiente requerido
🔄 Patrón de Migración
Section titled “🔄 Patrón de Migración”Estrategia progresiva para convertir páginas ASP.NET:
- Fase 1: Identificar sección específica (ej: tabla de resultados)
- Fase 2: Crear microfrontend integrado
- Fase 3: Reemplazar sección manteniendo el host
- Fase 4: (Opcional) Migrar completa a independiente
📦 Packages Compartidos: Cuándo y Cómo
Section titled “📦 Packages Compartidos: Cuándo y Cómo”🎯 Criterios de Decisión
Section titled “🎯 Criterios de Decisión”✅ SÍ crear package compartido cuando:
- La funcionalidad se usa en 2+ microfrontends
- Es genérica, sin lógica de negocio específica
- Tiene valor como abstracción reutilizable
- Puede evolucionar independientemente
❌ NO crear package compartido cuando:
- Solo se usa en un microfrontend
- Contiene lógica de negocio muy específica
- Es más código mantener la abstracción que duplicar
- Crea dependencias circulares
📊 Caso Real: flight-list + flight-review
Section titled “📊 Caso Real: flight-list + flight-review”Packages Creados Desde Cero:
1. 📋 Package types - Tipos Compartidos
export interface FlightSearchRequest { isPackage: boolean; mode: number; tripMode: number; startingFromAirport: string; returningFromAirport: string; startingFromDateTime: string; returningFromDateTime: string; adults: number; kids: number; // ... resto de propiedades del ejemplo real}
export interface FlightDetails { departure: FlightLeg; returning: FlightLeg;}
export interface FlightLeg { arrival: Airport; departure: Airport; flightNumbers: string[]; duration: number; airline: Airline; stops: number;}2. 🔧 Package utils - Utilidades Generales
export function generateGUID(): string { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c === "x" ? r : (r & 0x3) | 0x8; return v.toString(16); });}
// packages/utils/src/http/WebRequestService.tsexport class WebRequestService { async get<T>(url: string): Promise<ApiResponse<T>> { // Manejo estandarizado de peticiones HTTP }
async post<T>(url: string, data: unknown): Promise<ApiResponse<T>> { // Lógica común para POST con error handling }}
// packages/utils/src/form/DynamicFormService.tsexport class DynamicFormService { generateForm(action: string, data: Record<string, any>): HTMLFormElement { const form = document.createElement("form"); form.method = "POST"; form.action = action;
Object.entries(data).forEach(([key, value]) => { const input = document.createElement("input"); input.type = "hidden"; input.name = key; input.value = typeof value === "object" ? JSON.stringify(value) : value; form.appendChild(input); });
return form; }
submitForm(form: HTMLFormElement): void { document.body.appendChild(form); form.submit(); }}3. 🌐 Package vue-utils - Configuración Estandarizada
export async function i18n( app: App, i18Path: string, version: string, isDebug = false,): Promise<App> { // Configuración estandarizada de i18next // Mismo patrón en todos los microfrontends}Packages Reutilizados de Otros Proyectos:
vue-modal: Componentes de modal estandarizadosvue-alerts: Sistema de alertas consistentevue-upsell: Componentes de upselling reutilizables
🚫 Anti-patrones Encontrados
Section titled “🚫 Anti-patrones Encontrados”Lo que NO compartieron (y por qué estuvo bien):
- Componentes de UI específicos: Cada microfrontend tiene sus propios componentes de lista/review
- Stores de Pinia: Cada uno maneja su propio estado
- Lógica de validación específica: Validaciones de vuelos vs validaciones de review son diferentes
📋 Checklist para Crear Package Compartido
Section titled “📋 Checklist para Crear Package Compartido”- ¿Se usa en 2+ microfrontends actualmente?
- ¿Es infraestructura/utilidad, no lógica de negocio?
- ¿Puede evolucionar sin romper dependientes?
- ¿Mantenerlo centralizado es menos trabajo que duplicarlo?
- ¿No crea dependencias circulares?
🔄 Patrones de Comunicación
Section titled “🔄 Patrones de Comunicación”1. 🪟 Host ASP.NET → Microfrontend (window.__params)
Section titled “1. 🪟 Host ASP.NET → Microfrontend (window.__params)”Patrón establecido:
// En la página ASP.NET<script>window.__params = { searchData: @Json.Serialize(Model.SearchData), config: { baseUrl: '@Model.BaseUrl', currency: '@Model.Currency', language: '@Model.Language', listPath: '@Url.Action("List", "Flight")', reviewPath: '@Url.Action("Review", "Flight")' }}</script>
// En el microfrontendimport { configWindow } from '@/config.window'// Acceso tipado y seguro a los parámetrosCasos de uso:
- Pasar datos del servidor (usuario loggeado, configuración)
- URLs de navegación entre páginas
- Configuración específica de la sesión
2. 🔄 Microfrontend → Host ASP.NET (Form POST)
Section titled “2. 🔄 Microfrontend → Host ASP.NET (Form POST)”Patrón real usado en flight-list → flight-review:
// Navegación desde flight-list hacia review mediante form POSTimport { DynamicFormService } from "@pnpmworkspace/utils/form";
const selectFlight = (selectedFlight: FlightOption) => { const formService = new DynamicFormService();
// Crear form dinámico con datos de la selección const form = formService.generateForm(configWindow.config.reviewPath, { flightSelection: JSON.stringify(selectedFlight), searchCriteria: JSON.stringify(configWindow.flightSearchRequest), returnUrl: window.location.href, });
// Submit automático hacia la página de review formService.submitForm(form);};Desde flight-review de regreso a flight-list:
const backToSearch = () => { const formService = new DynamicFormService();
const form = formService.generateForm(configWindow.config.listPath, { searchCriteria: JSON.stringify(originalSearchData), preserveFilters: true, });
formService.submitForm(form);};¿Por qué Form POST y no navigation simple?
- Preserva datos complejos entre páginas ASP.NET
- Mantiene estado del servidor (sesión, autenticación)
- Permite POST data que excede límites de URL
- Consistente con el patrón ASP.NET existente
3. 🤝 Microfrontend ↔ Microfrontend (Casos especiales)
Section titled “3. 🤝 Microfrontend ↔ Microfrontend (Casos especiales)”En el proyecto flight-list + flight-review: No hubo comunicación directa porque son secciones completamente separadas - cada uno vive en su propia página ASP.NET.
Para casos futuros donde coexistan en la misma página:
// Microfrontend emisorconst emitFlightSelected = (flight: Flight) => { const event = new CustomEvent("flight:selected", { detail: { flight }, }); window.dispatchEvent(event);};
// Microfrontend receptoronMounted(() => { const handleFlightSelected = (event: CustomEvent) => { const { flight } = event.detail; reviewStore.setSelectedFlight(flight); };
window.addEventListener("flight:selected", handleFlightSelected);
onUnmounted(() => { window.removeEventListener("flight:selected", handleFlightSelected); });});4. 📡 Microfrontend ↔ API (Estándar)
Section titled “4. 📡 Microfrontend ↔ API (Estándar)”// Usando el WebRequestService compartidoimport { WebRequestService } from "@pnpmworkspace/utils/http";
const apiService = new WebRequestService();const response = await apiService.get<Flight[]>(`${configEnv.ApiUrl}/flights`);
if (response.success) { flightStore.setFlights(response.data);} else { flightStore.setError(response.error);}🎯 Lecciones Aprendidas
Section titled “🎯 Lecciones Aprendidas”✅ Decisiones que Funcionaron Bien
Section titled “✅ Decisiones que Funcionaron Bien”1. Separación por Responsabilidad
flight-list: Solo listado y selecciónflight-review: Solo revisión y confirmación- Evita microfrontends “god objects”
2. Package types Centralizado
- Consistencia automática de tipos
- Refactorings más seguros
- Documentación implícita de contratos
3. HTTP Service Abstracto
- Manejo de errores consistente
- Logging centralizado
- Fácil testing con mocks
🔄 Refactorizaciones Durante el Desarrollo
Section titled “🔄 Refactorizaciones Durante el Desarrollo”Problema Inicial: Duplicación de lógica de Form POST
// flight-list/src/navigation.ts ❌ (código duplicado)const navigateToReview = (flightData: any) => { const form = document.createElement("form"); form.method = "POST"; form.action = "/flight/review";
// Lógica duplicada de creación de inputs Object.entries(flightData).forEach(([key, value]) => { const input = document.createElement("input"); input.type = "hidden"; input.name = key; input.value = JSON.stringify(value); form.appendChild(input); });
document.body.appendChild(form); form.submit();};
// flight-review/src/navigation.ts ❌ (misma lógica duplicada)const backToList = (searchData: any) => { // ... exactamente la misma lógica duplicada};Solución: DynamicFormService centralizado en utils
// packages/utils/src/form/DynamicFormService.ts ✅export class DynamicFormService { generateForm(action: string, data: Record<string, any>): HTMLFormElement { // Lógica centralizada y probada }
submitForm(form: HTMLFormElement): void { // Manejo consistente del submit }}
// En ambos microfrontends ✅import { DynamicFormService } from "@pnpmworkspace/utils/form";const formService = new DynamicFormService();Refactorización de HTTP handling
// Problema inicial: Cada micro manejaba errores HTTP diferente// Solución: WebRequestService con error handling estandarizado// Beneficios: Logging consistente, retry logic, error formatting¿Por qué esta refactorización fue exitosa?
- Identificamos código literalmente duplicado (no solo similar)
- La abstracción era simple y enfocada
- Ambos microfrontends usaban exactamente el mismo patrón
- Fácil de testear de forma aislada
🚫 Decisiones que Evitamos
Section titled “🚫 Decisiones que Evitamos”1. Package compartido para componentes UI
- Componentes específicos de cada dominio funcionan mejor separados
- Forzar reutilización de UI crea acoplamiento innecesario
2. Store compartido entre microfrontends
- Cada microfrontend maneja su propio estado
- La comunicación via eventos es más explícita
3. Routing compartido
- Cada microfrontend independiente tiene su propio router
- Los integrados no necesitan routing interno complejo
✅ Checklist de Arquitectura
Section titled “✅ Checklist de Arquitectura”🏗️ Antes de Crear un Microfrontend
Section titled “🏗️ Antes de Crear un Microfrontend”- ¿Justifica ser una aplicación separada?
- ¿Tiene responsabilidades claramente definidas?
- ¿Integrado (ASP.NET) o independiente (SPA)?
- ¿Qué datos necesita del host?
📦 Antes de Crear un Package Compartido
Section titled “📦 Antes de Crear un Package Compartido”- ¿Se usará en 2+ proyectos?
- ¿Es infraestructura, no lógica de negocio?
- ¿Puede evolucionar independientemente?
- ¿Evita dependencias circulares?
🔄 Patrones de Comunicación
Section titled “🔄 Patrones de Comunicación”- Host → Micro: via
window.__params - Micro → Host: via navigation o postMessage
- Micro ↔ API: via packages compartidos
- Micro ↔ Micro: excepcional, via Custom Events
🚀 Próximos Pasos
Section titled “🚀 Próximos Pasos”- Templates y Scaffolding - Crear nuevos proyectos
- Desarrollo Día a Día - Workflows habituales