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.
wu.init()
Sección titulada «wu.init()»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.
Sobre strictFallback
Sección titulada «Sobre strictFallback»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()
Sección titulada «wu.whenReady()»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.
wu.define()
Sección titulada «wu.define()»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>;}): voidLo 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. |
Sí |
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.
hydrate (v2.7)
Sección titulada «hydrate (v2.7)»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.
wu.mount()
Sección titulada «wu.mount()»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:
- Comprobación de RBAC.
wu.can(appName)se evalúa antes que cualquier otra cosa. - Conteo de referencias. Se incrementa el contador de montajes de la app.
- Se espera cualquier desmontaje diferido en curso (con un límite de 5 s).
- Si ya está montada en el mismo contenedor → no-op. Si el contenedor es otro → la instancia anterior se desmonta a la fuerza primero.
- Las llamadas concurrentes a
mount()para la misma app y contenedor se deduplican y comparten una sola promesa. - Si la app está en estado keep-alive para ese contenedor → se hace
show(). - 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.
Reintentos y el conteo de referencias
Sección titulada «Reintentos y el conteo de referencias»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.
wu.update()
Sección titulada «wu.update()»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) sí 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.
wu.unmount()
Sección titulada «wu.unmount()»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 graciaawait wu.unmount('cart', { keepAlive: true }); // ocultar, preservar estadoawait wu.unmount('cart', { force: true }); // destruir ya, sin condicioneskeepAlive 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.
wu.hide() y wu.show()
Sección titulada «wu.hide() y wu.show()»hide(appName: string): Promise<void>show(appName: string): Promise<void>isHidden(appName: string): booleanLa 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 tardeawait wu.show('editor'); // instantáneo, sin recarga, con el scroll intactowu.isHidden('editor'); // falseEl 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.
wu.destroy()
Sección titulada «wu.destroy()»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 SPAafterEach(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.
Exports con nombre
Sección titulada «Exports con nombre»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 completaSi necesitas sandbox, strictFallback u overrides, usa wu.init().
