Ir al contenido

Ciclo de vida

Todo en Wu acaba pasando por estas ocho llamadas. El shell llama a init y mount. La micro-app llama a define. Nada más es obligatorio.

Todos los ejemplos asumen el import del singleton:

import { wu } from 'wu-framework';

El mismo objeto está en window.wu en el navegador, y así es como una app cargada de forma remota alcanza el framework sin importarlo. Para un acceso a prueba de colisiones, window[Symbol.for('wu-framework')] apunta a la misma instancia.

init(config: {
apps: WuAppConfig[];
sandbox?: 'module' | 'strict' | 'eval';
strictFallback?: boolean;
overrides?: any;
}): Promise<void>

Registra apps en el shell. Por cada entrada descarga el manifiesto de la app (wu.json), lo guarda y marca la app como registered. Todavía no se descarga ni se monta nada, salvo que una estrategia de carga lo pida.

Opción Tipo Por defecto Significado
apps WuAppConfig[] [] Apps a registrar. Cada una necesita al menos name y url.
sandbox 'module' | 'strict' | 'eval' 'module' Modo global de aislamiento de JS. El sandbox por app tiene prioridad sobre este.
strictFallback boolean false Si un import fallido del iframe strict puede degradar a eval.
overrides object Se pasa al subsistema de overrides antes de registrar las apps, para que un override de QA se aplique a la URL que queda registrada.

Configuración por app (WuAppConfig):

Campo Tipo Significado
name string Identificador que usan todas las demás llamadas. Debe coincidir con el nombre que la app pasa a wu.define().
url string URL base desde la que se sirve la app. El manifiesto se descarga en relación a ella.
container string Selector de contenedor por defecto (lo usa wu.app()).
keepAlive boolean Ocultar en lugar de destruir al hacer unmount().
sandbox 'module' | 'strict' | 'eval' Modo de aislamiento por app.
strictFallback boolean Override por app de la política global de fallback.
styleMode 'shared' | 'isolated' | 'fully-isolated' | 'none' | 'own-only' Aislamiento de CSS. none es alias de isolated; own-only es alias de fully-isolated.
roles string[] RBAC. Si está presente, solo un principal autorizado puede montar la app. Ver RBAC.
await wu.init({
sandbox: 'module',
apps: [
{ name: 'header', url: 'https://cdn.example.com/header' },
{ name: 'cart', url: 'https://cdn.example.com/cart', keepAlive: true },
{ name: 'admin', url: 'https://cdn.example.com/admin', roles: ['admin'] },
],
});

Casos límite. Llamar a init() dos veces no lanza — registra Framework already initialized y devuelve de inmediato, dejando intacto el registro existente. Eso significa que un segundo init() con otra lista de apps se ignora silenciosamente; si necesitas volver a registrar, llama antes a wu.destroy(). Si el manifiesto de alguna app no carga, todo el init() se rechaza con ese error (el registro no tolera resultados parciales). Un hook beforeInit que cancela hace que init() devuelva sin marcar isInitialized.

Antes de la v2.7 su valor por defecto era true. Una app que aislaste deliberadamente en un iframe caía silenciosamente a modo eval en cuanto faltaba una cabecera CORS: una decisión explícita se convertía en un camino accidental, y encima uno que un atacante podía provocar rompiendo CORS. Desde la v2.7 el valor por defecto es false y el montaje lanza en su lugar, nombrando la app y el error original.

Vuelve a activarlo solo cuando la degradación sea realmente aceptable:

await wu.init({
sandbox: 'strict',
strictFallback: true, // volver al comportamiento previo a 2.7
apps: [{ name: 'legacy-widget', url: 'http://localhost:5173' }],
});

Ten en cuenta que el modo eval no puede ejecutar módulos ES, así que para un bundle moderno el “fallback” normalmente convierte una app que funciona en una app rota, en lugar de en una menos aislada. Verifica lo que realmente obtuviste con wu.getSandboxInfo().

