React 服务器组件 (RSC) 通常指自 React 19 版以来提供的一种架构和一组 API。
来自文档
服务器组件是一种新型组件,它在打包之前、在一个与客户端应用程序或 SSR 服务器分离的环境中提前渲染。
React Router 提供了一组用于与 RSC 兼容的打包工具集成的 API,允许您在 React Router 应用程序中利用服务器组件和服务器函数。
最快的入门方法是使用我们的模板之一。
这些模板预先配置了 React Router RSC API,并与各自的打包工具集成,为您提供开箱即用的功能,例如:
"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
路由配置为 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"
来提供 clientLoader
、clientAction
和 shouldRevalidate
。
"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 提供了多个 API,使您可以轻松地与 RSC 兼容的打包工具集成,如果您正在使用 React Router 数据模式来构建自己的自定义框架,这将非常有用。
以下步骤展示了如何设置一个 React Router 应用程序以使用服务器组件 (RSC) 来服务器渲染 (SSR) 页面,并为单页面应用 (SPA) 导航进行水合。如果您不希望,则不必使用 SSR(甚至客户端水合)。如果您愿意,您还可以利用 HTML 生成来进行静态站点生成 (SSG) 或增量静态再生 (ISR)。本指南仅旨在解释如何为典型的基于 RSC 的应用程序连接所有不同的 API。
除了我们的路由定义之外,我们还需要配置以下内容:
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 RSC 文档。您也可以参考我们的 Parcel RSC Parcel 模板查看一个可运行的版本。
除了 react
、react-dom
和 react-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 RSC 文档。您也可以参考我们的 Vite RSC 模板查看一个可运行的版本。
除了 react
、react-dom
和 react-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,
},
);
});
});