Ir al contenido

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.

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.

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 — React
import { wuReact } from 'wu-framework/adapters/react';
import App from './App.jsx';
wuReact.register('cart', App);
// catalog — Vue 3
import { wuVue } from 'wu-framework/adapters/vue';
import App from './App.vue';
wuVue.register('catalog', App);
// header — Svelte
import { wuSvelte } from 'wu-framework/adapters/svelte';
import App from './App.svelte';
wuSvelte.register('header', App);
// analytics — vanilla
import { 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.

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/react del mismo origen, así que el navegador evalúa un solo módulo. Sale gratis, sin configurar nada.
  • En producción, "shared": ["react"] en wu.config.json empaqueta 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 install instala 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.

No se importa nada a través de las fronteras entre apps. Usa el sustrato:

// app cart
wu.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 existe
wu.store.on('cart.total', (total) => { badge = total; });
// app analytics (vanilla) — escucha todo
wu.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 servicio
wu.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 versiones
const 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.

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 shell

Un 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.

  • Todas las apps son tuyas → module en 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) → strict para las culpables.
  • Una app es un bundle UMD ya compilado → eval solo 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.

Ejecuta cada app en su propio puerto con CORS habilitado:

// vite.config.js en cada micro-app
export default {
server: { port: 5173, cors: true },
};
// shell, en desarrollo
await 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.

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 vivos
snap.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.