Vite RSC

使用 Vite 和 Nitro 的 React 服务器组件。
app/root.tsx
import "./index.css"; // css import is automatically injected in exported server components
import viteLogo from "./assets/vite.svg";
import { getServerCounter, updateServerCounter } from "./action.tsx";
import reactLogo from "./assets/react.svg";
import nitroLogo from "./assets/nitro.svg";
import { ClientCounter } from "./client.tsx";

export function Root(props: { url: URL }) {
  return (
    <html lang="en">
      <head>
        {/* eslint-disable-next-line unicorn/text-encoding-identifier-case */}
        <meta charSet="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/vite.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Nitro + Vite + RSC</title>
      </head>
      <body>
        <App {...props} />
      </body>
    </html>
  );
}

function App(props: { url: URL }) {
  return (
    <div id="root">
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev/reference/rsc/server-components" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>

        <a href="https://v3.nitro.build" target="_blank">
          <img src={nitroLogo} className="logo" alt="Nitro logo" />
        </a>
      </div>
      <h1>Vite + RSC + Nitro</h1>
      <div className="card">
        <ClientCounter />
      </div>
      <div className="card">
        <form action={updateServerCounter.bind(null, 1)}>
          <button>Server Counter: {getServerCounter()}</button>
        </form>
      </div>
      <div className="card">Request URL: {props.url?.href}</div>
      <ul className="read-the-docs">
        <li>
          Edit <code>src/client.tsx</code> to test client HMR.
        </li>
        <li>
          Edit <code>src/root.tsx</code> to test server HMR.
        </li>
        <li>
          Visit{" "}
          <a href="./_.rsc" target="_blank">
            <code>_.rsc</code>
          </a>{" "}
          to view RSC stream payload.
        </li>
        <li>
          Visit{" "}
          <a href="?__nojs" target="_blank">
            <code>?__nojs</code>
          </a>{" "}
          to test server action without js enabled.
        </li>
      </ul>
    </div>
  );
}

该示例演示了使用 Vite 的实验性 RSC 插件和 Nitro 的 React 服务器组件(RSC)。它包括服务器组件、客户端组件、服务器动作和流式 SSR。

概述

SSR 入口 处理传入请求并将 React 组件渲染为 HTML

根组件 作为服务器组件定义页面结构

客户端组件 使用 "use client" 指令实现交互部分

1. SSR 入口

app/framework/entry.ssr.tsx
import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
import React from "react";
import type { ReactFormState } from "react-dom/client";
import { renderToReadableStream } from "react-dom/server.edge";
import { injectRSCPayload } from "rsc-html-stream/server";
import type { RscPayload } from "./entry.rsc";

export default {
  fetch: async (request: Request) => {
    const rscEntryModule = await import.meta.viteRsc.loadModule<typeof import("./entry.rsc")>(
      "rsc",
      "index"
    );
    return rscEntryModule.default(request);
  },
};