wu.whenReady(): Promise<boolean>

Resuelve cuando init() ha terminado — ya sea porque terminó hace rato o porque está corriendo ahora mismo. Devuelve si el framework quedó inicializado. Si nadie ha llamado a init(), resuelve enseguida con false en vez de quedarse esperando un init que quizá no llegue.

if (!(await wu.whenReady())) {
console.warn('Nadie inicializó wu; no hay apps registradas.');
}

Rara vez hace falta llamarla a mano: wu.mount() ya espera sola a un init() en vuelo antes de mirar el registro. Eso es lo que evita el fallo clásico en shells de React, donde los efectos de los hijos corren antes que los del padre y el montaje se adelantaba al init:

App promo not registered. Call wu.init() first.

Dos init() concurrentes tampoco duplican trabajo: el segundo recibe la promesa del primero.

define(appName: string, lifecycle: {
mount: (container: HTMLElement) => void | Promise<void>;
unmount?: (container: HTMLElement) => void | Promise<void>;
activate?: (container: HTMLElement) => void | Promise<void>;
deactivate?: (container: HTMLElement) => void | Promise<void>;
update?: (container: HTMLElement, props: object) => void | Promise<void>;
hydrate?: (container: HTMLElement, ctx: { appName: string, props: object }) => void | Promise<void>;
}): void

Lo llama la micro-app, no el shell. Le entrega al framework las funciones que renderizan y desmontan la app. Este es el único contrato que una app debe cumplir para ser una app de Wu.

Slot Cuándo se llama Obligatorio
mount La app debe renderizarse dentro de container.
unmount La app se está destruyendo. Libera timers, listeners y raíces del framework. No, pero sin él tienes fugas
activate Una app keep-alive vuelve a mostrarse. No
deactivate Una app keep-alive se está ocultando. No
update wu.update() empuja props nuevas sin remontar. No
hydrate El contenedor ya contiene marcado renderizado en el servidor. Sustituye a mount. No
let root;
wu.define('cart', {
mount(container) {
root = createRoot(container);
root.render(<Cart />);
},
unmount() {
root?.unmount();
root = null;
},
update(container, props) {
root?.render(<Cart {...props} />);
},
});

En la práctica rara vez escribes esto a mano: los adaptadores del framework (wuReact.register('cart', App) y compañía) lo generan, incluyendo los slots update e hydrate cuando el framework puede soportarlos.

Casos límite. define() lanza [Wu] Mount function required for app: <name> si falta lifecycle.mount; todos los demás slots son opcionales. Es síncrono y devuelve undefined. También despacha un CustomEvent del DOM llamado wu:app:ready sobre window con detail: { appName, timestamp }, un hook cómodo para código que no tiene ninguna referencia a wu. Llamar a define() dos veces con el mismo nombre sobrescribe la definición anterior sin avisar.

Cuando el contenedor fue producido por wu-framework/server, el framework llama a hydrate en lugar de mount, pasándole las props con las que renderizó el servidor:

wu.define('product', {
mount(container) {
render(<Product {...fetchProps()} />, container);
},
hydrate(container, { appName, props }) {
hydrateRoot(container, <Product {...props} />);
},
});

Las props vienen de window.__WU_SSR_STATE__[appName], publicado por el renderStateScript() del servidor. Si ese global no existe, el slot se ejecuta igualmente con props como {}.

Una app con marcado del servidor pero sin slot hydrate no es un error: el framework registra una línea de depuración y llama a mount sobre el DOM existente, de modo que la salida del servidor actúa como un placeholder instantáneo que luego se repinta. Pierdes la propiedad de “sin parpadeo”, no la corrección.

mount(appName: string, containerSelector: string): Promise<void>

Carga la app si hace falta y ejecuta su slot mount (o hydrate) dentro de un Shadow DOM creado bajo containerSelector.

await wu.mount('cart', '#cart-slot');

