Shell multi-framework
El escenario para el que existe Wu: cuatro equipos, cuatro frameworks, una pantalla.
┌─ Shell (HTML plano, o Astro, o Next) ───────────────────┐│ ┌── #header ──────────────────────────────────────┐ ││ │ Svelte │ ││ └──────────────────────────────────────────────────┘ ││ ┌── #catalog ────────────┐ ┌── #cart ─────────────┐ ││ │ Vue 3 │ │ React 18 │ ││ └────────────────────────┘ └───────────────────────┘ ││ ┌── #analytics ───────────────────────────────────┐ ││ │ JavaScript vanilla │ ││ └──────────────────────────────────────────────────┘ │└──────────────────────────────────────────────────────────┘Cada uno es un bundle construido y desplegado de forma independiente. El shell solo conoce sus URLs.
El shell
Sección titulada «El shell»import { wu } from 'wu-framework';
await wu.init({ apps: [ { name: 'header', url: 'https://header.example.com' }, { name: 'catalog', url: 'https://catalog.example.com' }, { name: 'cart', url: 'https://cart.example.com' }, { name: 'analytics', url: 'https://analytics.example.com' }, ],});
await Promise.all([ wu.mount('header', '#header'), wu.mount('catalog', '#catalog'), wu.mount('cart', '#cart'), wu.mount('analytics', '#analytics'),]);Montar en paralelo está bien: cada app recibe su propio shadow root y su propio pipeline de carga.
Las apps
Sección titulada «Las apps»Cada app se registra con el adaptador de su framework. Esa es la única línea específica de Wu en toda la app.
// cart — Reactimport { wuReact } from 'wu-framework/adapters/react';import App from './App.jsx';wuReact.register('cart', App);// catalog — Vue 3import { wuVue } from 'wu-framework/adapters/vue';import App from './App.vue';wuVue.register('catalog', App);// header — Svelteimport { wuSvelte } from 'wu-framework/adapters/svelte';import App from './App.svelte';wuSvelte.register('header', App);// analytics — vanillaimport { wuVanilla } from 'wu-framework/adapters/vanilla';
wuVanilla.register('analytics', { mount(container) { container.innerHTML = '<div id="chart"></div>'; }, unmount(container) { container.innerHTML = ''; },});Cada una publica su wu.json:
{ "name": "cart", "entry": "assets/index-a1b2c3.js", "styleMode": "shared" }Adaptadores disponibles: react, vue, angular, svelte, preact, solid,
lit, vanilla, alpine, qwik, stencil, htmx, stimulus. Mira
Adaptadores.
Lo que esto cuesta de verdad
Sección titulada «Lo que esto cuesta de verdad»Ten clara la contrapartida antes de comprometerte:
Si no compartes el runtime, cada app carga el suyo. Dos apps React son dos copias de React descargadas, parseadas y en memoria.
Con wu-cli esto ya está resuelto y no hay que montarlo a mano:
- En desarrollo, todas las apps importan
/@modules/reactdel mismo origen, así que el navegador evalúa un solo módulo. Sale gratis, sin configurar nada. - En producción,
"shared": ["react"]enwu.config.jsonempaqueta una sola copia y la resuelve por import map. Medido en un proyecto real:dist/de 807 KB a 425 KB, cada micro-app de 207 KB a 12 KB. Ver compartir dependencias. - Si dos apps necesitan majors distintos,
wu installinstala la perdedora bajo un alias de npm y cada una recibe la suya, en desarrollo y en producción. Ver versiones por app.
Sin el CLI sigue existiendo la vía manual —los adapters de React, Vue, Preact,
Solid, Angular y Alpine reutilizan el global si existe, así que puedes exponer
window.React desde el shell y marcar React como external en cada app—, pero
ahí eres tú quien responde por las versiones: Wu no comprueba lo que reutiliza,
y si el shell publica una major distinta de la que espera una app, no hay
aviso.
Donde Wu gana es en el caso de arriba: cuatro frameworks genuinamente distintos, donde no hay nada que compartir de todos modos, o una migración en la que React y Vue deben coexistir durante dos trimestres.
Mitigaciones que sí funcionan:
- Pon la UI compartida en contratos de capacidades en lugar de bundles compartidos, para que las apps llamen a los objetos vivos de las otras en vez de importar código.
- Mantén los recursos realmente comunes (fuentes, sprites de iconos, el CSS del
design system) en el documento anfitrión y usa
styleMode: 'shared', para que se descarguen una sola vez. - El caché HTTP sí ayuda entre apps que fijan la misma versión del framework desde la misma URL de CDN, pero no deduplica el parseo ni la memoria.
Coordinación entre ellas
Sección titulada «Coordinación entre ellas»No se importa nada a través de las fronteras entre apps. Usa el sustrato:
// app cartwu.emit('cart:item-added', { sku: 'SKU-42', price: 19.9 });wu.store.set('cart.total', 142.5);
// app header (Svelte) — el badge se actualiza sin saber que cart existewu.store.on('cart.total', (total) => { badge = total; });
// app analytics (vanilla) — escucha todowu.on('cart:*', (event) => track(event.name, event.data));Cuando una app necesita llamar a otra, usa un contrato en vez de un evento:
// cart publica un serviciowu.provide('cart', { add, remove, clear }, { version: '2.1.0', shape: { add: 'function', remove: 'function', clear: 'function' }, app: 'cart',});
// catalog lo consume, con un rango de versionesconst cart = wu.consume('cart', '^2.0');cart.add('SKU-42');Si cart publica 3.0.0, el consume de catalog lanza un error con un mensaje
claro en vez de llamar a una API incompatible. Mira
Comunicación.
Estilar cuatro frameworks a la vez
Sección titulada «Estilar cuatro frameworks a la vez»Las apps casi con certeza no se ponen de acuerdo sobre el CSS. styleMode es
por app, declarado en el wu.json de cada una:
{ "styleMode": "shared" } // hereda el design system del shell{ "styleMode": "isolated" } // trae todo por su cuenta{ "styleMode": "fully-isolated" } // su propio CSS, nada del shellUn patrón útil para un parque mixto: el shell es dueño de los tokens como
propiedades personalizadas CSS, las apps usan isolated y leen los tokens. Las
propiedades personalizadas se heredan a través de la frontera del shadow DOM,
así que un --brand en :root llega a cada app incluso en el modo más
estricto.
Mira Aislamiento CSS.
Elegir sandbox para una página mixta
Sección titulada «Elegir sandbox para una página mixta»- Todas las apps son tuyas →
moduleen todas. Lo más rápido, con HMR intacto. - Las apps se pelean por los globales (dos versiones de React, polyfills que
compiten, una librería que parchea prototipos) →
strictpara las culpables. - Una app es un bundle UMD ya compilado →
evalsolo para esa.
Los modos son por app, así que puedes mezclar:
await wu.init({ sandbox: 'module', apps: [ { name: 'cart', url: '…' }, // module { name: 'legacy', url: '…', sandbox: 'eval' }, // UMD { name: 'vendor', url: '…', sandbox: 'strict' }, // realm propio ],});Mira Modos de sandbox.
Configuración de desarrollo
Sección titulada «Configuración de desarrollo»Ejecuta cada app en su propio puerto con CORS habilitado:
// vite.config.js en cada micro-appexport default { server: { port: 5173, cors: true },};// shell, en desarrolloawait wu.init({ apps: [ { name: 'cart', url: 'http://localhost:5173' }, { name: 'catalog', url: 'http://localhost:5174' }, ],});Cada app también corre sola en su propio puerto: los adaptadores montan en
#root cuando no hay shell presente. Así un equipo puede desarrollar aislado y
probar en el shell sin cambiar una línea.
Para apuntar una app a la rama de un compañero sin tocar la configuración del shell, mira Overrides de QA.
Depurar una página mixta
Sección titulada «Depurar una página mixta»Las devtools de cada framework solo ven su propia app. wu.inspect() es la
vista transversal:
const snap = wu.inspect();snap.apps; // [{ name, status, framework, sandbox, props, … }]snap.capabilities; // contratos vivossnap.events.recent;snap.store.snapshot;Para bugs de secuencia entre frameworks — “la app Vue renderizó antes de que la app React escribiera en el store” — el timeline registra escrituras del store, emisiones de eventos y transiciones de ciclo de vida en un solo diario, y puede reproducir la página entera paso a paso.
