React 服务器组件
本页内容

React 服务器组件



React 服务器组件支持是实验性的,可能会有重大更改。

React 服务器组件 (RSC) 通常指自 React 19 版以来提供的一种架构和一组 API。

来自文档

服务器组件是一种新型组件,它在打包之前、在一个与客户端应用程序或 SSR 服务器分离的环境中提前渲染。

- React "服务器组件" 文档

React Router 提供了一组用于与 RSC 兼容的打包工具集成的 API,允许您在 React Router 应用程序中利用服务器组件服务器函数

快速入门

最快的入门方法是使用我们的模板之一。

这些模板预先配置了 React Router RSC API,并与各自的打包工具集成,为您提供开箱即用的功能,例如:

  • 服务器组件路由
  • 服务器端渲染 (SSR)
  • 客户端组件(通过 "use client" 指令)
  • 服务器函数(通过 "use server" 指令)

Parcel 模板

parcel 模板使用官方的 React react-server-dom-parcel 插件。

npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-parcel

Vite 模板

vite 模板使用实验性的 Vite @vitejs/plugin-rsc 插件。

npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-vite

将 RSC 与 React Router 结合使用

配置路由

路由配置为 matchRSCServerRequest 的参数。至少需要一个路径和组件。

function Root() {
  return <h1>Hello world</h1>;
}

matchRSCServerRequest({
  // ...other options
  routes: [{ path: "/", Component: Root }],
});

虽然您可以内联定义组件,但我们建议使用 lazy() 选项并定义路由模块,以提高启动性能和代码组织性。

到目前为止,路由模块 API 只是框架模式独有的功能。然而,RSC 路由配置的 lazy 字段期望与路由模块导出相同的导出,从而进一步统一了 API。

import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router";

export function routes() {
  return [
    {
      id: "root",
      path: "",
      lazy: () => import("./root/route"),
      children: [
        {
          id: "home",
          index: true,
          lazy: () => import("./home/route"),
        },
        {
          id: "about",
          path: "about",
          lazy: () => import("./about/route"),
        },
      ],
    },
  ] satisfies RSCRouteConfig;
}

服务器组件路由

默认情况下,每个路由的 default 导出都会渲染一个服务器组件。

export default function Home() {
  return (
    <main>
      <article>
        <h1>Welcome to React Router RSC</h1>
        <p>
          You won't find me running any JavaScript in the
          browser!
        </p>
      </article>
    </main>
  );
}

服务器组件的一个很棒的功能是,您可以通过将其设置为异步来直接从组件中获取数据。

export default async function Home() {
  let user = await getUserData();

  return (
    <main>
      <article>
        <h1>Welcome to React Router RSC</h1>
        <p>
          You won't find me running any JavaScript in the
          browser!
        </p>
        <p>
          Hello, {user ? user.name : "anonymous person"}!
        </p>
      </article>
    </main>
  );
}

服务器组件也可以从您的加载器和操作中返回。通常,如果您使用 RSC 构建应用程序,加载器主要用于设置 status 代码或返回 redirect 等操作。

在加载器中使用服务器组件有助于逐步采用 RSC。

服务器函数

服务器函数是 React 的一项功能,允许您调用在服务器上执行的异步函数。它们使用 "use server" 指令定义。

"use server";

export async function updateFavorite(formData: FormData) {
  let movieId = formData.get("id");
  let intent = formData.get("intent");
  if (intent === "add") {
    await addFavorite(Number(movieId));
  } else {
    await removeFavorite(Number(movieId));
  }
}
import { updateFavorite } from "./action.ts";
export async function AddToFavoritesForm({
  movieId,
}: {
  movieId: number;
}) {
  let isFav = await isFavorite(movieId);
  return (
    <form action={updateFavorite}>
      <input type="hidden" name="id" value={movieId} />
      <input
        type="hidden"
        name="intent"
        value={isFav ? "remove" : "add"}
      />
      <AddToFavoritesButton isFav={isFav} />
    </form>
  );
}

请注意,调用服务器函数后,React Router 将自动重新验证路由并使用新的服务器内容更新 UI。您不必处理任何缓存失效的问题。

客户端属性

路由在运行时于服务器上定义,但我们仍然可以通过利用客户端引用和 "use client" 来提供 clientLoaderclientActionshouldRevalidate

"use client";

export function clientAction() {}

export function clientLoader() {}

