Ir al contenido

Renderizado en servidor

Wu renderiza micro-apps en el servidor y las hidrata en el navegador. El marcado se envía ya encapsulado en su Shadow DOM, estilos incluidos: la primera respuesta es contenido real, no un punto de montaje vacío.

Funciona con Node a secas: sin vm, sin --experimental-vm-modules, sin acoplarse al bundler. El módulo de servidor de cada app se carga con un import() normal.

import { renderApp } from 'wu-framework/server';
const cart = await renderApp('cart', {
module: () => import('./apps/cart/server.js'),
props: { userId: 42 },
});
res.end(template.replace('<!--cart-->', cart.html));

Esto es lo que produce, visible con curl y sin que intervenga JavaScript:

<div id="wu-app-cart" data-wu-app="cart" data-wu-ssr="open">
<template shadowrootmode="open">
<style>h1{color:#c0392b}</style>
<div data-wu-root><h1>Your cart</h1><span>3 items</span></div>
</template>
</div>

El navegador materializa ese <template shadowrootmode> en un shadow root real mientras parsea el HTML, antes de que se ejecute ningún script. Eso es Declarative Shadow DOM.

Navegador Soporte
Chrome / Edge 111+
Safari 16.4+
Firefox 123+

Para versiones anteriores, declarativeShadowDomPolyfill() emite un script que recorre los elementos <template shadowrootmode> que quedaron sueltos y adjunta los shadow roots a mano. No hace nada donde el soporte es nativo. Wu también hace esa misma materialización dentro de mount(), así que la hidratación funciona aunque te saltes el polyfill; el polyfill solo sirve para que el contenido sea visible antes de que cargue el runtime de wu.

Junto a su entrada de navegador, la app exporta un render. Los helpers lo construyen a partir de un componente:

apps/cart/server.js
import { renderReact } from 'wu-framework/server';
import App from './App.jsx';
import css from './cart.css?inline';
export default renderReact(App, { styles: css });

Un helper por framework — renderReact, renderVue, renderPreact, renderSvelte, renderSolid — más renderHtml para cualquier otra cosa. Cada helper importa de forma perezosa el renderizador de servidor de su propio framework, así que instalar wu no arrastra React, Vue ni nada más. Consulta Renderizar apps.

import { renderApps, renderStateScript } from 'wu-framework/server';
const { html, byApp, state, errors } = await renderApps([
{ name: 'header', module: () => import('./apps/header/server.js') },
{ name: 'cart', module: () => import('./apps/cart/server.js'), props: { userId } },
]);
for (const { app, error } of errors) log.warn(`SSR failed for ${app}`, error);
res.end(
template
.replace('<!--header-->', byApp.header)
.replace('<!--cart-->', byApp.cart)
.replace('<!--state-->', renderStateScript(state))
);

En el cliente no cambia nada:

await wu.init({ apps: [{ name: 'cart', url: '/apps/cart' }] });
await wu.mount('cart', '#wu-app-cart');

mount() ve el shadow root del servidor, se salta la creación de uno nuevo y llama al slot hydrate de la app con exactamente las props con las que renderizó el servidor. Consulta Hidratación.

Nada impide que una app sea React y la siguiente Vue: la propiedad que hace a wu multi-framework en el navegador se mantiene también en el servidor. El shell puede ser una app Next.js (React) mientras la micro-app que renderiza en servidor es Svelte; nunca comparten runtime, solo HTML.

Por qué esto es distinto de la federación a nivel de bundler

Sección titulada «Por qué esto es distinto de la federación a nivel de bundler»

Renderizar un micro-frontend en servidor suele implicar renunciar al aislamiento: el marcado aterriza en el documento global, compitiendo con el CSS de todos los demás.

Aquí pasa lo contrario. Como los estilos de la app viajan dentro del shadow root, el marcado renderizado en servidor está encapsulado desde el primer byte, sin inyección de estilos en cliente y sin destello de contenido sin estilo.

  • No hay streaming. renderApp resuelve a una cadena completa; no existe un equivalente de renderToPipeableStream. En páginas muy grandes, eso cuesta latencia.
  • No hay capa de obtención de datos. Lo que la app necesite para renderizar se lo pasas tú en props. Wu no lo busca por ti y no hay caché en servidor.
  • hydrate depende de cada adaptador. React (hydrateRoot) y Vue (createSSRApp) lo traen. Los demás se montan encima del marcado del servidor: correcto, pero implica un repintado.
  • Shadow DOM no es una frontera de seguridad. El SSR no cambia eso. Consulta Modelo de amenazas.
  • shadowMode: 'closed' bloquea la hidratación. Úsalo solo para marcado inerte.