Lo que ocurre, en orden:

  1. Comprobación de RBAC. wu.can(appName) se evalúa antes que cualquier otra cosa.
  2. Conteo de referencias. Se incrementa el contador de montajes de la app.
  3. Se espera cualquier desmontaje diferido en curso (con un límite de 5 s).
  4. Si ya está montada en el mismo contenedor → no-op. Si el contenedor es otro → la instancia anterior se desmonta a la fuerza primero.
  5. Las llamadas concurrentes a mount() para la misma app y contenedor se deduplican y comparten una sola promesa.
  6. Si la app está en estado keep-alive para ese contenedor → se hace show().
  7. Si no: crear el sandbox, cargar el módulo, esperar a wu.define() y ejecutar el slot del ciclo de vida.

Casos límite — esta llamada lanza. A diferencia de update(), mount() se rechaza a gritos:

Condición Error
Principal no autorizado Error con err.code === 'WU_ACCESS_DENIED', más err.appName y err.required
App nunca registrada App <name> not registered. Call wu.init() first.
El selector no coincide con nada Container not found: <selector>
El módulo cargó pero nunca llamó a define() [<mode>] App '<name>' loaded but wu.define() was not called within 10000ms
Falló el import del iframe strict y strictFallback está desactivado [strict] iframe import failed for '<name>' and strictFallback is disabled.

Un hook beforeLoad o beforeMount que cancela hace que mount() resuelva sin montar — sin lanzar, así que revisa el DOM si te importa.

Los fallos de montaje se reintentan hasta 3 intentos en total, con un backoff lineal de 1000 × (attempt + 1) ms, pero solo cuando el error boundary pide un reintento (action: 'retry' o 'retry-with-longer-timeout'). Entre intentos se limpia el contenedor pero se conserva el registro de wu.define(): un módulo ES cacheado no se reejecuta al reimportarlo, así que descartarlo dejaría al reintento incapaz de recuperarse para siempre.

El conteo de referencias existe por React StrictMode y Suspense, que montan, desmontan y remontan el mismo árbol en rápida sucesión. Cada mount() incrementa, cada unmount() decrementa, y el desmontaje real solo ocurre cuando el contador llega a cero y se dispara un temporizador de gracia de 60 ms sin que haya un remontaje. Por eso un unmount() seguido inmediatamente de un mount() no cuesta nada.

update(appName: string, props?: object): Promise<boolean>

Empuja props nuevas a una app ya montada sin remontarla, llamando al slot opcional update(container, props) de la app.

const ok = await wu.update('cart', { currency: 'EUR', items: 3 });
if (!ok) {
// Este adaptador no soporta props vivas: remonta, o usa el store.
}

Casos límite — esta llamada nunca lanza. Resuelve false y registra un aviso en todos los modos de fallo, y true solo cuando la app realmente recibió las props:

Situación Resultado
La app no está montada (ni oculta) false
La app está a mitad de desmontaje false
El adaptador no expone slot update false
Un hook beforeUpdate canceló false
El update() de la app lanzó false, enrutado al error boundary
Props entregadas true

Las props se fusionan sobre el registro de montaje ({ ...previous, ...props }) para poder inspeccionarlas, y se emite app:updated en el bus de eventos. Las apps ocultas (keep-alive) se pueden actualizar: el slot se ejecuta aunque no haya nada visible.

La limitación honesta: que las props vivas funcionen o no depende del adaptador, no de Wu. Los frameworks capaces de re-renderizar en el sitio exponen el slot; el resto no. Consulta wu.inspect().apps[].liveProps para ver cuál es cuál.

unmount(appName: string, options?: { keepAlive?: boolean; force?: boolean }): Promise<void>

Desmonta la app, u oculta si keep-alive está en juego.

Opción Por defecto Efecto
keepAlive config de la app, luego false Ocultar en vez de destruir: se preservan el DOM, el estado JS, los timers y el iframe.
force false Destruir de inmediato: salta el conteo de referencias, el temporizador de gracia y keep-alive.
await wu.unmount('cart'); // con conteo de referencias, 60 ms de gracia
await wu.unmount('cart', { keepAlive: true }); // ocultar, preservar estado
await wu.unmount('cart', { force: true }); // destruir ya, sin condiciones