export function shouldRevalidate() {}

然后,我们可以从延迟加载的路由模块中重新导出这些内容。

export {
  clientAction,
  clientLoader,
  shouldRevalidate,
} from "./route.client";

export default function Root() {
  // ...
}

这也是我们将整个路由变成客户端组件的方式。

import { default as ClientRoot } from "./route.client";
export {
  clientAction,
  clientLoader,
  shouldRevalidate,
} from "./route.client";

export default function Root() {
  // Adding a Server Component at the root is required by bundlers
  // if you're using css side-effects imports.
  return <ClientRoot />;
}

使用 React Router 配置 RSC

React Router 提供了多个 API,使您可以轻松地与 RSC 兼容的打包工具集成,如果您正在使用 React Router 数据模式来构建自己的自定义框架,这将非常有用。

以下步骤展示了如何设置一个 React Router 应用程序以使用服务器组件 (RSC) 来服务器渲染 (SSR) 页面,并为单页面应用 (SPA) 导航进行水合。如果您不希望,则不必使用 SSR(甚至客户端水合)。如果您愿意,您还可以利用 HTML 生成来进行静态站点生成 (SSG) 或增量静态再生 (ISR)。本指南仅旨在解释如何为典型的基于 RSC 的应用程序连接所有不同的 API。

入口点

除了我们的路由定义之外,我们还需要配置以下内容:

  1. 一个服务器来处理传入请求、获取 RSC 负载并将其转换为 HTML。
  2. 一个 React 服务器来生成 RSC 负载。
  3. 一个浏览器处理程序来水合生成的 HTML,并设置 callServer 函数以支持水合后的服务器操作。

为了熟悉和简单,我们选择了以下命名约定。您可以根据需要随意命名和配置您的入口点。

请参阅下面相关的打包工具文档,以获取以下每个入口点的具体代码示例。

这些示例都使用 express@remix-run/node-fetch-server 来进行服务器和请求处理。

路由

请参阅配置路由

服务器

您完全不必使用 SSR。您可以选择使用 RSC 来“预渲染”HTML,用于静态站点生成 (SSG) 或增量静态再生 (ISR) 等。

entry.ssr.tsx 是服务器的入口点。它负责处理请求,调用 RSC 服务器,并在文档请求时将 RSC 负载转换为 HTML(服务器端渲染)。

相关 API

RSC 服务器

尽管您有一个“React 服务器”和一个负责请求处理/SSR 的服务器,但您实际上并不需要有两个独立的服务器。您可以在同一个服务器内拥有两个独立的模块图。这很重要,因为 React 在生成 RSC 负载时和生成要在客户端水合的 HTML 时的行为是不同的。

entry.rsc.tsx 是 React 服务器的入口点。它负责将请求与路由匹配并生成 RSC 负载。

相关 API

浏览器

entry.browser.tsx 是客户端的入口点。它负责水合生成的 HTML,并设置 callServer 函数以支持水合后的服务器操作。

相关 API

Parcel

有关更多信息,请参阅 Parcel RSC 文档。您也可以参考我们的 Parcel RSC Parcel 模板查看一个可运行的版本。

除了 reactreact-domreact-router 之外,您还需要以下依赖项:

# install runtime dependencies
npm i @parcel/runtime-rsc react-server-dom-parcel

# install dev dependencies
npm i -D parcel

package.json

要配置 Parcel,请将以下内容添加到您的 package.json 中:

{
  "scripts": {
    "build": "parcel build --no-autoinstall",
    "dev": "cross-env NODE_ENV=development parcel --no-autoinstall --no-cache",
    "start": "cross-env NODE_ENV=production node dist/server/entry.rsc.js"
  },
  "targets": {
    "react-server": {
      "context": "react-server",
      "source": "src/entry.rsc.tsx",
      "scopeHoist": false,
      "includeNodeModules": {
        "@remix-run/node-fetch-server": false,
        "compression": false,
        "express": false
      }
    }
  }
}

routes/config.ts

您必须在定义路由的文件顶部添加 "use server-entry"。此外,您需要导入客户端入口点,因为它将使用 "use client-entry" 指令(见下文)。

"use server-entry";

import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router";

import "../entry.browser";

