Renderizar apps
El renderizado en servidor tiene dos mitades: cada app publica un módulo de servidor y el shell lo renderiza dentro de la página.
El módulo de servidor de la app
Sección titulada «El módulo de servidor de la app»El contrato es un objeto con un render(props) que devuelve una cadena o { html, styles?, state? }:
interface WuServerModule { render(props: Record<string, any>): | string | { html: string; styles?: string | string[]; state?: unknown } | Promise<string | { html: string; styles?: string | string[]; state?: unknown }>;}Rara vez lo escribes a mano. Los helpers lo construyen a partir de un componente:
import { renderReact } from 'wu-framework/server';import App from './App.jsx';import css from './cart.css?inline';
export default renderReact(App, { styles: css });Helpers
Sección titulada «Helpers»| Helper | Necesita instalado | Usa |
|---|---|---|
renderReact(Component, opts) |
react + react-dom |
renderToString |
renderPreact(Component, opts) |
preact + preact-render-to-string |
render |
renderVue(Component, opts) |
vue + @vue/server-renderer |
createSSRApp + renderToString |
renderSvelte(Component, opts) |
svelte (build de servidor) |
Component.render() (v4) o render() de svelte/server (v5) |
renderSolid(Component, opts) |
solid-js |
renderToString de solid-js/web |
renderHtml(fn, opts) |
nada | tu propia función |
Todos los helpers aceptan { styles }, que puede ser una cadena, un array de cadenas o una función de las props:
export default renderReact(App, { styles: (props) => props.theme === 'dark' ? darkCss : lightCss,});Extras:
| Helper | Opción extra | Significado |
|---|---|---|
renderReact |
strictMode (por defecto false) |
Envuelve el elemento en React.StrictMode |
renderVue |
setup(app, props) |
Se llama con la instancia de la app SSR antes del render: plugins, provides, i18n |
La vía de escape
Sección titulada «La vía de escape»renderHtml acepta cualquier función que produzca HTML: una plantilla literal, una llamada a @lit-labs/ssr de Lit, un renderizador de Markdown.
import { renderHtml } from 'wu-framework/server';
export default renderHtml( (props) => `<h1>Hello ${props.name}</h1>`, { styles: 'h1 { font-weight: 600 }' });Renderizar desde el shell
Sección titulada «Renderizar desde el shell»Una app
Sección titulada «Una app»import { renderApp } from 'wu-framework/server';
const cart = await renderApp('cart', { module: () => import('./apps/cart/server.js'), props: { userId: 42 },});
// cart.html → el <div>…</div> completo// cart.inner → solo el <template> (para integraciones que construyen el div ellas mismas)// cart.containerAttrs→ { id, 'data-wu-app', 'data-wu-ssr', class? }// cart.state → las props, para renderStateScriptmodule acepta el módulo ya cargado, una función que lo devuelva, una promesa o un namespace con un default. Así, () => import('./apps/cart/server.js') simplemente funciona y el módulo se queda fuera del grafo hasta que ocurre el render.
Varias apps
Sección titulada «Varias apps»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 } },]);Se renderizan en paralelo. Por defecto, una app que lanza un error no tumba la página: su entrada en byApp es un contenedor vacío <div id="wu-app-cart" data-wu-app="cart"></div> — fíjate en que falta data-wu-ssr, que es justo lo que le dice al cliente que la monte normalmente en vez de hidratarla — y el error aterriza en errors. Pasa { failFast: true } para relanzar el primero en su lugar.
for (const { app, error } of errors) { log.warn(`SSR failed for ${app}: ${error.message}`, error.cause);}Un renderApp que falla lanza un Error con code: 'WU_SSR_RENDER_FAILED', appName y el error original en cause, de modo que un shell puede elegir entre un 500 y degradar a un montaje en cliente.
Publicar las props
Sección titulada «Publicar las props»La hidratación necesita las props con las que renderizó el servidor. renderStateScript las emite:
res.end(template.replace('<!--state-->', renderStateScript(state)));// <script>window.__WU_SSR_STATE__={"cart":{"userId":42}}</script>Los valores se serializan con </script> y U+2028/U+2029 escapados, así que una cadena de tus props no puede escaparse del script.
Usa { merge: true } cuando varios componentes independientes emitan cada uno su propio script: cada uno se fusiona con el objeto global en vez de sobrescribirlo. Es lo que hacen las integraciones de Next.js y Astro.
Integraciones con frameworks
Sección titulada «Integraciones con frameworks»Next.js (App Router)
Sección titulada «Next.js (App Router)»WuAppSSR es un Server Component asíncrono. Llama a renderApp() durante el render de servidor, emite el contenedor con dangerouslySetInnerHTML (React no debe ser dueño de ese subárbol: wu le adjunta un shadow root) y añade un pequeño WuMount con 'use client' que hidrata después de la hidratación de React.
// app/page.jsx — un Server Component, sin 'use client'import { WuAppSSR } from '@wu-framework/next';
export default function Page() { return ( <main> <WuAppSSR name="cart" module={() => import('../apps/cart/server.js')} props={{ userId: 42 }} /> </main> );}WuAppSSR.astro acepta las mismas props y funciona tanto en salida SSR como en builds estáticos: en un build estático el marcado se hornea en tiempo de compilación, que es justo lo que quieres para páginas de contenido.
---import WuAppSSR from '@wu-framework/astro/WuAppSSR.astro';import cartServer from '../apps/cart/server.js';---<WuAppSSR name="cart" module={cartServer} props={{ userId: 42 }} />Props compartidas
Sección titulada «Props compartidas»| Prop | Tipo | Por defecto | Qué hace |
|---|---|---|---|
name |
string |
obligatorio | Nombre de la app; debe coincidir con el que registra el cliente |
appName |
string |
name |
Solo en Next: sobrescribe el nombre de la app |
module |
module | () => import() |
obligatorio | El módulo de servidor de la app |
props |
object |
{} |
Props del render; se publican para la hidratación |
styles |
string | string[] |
— | CSS adicional a encapsular |
className / class |
string |
— | Clases del contenedor |
style |
— | — | Estilo en línea del contenedor |
shadowMode |
'open' | 'closed' |
'open' |
Solo en Next; closed bloquea la hidratación |
hydrate |
boolean |
true |
Con false emite marcado inerte y nunca monta |
fallbackToClient |
boolean |
true |
Ante un error de render, deja el hueco y monta en cliente en vez de tumbar la página |
WuApp (el componente más antiguo) sigue siendo solo de cliente: renderiza un div vacío y monta después de la hidratación. Sigue usándolo para islas interactivas donde el primer pintado no importa.
