SSR e hidratación
Contrato render del servidor, Declarative Shadow DOM, estado de hidratación y helpers React/Vue.
Consulta las versiones, cambios y requisitos en Framework 4.0 y CLI 1.2.
Qué produce el servidor
wu-framework/server transforma el resultado de un renderer en el HTML de un contenedor WU. Por defecto encapsula contenido y estilos en Declarative Shadow DOM. El navegador puede materializar ese árbol antes de que cargue el JavaScript de la app.
El servidor recibe un módulo con render(props), no el lifecycle de montaje del cliente. El resultado puede ser un string HTML o { html, styles?, state? }. Quien genera el HTML debe escapar los datos no confiables antes de incorporarlos.
import { renderApp, escapeHtml, renderStateScript } from 'wu-framework/server';
const result = await renderApp('greeting', {
props: { name: 'Ada' },
module: {
render(props) {
return {
html: `<p>Hola, ${escapeHtml(props.name)}</p>`,
styles: 'p { color: #f97316; }',
state: props,
};
},
},
});
const html = result.html + renderStateScript({ greeting: result.state });
renderApp también acepta un módulo ya importado, una promesa, un módulo default o una función que lo carga. El host decide qué módulos puede ejecutar en el servidor; no conviertas un input de usuario en un import arbitrario.
Resultado y opciones
| Elemento | Contrato |
|---|---|
result.html |
Contenedor completo |
result.inner |
Interior declarativo o plano |
result.containerAttrs |
ID, data-wu-app, data-wu-ssr, clase opcional |
result.state |
Estado opcional devuelto por renderer |
containerId |
Por defecto wu-app-<name> |
shadowMode |
open por defecto; closed impide hidratación normal |
declarative |
true por defecto |
styles |
CSS extra del host para esa app |
nonce |
Nonce CSP para estilos de esa respuesta |
renderApps procesa varias apps en paralelo. Por defecto registra fallos individuales en errors y conserva las otras; failFast: true permite abortar. Un renderer que falla en renderApp produce un error con código WU_SSR_RENDER_FAILED.
Hidratar sin borrar
En el cliente, el shell registra la app normalmente y monta en su contenedor SSR. El sandbox detecta data-wu-ssr y la raíz data-wu-root. Si existe lifecycle.hydrate, el core lo invoca con las props de servidor y la facade scoped. Sin ese slot llama mount y el HTML previo funciona como contenido inicial hasta que el renderer lo reemplace.
wu.define('greeting', {
mount(container) {
container.textContent = 'Montaje cliente';
},
hydrate(container, { props }) {
// Adoptar lo que ya existe. No limpiar container primero.
const paragraph = container.querySelector('p');
paragraph?.setAttribute('aria-label', `Saludo para ${props.name}`);
},
unmount(container) {
container.replaceChildren();
},
});
Para React, hydrateRoot debe recibir el mismo árbol/props que produjo el servidor. Para Vue, el adapter usa createSSRApp y .mount para adoptar el DOM existente. El estado publicado debe conservar esa correspondencia; cambiar datos entre render e hidratación causa mismatches.
Helpers de renderer
El subpath server exporta renderReact, renderPreact, renderVue, renderSvelte, renderSolid y renderHtml. Los helpers devuelven un módulo conforme al contrato render.
import * as React from 'react';
import { renderToString } from 'react-dom/server';
import { renderReact } from 'wu-framework/server';
import App from './App.jsx';
export default renderReact(App, {
react: React,
renderToString,
styles: '.greeting { font-weight: 600; }',
});
Inyectar renderer y runtime evita resolver otra copia de React fuera del bundle del servidor. Vue admite inyección de createSSRApp y renderToString, además de setup. Los nombres de opciones server no son idénticos a runtime: { React, ReactDOM } del adapter cliente.
Astro
---
import WuAppSSR from 'wu-framework/integrations/astro/WuAppSSR.astro';
import greetingServer from '../apps/greeting/server.js';
---
<WuAppSSR
name="greeting"
module={greetingServer}
props={{ name: 'Ada' }}
fallbackToClient={false}
/>
El shell aún debe inicializar el registro cliente correspondiente. El componente publica estado como JSON para hidratar; hydrate es true por defecto. fallbackToClient también es true: un fallo SSR deja el slot para un montaje cliente, salvo que lo desactives como en el ejemplo.
En una build estática de Astro el renderer corre al compilar; no convierte esa página en contenido personalizado por petición. Para datos dependientes del usuario necesitas una estrategia de servidor o carga cliente adecuada.
Estado y cabeceras
serializeState y renderStateScript resuelven la serialización para el contexto HTML/script. escapeHtml escapa texto, no valida una aplicación HTML arbitraria. No inyectes secretos en estado de hidratación: todo lo publicado en esa página es visible para su lector.
createNonce() produce un nonce para una respuesta. securityHeaders, applySecurityHeaders, wuSecurityMiddleware y withSecurityHeaders ayudan a instalar CSP y otras cabeceras. Usa el mismo nonce de respuesta en estilos/scripts que deban ejecutarse y un nonce nuevo en la siguiente respuesta; el servidor controla esa coordinación.
Referencia de implementación
Exports: src/server/index.js; render: src/server/wu-server-render.js; varios renders: src/server/wu-server-render.js; tipos: src/server/index.d.ts; adopción de DOM: src/core/wu-sandbox.js; slot hydrate: src/core/wu-core.js; Astro: integrations/astro/WuAppSSR.astro. Pruebas: tests/unit/server-render.test.js, tests/unit/server-security-fuzz.test.js, tests/unit/security-headers.test.js.
Para aislar cada petición y limitar trabajo, consulta createServerRequest, loaders privados, cancelación y concurrencia en la referencia SSR. Los wrappers de cabeceras 4.0 rechazan un nonce fijo: usa una función por petición o la generación predeterminada.