# 服务器与 SSR 实战方案

在 Node 中使用 WU 渲染 HTML、React 和 Vue；检查转义、状态、CSP、错误和真实 HTTP 响应。

<span id="de-un-renderer-a-una-respuesta-http"></span>

<span class="section-anchor" id="section-1"></span>

## 从 renderer 到 HTTP 响应

WU 服务器接收具有 `render(props)` 的模块，返回微应用标记。HTTP 宿主负责加载数据、选择可信模块和构建响应。使用 `wu-framework/server` 无需初始化浏览器运行时。

项目在 `examples/ssr/render-page.mjs` 中提供完整方案。它组合 HTML、React 和 Vue 三种真实 renderer，生成含 Declarative Shadow DOM、水合状态和 CSP 响应头的文档。

你可以[下载可运行方案](/examples/wu-server-recipes.zip)，解压 ZIP 后执行 `npm install`。其中包含代码、测试和独立 manifest，可在门户之外复现。

```sh
node examples/ssr/render-page.mjs Ada
node examples/ssr/server.mjs
```

第一条命令将 HTML 写入 stdout。第二条在 `http://127.0.0.1:4310/?name=Ada` 提供服务。命令从已安装依赖的项目根目录运行。服务器接受 `PORT` 环境变量，按 `Ctrl+C` 关闭。

<span id="un-renderer-html-pequeño"></span>

<span class="section-anchor" id="section-2"></span>

## 一个小型 HTML renderer

```js
import { escapeHtml, renderApp } from 'wu-framework/server';

const greeting = {
  render({ name }) {
    return {
      html: `<p>Hola, ${escapeHtml(name)}.</p>`,
      styles: 'p { color: #b7d900; }',
      state: { name },
    };
  },
};

const result = await renderApp('greeting', {
  module: greeting,
  props: { name: 'Ada' },
});
```

完整容器位于 `result.html` 中，结构如下：

```html
<div id="wu-app-greeting" data-wu-app="greeting" data-wu-ssr="open">
  <template shadowrootmode="open">
    <style>p { color: #b7d900; }</style>
    <div data-wu-root><p>Hola, Ada.</p></div>
  </template>
</div>
```

`result.inner` 包含容器内部内容；`result.containerAttrs` 让 Astro 或其他引擎构建外层元素。`state` 仅包含 renderer 显式返回的内容。自定义 HTML renderers 决定发布哪些数据；React 和 Vue helpers 将 props 作为状态返回。

<span id="react-y-vue-sin-compilar-jsx-ni-sfc"></span>

<span class="section-anchor" id="section-3"></span>

## 无需编译 JSX 或 SFC 的 React 与 Vue

此方案使用 `React.createElement` 和 `h`，因此 Node 可以直接执行。在组件项目中，可以导入对应的服务器构建结果。

```js
import * as React from 'react';
import { renderToString as reactToString } from 'react-dom/server';
import { createSSRApp, h } from 'vue';
import { renderToString as vueToString } from '@vue/server-renderer';
import { renderReact, renderVue, renderApps } from 'wu-framework/server';

const reactApp = renderReact(
  ({ name }) => React.createElement('p', null, `Hola, ${name}`),
  { react: React, renderToString: reactToString },
);
const vueApp = renderVue(
  { props: ['name'], setup: props => () => h('p', `Hola, ${props.name}`) },
  { createSSRApp, renderToString: vueToString },
);

const result = await renderApps([
  { name: 'react-greeting', module: reactApp, props: { name: 'Ada' } },
  { name: 'vue-greeting', module: vueApp, props: { name: 'Ada' } },
]);
```

renderer 注入保持使用与宿主相同的 React 或 Vue 实例。helpers 的公共导入路径是 `wu-framework/server` 和 `wu-framework/server/adapters`；包的 exports 映射中不存在 `wu-framework/server/react` 或 `wu-framework/server/vue` subpaths。

<span id="estado-y-csp-por-respuesta"></span>

<span class="section-anchor" id="section-4"></span>

## 每个响应的状态与 CSP

```js
import { createNonce, renderApps, renderStateScript, securityHeaders } from 'wu-framework/server';

const nonce = createNonce();
const rendered = await renderApps(apps, { nonce, failFast: true });
const headers = securityHeaders({ nonce });
const body = rendered.html + renderStateScript(rendered.state, { nonce });
```