// This needs to be a function so Parcel can add a `bootstrapScript` property.
export function routes() {
  return [
    {
      id: "root",
      path: "",
      lazy: () => import("./root/route"),
      children: [
        {
          id: "home",
          index: true,
          lazy: () => import("./home/route"),
        },
        {
          id: "about",
          path: "about",
          lazy: () => import("./about/route"),
        },
      ],
    },
  ] satisfies RSCRouteConfig;
}

entry.ssr.tsx

以下是 Parcel SSR 服务器的简化示例。

import { renderToReadableStream as renderHTMLToReadableStream } from "react-dom/server.edge";
import {
  unstable_routeRSCServerRequest as routeRSCServerRequest,
  unstable_RSCStaticRouter as RSCStaticRouter,
} from "react-router";
import { createFromReadableStream } from "react-server-dom-parcel/client.edge";

export async function generateHTML(
  request: Request,
  fetchServer: (request: Request) => Promise<Response>,
  bootstrapScriptContent: string | undefined,
): Promise<Response> {
  return await routeRSCServerRequest({
    // The incoming request.
    request,
    // How to call the React Server.
    fetchServer,
    // Provide the React Server touchpoints.
    createFromReadableStream,
    // Render the router to HTML.
    async renderHTML(getPayload) {
      const payload = await getPayload();
      const formState =
        payload.type === "render"
          ? await payload.formState
          : undefined;

      return await renderHTMLToReadableStream(
        <RSCStaticRouter getPayload={getPayload} />,
        {
          bootstrapScriptContent,
          formState,
        },
      );
    },
  });
}

entry.rsc.tsx

以下是 Parcel RSC 服务器的简化示例。

import { createRequestListener } from "@remix-run/node-fetch-server";
import express from "express";
import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router";
import {
  createTemporaryReferenceSet,
  decodeAction,
  decodeFormState,
  decodeReply,
  loadServerAction,
  renderToReadableStream,
} from "react-server-dom-parcel/server.edge";

// Import the generateHTML function from the react-client environment
import { generateHTML } from "./entry.ssr" with { env: "react-client" };
import { routes } from "./routes/config";

function fetchServer(request: Request) {
  return matchRSCServerRequest({
    // Provide the React Server touchpoints.
    createTemporaryReferenceSet,
    decodeAction,
    decodeFormState,
    decodeReply,
    loadServerAction,
    // The incoming request.
    request,
    // The app routes.
    routes: routes(),
    // Encode the match with the React Server implementation.
    generateResponse(match) {
      return new Response(
        renderToReadableStream(match.payload),
        {
          status: match.statusCode,
          headers: match.headers,
        },
      );
    },
  });
}

const app = express();

// Serve static assets with compression and long cache lifetime.
app.use(
  "/client",
  compression(),
  express.static("dist/client", {
    immutable: true,
    maxAge: "1y",
  }),
);
// Hook up our application.
app.use(
  createRequestListener((request) =>
    generateHTML(
      request,
      fetchServer,
      (routes as unknown as { bootstrapScript?: string })
        .bootstrapScript,
    ),
  ),
);

app.listen(3000, () => {
  console.log("Server listening on port 3000");
});

entry.browser.tsx

"use client-entry";

import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import {
  unstable_createCallServer as createCallServer,
  unstable_getRSCStream as getRSCStream,
  unstable_RSCHydratedRouter as RSCHydratedRouter,
  type unstable_RSCPayload as RSCServerPayload,
} from "react-router";
import {
  createFromReadableStream,
  createTemporaryReferenceSet,
  encodeReply,
  setServerCallback,
} from "react-server-dom-parcel/client";

// Create and set the callServer function to support post-hydration server actions.
setServerCallback(
  createCallServer({
    createFromReadableStream,
    createTemporaryReferenceSet,
    encodeReply,
  }),
);

// Get and decode the initial server payload.
createFromReadableStream(getRSCStream()).then(
  (payload: RSCServerPayload) => {
    startTransition(async () => {
      const formState =
        payload.type === "render"
          ? await payload.formState
          : undefined;

      hydrateRoot(
        document,
        <StrictMode>
          <RSCHydratedRouter
            createFromReadableStream={
              createFromReadableStream
            }
            payload={payload}
          />
        </StrictMode>,
        {
          formState,
        },
      );
    });
  },
);

Vite

有关更多信息,请参阅 Vite RSC 文档。您也可以参考我们的 Vite RSC 模板查看一个可运行的版本。

