使用 Rsbuild 和 React 的 SSR

在 Nitro 中使用 Rsbuild 和 React 进行服务端渲染。

routes
api
hello.ts
src
app.tsx
entry-client.tsx
entry-server.tsx
env.d.ts
styles.css
package.json
rsbuild.config.ts
tsconfig.json
import { renderToReadableStream } from "react-dom/server.edge";
import { App } from "./app.tsx";

import clientAssets from "./entry-client.tsx?assets=client";

export default {
  async fetch(_req: Request) {
    return new Response(
      await renderToReadableStream(
        <html lang="en">
          <head>
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            {clientAssets.css.map((attr) => (
              <link key={attr.href} rel="stylesheet" {...attr} />
            ))}
            {clientAssets.js.map((attr) => (
              <script key={attr.href} defer src={attr.href} />
            ))}
          </head>
          <body id="app">
            <App />
          </body>
        </html>
      ),
      { headers: { "Content-Type": "text/html;charset=utf-8" } }
    );
  },
};

使用 React、Rsbuild 和实验性的 nitro/rsbuild 插件设置服务端渲染(SSR)。此配置支持流式 HTML 响应、通过 ?assets 导入注入客户端资源,以及客户端水合。

#概述

在 Rsbuild 配置中添加 Nitro 和 React 插件。
配置客户端入口。
创建将应用渲染为 HTML 的服务器入口。
创建对服务器渲染 HTML 进行水合的客户端入口。
添加 API 路由以提供服务器端数据。

#1. 配置 Rsbuild

在 Rsbuild 配置中添加 Nitro 和 React 插件,并设置客户端入口:

rsbuild.config.ts
import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { nitro } from "nitro/rsbuild";

export default defineConfig({
  source: {
    entry: { index: "./src/entry-client.tsx" },
  },
  // Pages are rendered by the server entry
  tools: { htmlPlugin: false },
  plugins: [pluginReact(), nitro({ serverDir: "./" })],
});

Nitro 会自动检测 app/、src/ 或项目根目录中名为 entry-server 的文件,并为其添加 ssr 环境。服务器入口会渲染整个文档,因此通过 tools.htmlPlugin: false 禁用 Rsbuild HTML 生成。

#2. 创建应用组件

创建一个供服务器和客户端共同运行的 React 组件:

src/app.tsx
import { useState } from "react";

export function App() {
  const [count, setCount] = useState(0);
  return (
    <>
      <h1 className="hero">Nitro + Rsbuild + React</h1>
      <button onClick={() => setCount((c) => c + 1)}>Count is {count}</button>
    </>
  );
}

#3. 创建服务器入口

服务器入口使用 react-dom/server.edge 将 React 应用渲染为流式 HTML 响应:

src/entry-server.tsx
import { renderToReadableStream } from "react-dom/server.edge";
import { App } from "./app.tsx";

import clientAssets from "./entry-client.tsx?assets=client";

export default {
  async fetch(_req: Request) {
    return new Response(
      await renderToReadableStream(
        <html lang="en">
          <head>
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            {clientAssets.css.map((attr) => (
              <link key={attr.href} rel="stylesheet" {...attr} />
            ))}
            {clientAssets.js.map((attr) => (
              <script key={attr.href} defer src={attr.href} />
            ))}
          </head>
          <body id="app">
            <App />
          </body>
        </html>
      ),
      { headers: { "Content-Type": "text/html;charset=utf-8" } }
    );
  },
};

?assets=client 导入会提供客户端入口(src/entry-client.tsx)的脚本和样式表,在开发和生产环境中均可使用。脚本为经典脚本(最后一项是 entry),并通过 defer 加载。使用 renderToReadableStream 可在 React 渲染时流式传输 HTML,从而缩短首字节时间。

#4. 创建客户端入口

客户端入口会导入样式,并对服务器渲染的 HTML 进行水合,以附加 React 事件处理器:

src/entry-client.tsx
import "./styles.css";
import { hydrateRoot } from "react-dom/client";
import { App } from "./app.tsx";

hydrateRoot(document.querySelector("#app")!, <App />);

hydrateRoot 函数会将 React 附加到现有的服务器渲染 DOM,而不会重新渲染。在开发模式下,@rsbuild/plugin-react 会为客户端代码启用 React Fast Refresh。

#5. 添加 API 路由

服务器路由(例如 routes/api/hello.ts)与页面一起由 Nitro 提供:

routes/api/hello.ts
import { defineHandler } from "nitro";

export default defineHandler(() => ({ message: "Hello from Nitro!" }));

#了解更多