keepAlive se resuelve por orden de prioridad: la opción de la llamada, luego la configuración registrada de la app, luego false.

Casos límite. Si la app no está montada, unmount() registra App <name> not mounted y devuelve — no lanza. La única excepción es { force: true } sobre una app oculta, que sí la destruye. Por el temporizador de gracia, un unmount() normal habitualmente devuelve antes de que la app haya desaparecido de verdad; si necesitas que el desmontaje esté completo, usa { force: true }. Un hook beforeUnmount que cancela deja la app montada.

En un desmontaje real el framework ejecuta el slot unmount de la app, destruye el sandbox de iframe si lo hay, limpia el puente de estilos y los observers, revoca los contratos de capacidad de la app y emite app:unmounted.

hide(appName: string): Promise<void>
show(appName: string): Promise<void>
isHidden(appName: string): boolean

La pareja keep-alive. hide() pone el contenedor anfitrión en display: none y mueve el registro de montaje al registro de ocultas; todo lo que hay dentro del Shadow DOM —nodos del DOM, estado de componentes, timers en marcha, el iframe— sigue vivo. show() lo revierte.

await wu.hide('editor'); // el usuario cambió de pestaña
// ...más tarde
await wu.show('editor'); // instantáneo, sin recarga, con el scroll intacto
wu.isHidden('editor'); // false

El deactivate(container) opcional de la app se ejecuta al ocultar y activate(container) al mostrar. Ambos se esperan y ambos están envueltos: si el tuyo lanza, el framework registra un aviso y sigue ocultando o mostrando igualmente.

Casos límite. hide() sobre una app que no está montada registra Cannot hide <name>: not mounted y devuelve. show() sobre una app que no está en estado keep-alive registra Cannot show <name>: not in keep-alive state y devuelve. Ninguna lanza. Los hooks beforeUnmount/afterUnmount se disparan al ocultar y beforeMount/afterMount al mostrar, cada uno con keepAlive: true en el contexto — los plugins que asumen que esas fases significan un desmontaje real necesitan comprobar esa bandera.

El coste es honesto: una app oculta sigue reteniendo su memoria, sus timers siguen corriendo y sus llamadas de red se siguen disparando. Keep-alive cambia RAM por cambios de pestaña instantáneos.

destroy(): Promise<void>

Desmonta el framework entero: cierra el inspector, detiene el grabador de la timeline, fuerza la destrucción de todas las apps ocultas y montadas, y vacía la caché, el bus de eventos, las métricas de rendimiento, los plugins, las estrategias, los hooks, el prefetcher, los contratos, todos los registros y el store. isInitialized vuelve a false, así que un wu.init() nuevo después funciona.

// p. ej. en el teardown de un test, o al salir de una ruta con Wu en un shell SPA
afterEach(async () => {
await wu.destroy();
});

Casos límite. Los esperadores pendientes de wu.define() se rechazan con Framework destroyed, de modo que un mount() en curso se rechaza en vez de quedarse colgado. destroy() relanza el error si algún paso falla. Ten en cuenta que el objeto singleton sobrevive: destroy() reinicia el estado, no invalida tu referencia a wu ni elimina window.wu.

Cada método de arriba tiene un export independiente, para bases de código que prefieren funciones a un objeto namespace:

import { init, mount, unmount, define, destroy, hide, show, isHidden } from 'wu-framework';

Una asimetría que conviene conocer: el export de conveniencia init recibe el array de apps directamente, no el objeto de configuración.

init([{ name: 'cart', url: '...' }]); // === wu.init({ apps: [...] })
wu.init({ apps: [...], sandbox: 'strict' }); // la forma completa

Si necesitas sandbox, strictFallback u overrides, usa wu.init().