除了 reactreact-domreact-router 之外,您还需要以下依赖项:

npm i -D vite @vitejs/plugin-react @vitejs/plugin-rsc

vite.config.ts

要配置 Vite,请将以下内容添加到您的 vite.config.ts 中:

import rsc from "@vitejs/plugin-rsc/plugin";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    react(),
    rsc({
      entries: {
        client: "src/entry.browser.tsx",
        rsc: "src/entry.rsc.tsx",
        ssr: "src/entry.ssr.tsx",
      },
    }),
  ],
});
import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router";

export function routes() {
  return [
    {
      id: "root",
      path: "",
      lazy: () => import("./root/route"),
      children: [
        {
          id: "home",
          index: true,
          lazy: () => import("./home/route"),
        },
        {
          id: "about",
          path: "about",
          lazy: () => import("./about/route"),
        },
      ],
    },
  ] satisfies RSCRouteConfig;
}

entry.ssr.tsx

以下是 Vite SSR 服务器的简化示例。

import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
import { renderToReadableStream as renderHTMLToReadableStream } from "react-dom/server.edge";
import {
  unstable_routeRSCServerRequest as routeRSCServerRequest,
  unstable_RSCStaticRouter as RSCStaticRouter,
} from "react-router";

export async function generateHTML(
  request: Request,
  fetchServer: (request: Request) => Promise<Response>,
): Promise<Response> {
  return await routeRSCServerRequest({
    // The incoming request.
    request,
    // How to call the React Server.
    fetchServer,
    // Provide the React Server touchpoints.
    createFromReadableStream,
    // Render the router to HTML.
    async renderHTML(getPayload) {
      const payload = await getPayload();
      const formState =
        payload.type === "render"
          ? await payload.formState
          : undefined;

      const bootstrapScriptContent =
        await import.meta.viteRsc.loadBootstrapScriptContent(
          "index",
        );

      return await renderHTMLToReadableStream(
        <RSCStaticRouter getPayload={getPayload} />,
        {
          bootstrapScriptContent,
          formState,
        },
      );
    },
  });
}

entry.rsc.tsx

以下是 Vite RSC 服务器的简化示例。

import {
  createTemporaryReferenceSet,
  decodeAction,
  decodeFormState,
  decodeReply,
  loadServerAction,
  renderToReadableStream,
} from "@vitejs/plugin-rsc/rsc";
import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router";

import { routes } from "./routes/config";

function fetchServer(request: Request) {
  return matchRSCServerRequest({
    // Provide the React Server touchpoints.
    createTemporaryReferenceSet,
    decodeAction,
    decodeFormState,
    decodeReply,
    loadServerAction,
    // The incoming request.
    request,
    // The app routes.
    routes: routes(),
    // Encode the match with the React Server implementation.
    generateResponse(match) {
      return new Response(
        renderToReadableStream(match.payload),
        {
          status: match.statusCode,
          headers: match.headers,
        },
      );
    },
  });
}

export default async function handler(request: Request) {
  // Import the generateHTML function from the client environment
  const ssr = await import.meta.viteRsc.loadModule<
    typeof import("./entry.ssr")
  >("ssr", "index");

  return ssr.generateHTML(request, fetchServer);
}

entry.browser.tsx

import {
  createFromReadableStream,
  createTemporaryReferenceSet,
  encodeReply,
  setServerCallback,
} from "@vitejs/plugin-rsc/browser";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import {
  unstable_createCallServer as createCallServer,
  unstable_getRSCStream as getRSCStream,
  unstable_RSCHydratedRouter as RSCHydratedRouter,
  type unstable_RSCPayload as RSCServerPayload,
} from "react-router";

// Create and set the callServer function to support post-hydration server actions.
setServerCallback(
  createCallServer({
    createFromReadableStream,
    createTemporaryReferenceSet,
    encodeReply,
  }),
);

// Get and decode the initial server payload.
createFromReadableStream<RSCServerPayload>(
  getRSCStream(),
).then((payload) => {
  startTransition(async () => {
    const formState =
      payload.type === "render"
        ? await payload.formState
        : undefined;

    hydrateRoot(
      document,
      <StrictMode>
        <RSCHydratedRouter
          createFromReadableStream={
            createFromReadableStream
          }
          payload={payload}
        />
      </StrictMode>,
      {
        formState,
      },
    );
  });
});
文档和示例 CC 4.0
编辑