Pestañas con keep-alive
Por defecto, wu.unmount() destruye una app: se ejecuta el unmount del ciclo
de vida, el iframe se destruye, el shadow root se vacía. Volver a montar es
empezar de cero.
Keep-alive convierte eso en un ocultar. El DOM, el estado de JS, los timers
y el iframe sobreviven; solo el display del contenedor anfitrión pasa a
none.
unmount() unmount({ keepAlive: true }) ├─ lifecycle.unmount() ├─ lifecycle.deactivate() ├─ iframe.destroy() ├─ hostContainer.display = 'none' ├─ shadowRoot.innerHTML = '' └─ pasa: mounted → hidden └─ eliminada del registro ~coste de remontaje completo restauración instantáneaCómo activarlo
Sección titulada «Cómo activarlo»Por app, en el init:
await wu.init({ apps: [ { name: 'dashboard', url: '…', keepAlive: true }, { name: 'reports', url: '…', keepAlive: true }, ],});O por llamada a unmount:
await wu.unmount('dashboard', { keepAlive: true });Orden de resolución: gana la opción de la llamada, luego la configuración de la
app, y por último false.
Un selector de pestañas
Sección titulada «Un selector de pestañas»const tabs = ['dashboard', 'reports', 'settings'];let active = null;
async function activate(name) { if (active === name) return; if (active) await wu.unmount(active, { keepAlive: true }); await wu.mount(name, '#tab-outlet'); active = name;}wu.mount() sobre una app oculta se resuelve como wu.show(): el contenedor
se vuelve visible y se llama a activate(). Sin recarga, sin re-render, sin
perder la posición de scroll.
También puedes controlarlo de forma explícita:
await wu.hide('dashboard'); // deactivate + display:noneawait wu.show('dashboard'); // display:'' + activatewu.isHidden('dashboard'); // booleanoOcultar no pausa nada
Sección titulada «Ocultar no pausa nada»Esta es la parte que la gente se equivoca. Una app oculta está totalmente viva:
setIntervalsigue disparándose- el polling sigue golpeando tu API
- los WebSockets siguen abiertos
- las animaciones siguen corriendo (aunque el navegador puede limitar el trabajo fuera de pantalla)
- las suscripciones al bus de eventos y al store siguen recibiendo
Wu no va a silenciar nada de esto por ti. Para eso está deactivate:
wu.define('dashboard', { mount(container) { this.timer = setInterval(() => this.refresh(), 5000); this.socket = new WebSocket('wss://…'); },
deactivate(container) { clearInterval(this.timer); this.timer = null; this.socket?.close(); // o simplemente deja de reaccionar a los mensajes },
activate(container) { this.refresh(); // ponte al día con lo que te perdiste this.timer = setInterval(() => this.refresh(), 5000); this.socket = new WebSocket('wss://…'); },
unmount(container) { clearInterval(this.timer); this.socket?.close(); this.root?.unmount(); },});Los errores lanzados desde activate o deactivate se capturan y se registran:
nunca rompen la transición, así que una pausa fallida no deja a la app varada a
medio ocultar. Eso también significa que un fallo silencioso es posible; registra
dentro de tus propios handlers si te importa.
El conteo de referencias también aplica aquí
Sección titulada «El conteo de referencias también aplica aquí»Keep-alive participa del mismo conteo de referencias que un unmount normal: cada
unmount({ keepAlive: true }) libera una referencia, y solo la última oculta
realmente la app. Sin eso, el doble efecto de React StrictMode filtraría una
referencia en cada ciclo de ocultar/mostrar y la app nunca podría destruirse.
Si ves hide skipped: N ref(s) still active, algo montó la app más veces de las
que la desmontó.
Destruir de verdad una app con keep-alive
Sección titulada «Destruir de verdad una app con keep-alive»unmount() sobre una app con keep-alive la oculta. Para desmontarla de verdad:
await wu.unmount('dashboard', { force: true });force saltea keep-alive, saltea el conteo de referencias y saltea el timer de
gracia. Funciona tanto si la app está montada como si está oculta. Úsalo cuando
el usuario cierra sesión, cuando se abandona definitivamente una sección de
rutas, o en el teardown de los tests.
Lo que cuesta
Sección titulada «Lo que cuesta»Keep-alive cambia memoria por latencia, y la memoria no es poca:
| Se mantiene vivo | Coste |
|---|---|
| Shadow root + árbol DOM completo | proporcional a la UI de la app |
| Instancia del framework y estado de componentes | árbol de fibers de React, grafo reactivo de Vue, … |
| Hojas de estilo inyectadas | la parte del styleMode de la app, por app |
El iframe, en modo strict/eval |
un realm entero cada uno |
| Timers, sockets, suscripciones | salvo que los detengas en deactivate |
Diez apps con keep-alive en modo strict significan diez iframes vivos. Eso está
bien para una consola de administración de cinco pestañas y es mala idea para un
árbol de navegación de cincuenta elementos.
Una política razonable: keep-alive para los dos o tres destinos entre los que los usuarios rebotan, y destruye todo lo demás. O keep-alive por orden de uso más reciente y destruye por la fuerza la cola:
const MAX_ALIVE = 3;const recent = [];
async function activate(name) { if (active) await wu.unmount(active, { keepAlive: true }); await wu.mount(name, '#outlet'); active = name;
recent.splice(recent.indexOf(name) >>> 0, recent.includes(name) ? 1 : 0); recent.unshift(name); for (const stale of recent.splice(MAX_ALIVE)) { await wu.unmount(stale, { force: true }); }}Hooks y eventos
Sección titulada «Hooks y eventos»Las transiciones de keep-alive ejecutan las mismas fases de hooks que un
mount/unmount real, con keepAlive: true en el contexto, para que los plugins
puedan distinguirlas:
wu.hooks.use('beforeUnmount', async (ctx, next) => { if (ctx.keepAlive) console.log(`${ctx.appName} se está ocultando, no destruyendo`); await next();});Eventos: app:hidden y app:shown (este último lleva showTime).
Consultar el estado
Sección titulada «Consultar el estado»wu.isHidden('dashboard'); // true mientras esté viva y ocultawu.inspect().summary; // { registered, defined, mounted, hidden }getSandboxInfo() sigue devolviendo información para una app oculta, con
mounted: false: la app existe, simplemente no está en pantalla.