export async function renderHTML(
  rscStream: ReadableStream<Uint8Array>,
  options: {
    formState?: ReactFormState;
    nonce?: string;
    debugNoJS?: boolean;
  }
): Promise<{ stream: ReadableStream<Uint8Array>; status?: number }> {
  // 将一个 RSC 流复制成两个流。
  // - 一个用于 SSR(下方的 ReactClient.createFromReadableStream)
  // - 另一个用于通过注入 <script>...FLIGHT_DATA...</script> 实现浏览器水合
  const [rscStream1, rscStream2] = rscStream.tee();

  // 将 RSC 流反序列化为 React 虚拟 DOM
  let payload: Promise<RscPayload> | undefined;
  function SsrRoot() {
    // 反序列化需要在 ReactDOMServer 上下文中启动,
    // 以便 ReactDOMServer 的预初始化/预加载生效
    payload ??= createFromReadableStream<RscPayload>(rscStream1);
    return React.use(payload).root;
  }

  // 渲染 HTML(传统 SSR)
  const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent("index");

  let htmlStream: ReadableStream<Uint8Array>;
  let status: number | undefined;

  try {
    htmlStream = await renderToReadableStream(<SsrRoot />, {
      bootstrapScriptContent: options?.debugNoJS ? undefined : bootstrapScriptContent,
      nonce: options?.nonce,
      formState: options?.formState,
    });
  } catch {
    // 回退渲染一个空壳,在浏览器纯 CSR 运行,
    // 该过程能重放服务器组件错误并触发错误边界。
    status = 500;
    htmlStream = await renderToReadableStream(
      <html>
        <body>
          <noscript>服务器内部错误:SSR 失败</noscript>
        </body>
      </html>,
      {
        bootstrapScriptContent:
          `self.__NO_HYDRATE=1;` + (options?.debugNoJS ? "" : bootstrapScriptContent),
        nonce: options?.nonce,
      }
    );
  }

  let responseStream: ReadableStream<Uint8Array> = htmlStream;
  if (!options?.debugNoJS) {
    // 初始 RSC 流作为 <script>...FLIGHT_DATA...</script> 注入到 HTML 流中,
    // 使用 devongovett 开发的工具 https://github.com/devongovett/rsc-html-stream
    responseStream = responseStream.pipeThrough(
      injectRSCPayload(rscStream2, {
        nonce: options?.nonce,
      })
    );
  }

  return { stream: responseStream, status };
}

SSR 入口处理渲染流程。它加载 RSC 入口模块,复制 RSC 流(一份用于 SSR,一份用于水合),将流反序列化回 React 虚拟 DOM 并渲染为 HTML。RSC 负载通过注入到 HTML 中实现客户端水合。

2. 根服务器组件

app/root.tsx
import "./index.css"; // css 导入会自动注入导出的服务器组件中
import viteLogo from "./assets/vite.svg";
import { getServerCounter, updateServerCounter } from "./action.tsx";
import reactLogo from "./assets/react.svg";
import nitroLogo from "./assets/nitro.svg";
import { ClientCounter } from "./client.tsx";

export function Root(props: { url: URL }) {
  return (
    <html lang="en">
      <head>
        {/* eslint-disable-next-line unicorn/text-encoding-identifier-case */}
        <meta charSet="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/vite.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Nitro + Vite + RSC</title>
      </head>
      <body>
        <App {...props} />
      </body>
    </html>
  );
}

function App(props: { url: URL }) {
  return (
    <div id="root">
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev/reference/rsc/server-components" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>

        <a href="https://v3.nitro.build" target="_blank">
          <img src={nitroLogo} className="logo" alt="Nitro logo" />
        </a>
      </div>
      <h1>Vite + RSC + Nitro</h1>
      <div className="card">
        <ClientCounter />
      </div>
      <div className="card">
        <form action={updateServerCounter.bind(null, 1)}>
          <button>服务器计数器:{getServerCounter()}</button>
        </form>
      </div>
      <div className="card">请求 URL: {props.url?.href}</div>
      <ul className="read-the-docs">
        <li>
          编辑 <code>src/client.tsx</code> 来测试客户端 HMR。
        </li>
        <li>
          编辑 <code>src/root.tsx</code> 来测试服务器端 HMR。
        </li>
        <li>
          访问{" "}
          <a href="./_.rsc" target="_blank">
            <code>_.rsc</code>
          </a>{" "}
          查看 RSC 流负载。
        </li>
        <li>
          访问{" "}
          <a href="?__nojs" target="_blank">
            <code>?__nojs</code>
          </a>{" "}
          测试在不启用 JS 的情况下的服务器动作。
        </li>
      </ul>
    </div>
  );
}

服务器组件仅在服务器端运行。它们可以直接导入 CSS,使用服务器端数据并调用服务器动作。ClientCounter 组件被导入但运行在客户端,因为它带有 "use client" 指令。

3. 客户端组件

app/client.tsx
"use client";

import React from "react";

export function ClientCounter() {
  const [count, setCount] = React.useState(0);

  return <button onClick={() => setCount((count) => count + 1)}>客户端计数器:{count}</button>;
}

"use client" 指令将其标记为客户端组件。它在浏览器中水合并处理交互状态。服务器组件可以导入和渲染客户端组件,但客户端组件不能导入服务器组件。

了解更多