WU / 文档通用微前端
中文

服务器与 SSR 实战方案

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

从 renderer 到 HTTP 响应

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

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

你可以下载可运行方案,解压 ZIP 后执行 npm install。其中包含代码、测试和独立 manifest,可在门户之外复现。

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 关闭。

一个小型 HTML renderer

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 中,结构如下:

<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 作为状态返回。

无需编译 JSX 或 SFC 的 React 与 Vue

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

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。

每个响应的状态与 CSP

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 对客户端可见。

转义不等于清洗文档

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

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

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

部分失败与 failFast

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 失败错误码

完成水合还需要什么

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

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

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

执行检查

node scripts/verify-server-examples.mjs

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

一起完善 WU

发现了问题?有新的想法?

你的使用体验能让 WU 变得更好。告诉我们遇到的问题,或支持项目的持续开发。