Ir al contenido

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ánea

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.

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:none
await wu.show('dashboard'); // display:'' + activate
wu.isHidden('dashboard'); // booleano

Esta es la parte que la gente se equivoca. Una app oculta está totalmente viva:

  • setInterval sigue 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ó.

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.

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 });
}
}

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

wu.isHidden('dashboard'); // true mientras esté viva y oculta
wu.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.