在同一响应的样式、状态脚本和 CSP 响应头中使用同一个 nonce；为下一个响应生成新的 nonce。`renderStateScript` 默认发布 `window.__WU_SSR_STATE__`。各集成分别输出脚本时，其 `merge: true` 选项可保留其他应用的条目。

本地 HTTP 方案使用 `securityHeaders({ nonce, https: false, hsts: false })`，因为它通过 HTTP 监听 loopback。HTTPS 服务器应配置对应传输策略。该 helper 不会启动 TLS 或管理证书。

不要在状态中发布秘密：响应 HTML 和水合 props 对客户端可见。

<span id="escapar-no-es-sanitizar-un-documento"></span>

<span class="section-anchor" id="section-5"></span>

## 转义不等于清洗文档

`escapeHtml` 转换文本值中的特殊字符。`serializeState` 保持 JSON 有效，并处理 `<`、`>`、U+2028 和 U+2029，防止数据提前关闭状态脚本。测试将 `</script><img ...>` 作为名称传入，并检查不会出现可执行标签。

`render()` 返回的 HTML 会原样插入。如果收到外部 HTML，在交给 renderer 前应按产品适用的策略清洗；`renderApp` 不会移除其中危险的标签或属性。renderer 的 CSS 也属于可信代码。

状态必须可序列化为 JSON。循环引用和 `BigInt` 会使 `serializeState` 失败；函数和 `undefined` 属性不能构成可移植的水合状态。

<span id="fallos-parciales-y-failfast"></span>

<span class="section-anchor" id="section-6"></span>

## 部分失败与 failFast

```js
import { renderApps } from 'wu-framework/server';

const result = await renderApps([
  { name: 'healthy', module: { render: () => '<p>Disponible</p>' } },
  { name: 'optional', module: { render() { throw new Error('Sin datos'); } } },
]);

console.log(result.errors[0].app); // optional
console.log(result.byApp.optional);
// <div id="wu-app-optional" data-wu-app="optional"></div>
```

`renderApps` 即使并行渲染也会保留输入顺序。不使用 `failFast` 时，它返回正常应用的 HTML、失败应用的空容器，以及 `errors` 列表。宿主决定返回 200 并显示降级结果，还是更改 HTTP 状态。

使用 `failFast: true` 时，Promise 拒绝。但这不会自动取消其他已经开始的 renderers；应避免在 `render` 内执行带外部副作用的工作。

| 故障 | 结果 |
| --- | --- |
| 名称无效 | 渲染前拒绝；最多 50 个字符，以字母或数字开头，其余允许字母、数字、`_` 或 `-` |
| 缺少 `module` | 提示需要 renderer 的错误 |
| 模块没有 `render` | 解析错误 |
| `render` 抛出异常 | 包含 `code: WU_SSR_RENDER_FAILED`、`appName` 和 `cause` 的错误 |
| `shadowMode` 不是 `open`/`closed` | 选项错误 |
| Nonce 或 `varName` 无效 | 输出对应属性或脚本前报错 |
| `render` 返回不支持的结果 | 规范化错误；不一定具有 renderer 失败错误码 |

<span id="qué-falta-para-hidratar"></span>

<span class="section-anchor" id="section-7"></span>

## 完成水合还需要什么

该方案无需下载应用 JavaScript 即可生成真实 HTML。要实现交互，在客户端注册相同应用，并将其挂载到 SSR 容器。具有 `hydrate` 的生命周期接管已有 DOM；否则 WU 使用 `mount`。React 和 Vue adapters 必须收到与服务器相同的树和 props。

`shadowMode: 'closed'` 适用于静态标记：常规水合无法访问该根节点。`declarative: false` 生成带 `data-wu-ssr="flat"` 的平面标记，适用于不需要声明式封装的场景。

参阅 [SSR 与水合](/zh-cn/docs/framework/ssr/)以及[生命周期](/zh-cn/docs/framework/lifecycle/)，连接服务器与客户端。WASM/WASI 模块可通过宿主 runner 为 renderer 提供可移植逻辑；它本身不会替代 HTTP 服务器、数据解析或响应策略。[通过 WU 连接服务](/zh-cn/docs/guides/browser-backend/)指南演示浏览器中应用间的请求、契约与取消。

<span id="ejecutar-las-comprobaciones"></span>

<span class="section-anchor" id="section-8"></span>

## 执行检查

```sh
node scripts/verify-server-examples.mjs
```

测试套件检查三种 renderers、nonce 与 CSP 一致性、状态、转义、无效选项、故障错误码和上下文、部分降级以及平面输出。测试使用已安装包的 exports，不复制其函数实现。