服务器组件

React 服务器组件

服务器组件用于React 服务器组件

服务器组件是一种新型组件,它在捆绑之前提前渲染,在与客户端应用程序或 SSR 服务器不同的环境中。

这个单独的环境就是 React 服务器组件中的“服务器”。服务器组件可以在您的 CI 服务器上构建时运行一次,或者可以使用 Web 服务器为每个请求运行。

注意

如何构建对服务器组件的支持?

虽然 React 19 中的 React 服务器组件是稳定的,并且不会在主要版本之间中断,但用于实现 React 服务器组件捆绑器或框架的基础 API 并不遵循语义版本控制,并且可能在 React 19.x 的次要版本之间中断。

为了支持作为捆绑器或框架的 React 服务器组件,我们建议固定到特定的 React 版本,或使用 Canary 版本。我们将继续与捆绑器和框架合作,以在将来稳定用于实现 React 服务器组件的 API。

无服务器的服务器组件

服务器组件可以在构建时运行以读取文件系统或获取静态内容,因此不需要 Web 服务器。例如,您可能希望从内容管理系统读取静态数据。

在没有服务器组件的情况下,通常使用 Effect 在客户端获取静态数据

// bundle.js
import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)

function Page({page}) {
const [content, setContent] = useState('');
// NOTE: loads *after* first page render.
useEffect(() => {
fetch(`/api/content/${page}`).then((data) => {
setContent(data.content);
});
}, [page]);

return <div>{sanitizeHtml(marked(content))}</div>;
}
// api.js
app.get(`/api/content/:page`, async (req, res) => {
const page = req.params.page;
const content = await file.readFile(`${page}.md`);
res.send({content});
});

这种模式意味着用户需要下载和解析额外的 75K(gzip 压缩)的库,并在页面加载后等待第二个请求来获取数据,只是为了渲染在页面生命周期内不会更改的静态内容。

使用服务器组件,您可以在构建时渲染这些组件一次

import marked from 'marked'; // Not included in bundle
import sanitizeHtml from 'sanitize-html'; // Not included in bundle

async function Page({page}) {
// NOTE: loads *during* render, when the app is built.
const content = await file.readFile(`${page}.md`);

return <div>{sanitizeHtml(marked(content))}</div>;
}

然后可以将渲染后的输出服务器端渲染 (SSR) 到 HTML 并上传到 CDN。当应用程序加载时,客户端将看不到原始的Page组件,或用于渲染 markdown 的昂贵库。客户端只会看到渲染后的输出

<div><!-- html for markdown --></div>

这意味着内容在第一次页面加载时可见,并且捆绑包不包含渲染静态内容所需的昂贵库。

注意

您可能会注意到上面的服务器组件是一个异步函数

async function Page({page}) {
//...
}

异步组件是服务器组件的一项新功能,允许您在渲染中使用await

请参见下面的带有服务器组件的异步组件

有服务器的服务器组件

服务器组件也可以在页面请求期间在 Web 服务器上运行,让您可以访问数据层而无需构建 API。它们在应用程序捆绑之前进行渲染,并且可以将数据和 JSX 作为 props 传递给客户端组件。

在没有服务器组件的情况下,通常在 Effect 中在客户端获取动态数据

// bundle.js
function Note({id}) {
const [note, setNote] = useState('');
// NOTE: loads *after* first render.
useEffect(() => {
fetch(`/api/notes/${id}`).then(data => {
setNote(data.note);
});
}, [id]);

return (
<div>
<Author id={note.authorId} />
<p>{note}</p>
</div>
);
}

function Author({id}) {
const [author, setAuthor] = useState('');
// NOTE: loads *after* Note renders.
// Causing an expensive client-server waterfall.
useEffect(() => {
fetch(`/api/authors/${id}`).then(data => {
setAuthor(data.author);
});
}, [id]);

return <span>By: {author.name}</span>;
}
// api
import db from './database';

app.get(`/api/notes/:id`, async (req, res) => {
const note = await db.notes.get(id);
res.send({note});
});

app.get(`/api/authors/:id`, async (req, res) => {
const author = await db.authors.get(id);
res.send({author});
});

使用服务器组件,您可以读取数据并在组件中渲染它

import db from './database';

async function Note({id}) {
// NOTE: loads *during* render.
const note = await db.notes.get(id);
return (
<div>
<Author id={note.authorId} />
<p>{note}</p>
</div>
);
}

async function Author({id}) {
// NOTE: loads *after* Note,
// but is fast if data is co-located.
const author = await db.authors.get(id);
return <span>By: {author.name}</span>;
}

然后,捆绑器将数据、渲染的服务器组件和动态客户端组件组合到一个捆绑包中。可选地,该捆绑包然后可以服务器端渲染 (SSR) 以创建页面的初始 HTML。当页面加载时,浏览器不会看到原始的NoteAuthor组件;只有渲染后的输出才会发送到客户端

<div>
<span>By: The React Team</span>
<p>React 19 is...</p>
</div>

可以通过从服务器重新获取服务器组件来使其动态化,服务器组件可以在其中访问数据并再次渲染。这种新的应用程序架构将服务器中心多页应用程序的简单“请求/响应”思维模型与客户端中心单页应用程序的无缝交互性相结合,让您同时获得两者的优势。

在服务器组件中添加交互性

服务器组件不会发送到浏览器,因此它们无法使用交互式 API,例如 useState。要在服务器组件中添加交互性,可以使用 "use client" 指令将其与客户端组件组合。

注意

服务器组件没有指令。

一个常见的误解是,服务器组件由 "use server" 表示,但服务器组件没有指令。"use server" 指令用于服务器函数。

更多信息,请参阅关于指令 的文档。

在下面的示例中,Notes 服务器组件导入了一个使用状态切换其 expanded 状态的 Expandable 客户端组件。

// Server Component
import Expandable from './Expandable';

async function Notes() {
const notes = await db.notes.getAll();
return (
<div>
{notes.map(note => (
<Expandable key={note.id}>
<p note={note} />
</Expandable>
))}
</div>
)
}
// Client Component
"use client"

export default function Expandable({children}) {
const [expanded, setExpanded] = useState(false);
return (
<div>
<button
onClick={() => setExpanded(!expanded)}
>
Toggle
</button>
{expanded && children}
</div>
)
}

它的工作原理是:首先将 Notes 渲染为服务器组件,然后指示捆绑器为客户端组件 Expandable 创建一个捆绑包。在浏览器中,客户端组件将看到作为 props 传递的服务器组件的输出。

<head>
<!-- the bundle for Client Components -->
<script src="bundle.js" />
</head>
<body>
<div>
<Expandable key={1}>
<p>this is the first note</p>
</Expandable>
<Expandable key={2}>
<p>this is the second note</p>
</Expandable>
<!--...-->
</div>
</body>

使用服务器组件的异步组件

服务器组件引入了一种使用 async/await 编写组件的新方法。当你在异步组件中 await 时,React 将挂起并等待 promise 解析后再恢复渲染。这在服务器/客户端边界上都能正常工作,并支持 Suspense 的流式传输。

你甚至可以在服务器上创建一个 promise,并在客户端等待它。

// Server Component
import db from './database';

async function Page({id}) {
// Will suspend the Server Component.
const note = await db.notes.get(id);

// NOTE: not awaited, will start here and await on the client.
const commentsPromise = db.comments.get(note.id);
return (
<div>
{note}
<Suspense fallback={<p>Loading Comments...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</div>
);
}
// Client Component
"use client";
import {use} from 'react';

function Comments({commentsPromise}) {
// NOTE: this will resume the promise from the server.
// It will suspend until the data is available.
const comments = use(commentsPromise);
return comments.map(commment => <p>{comment}</p>);
}

note 内容是页面渲染的重要数据,因此我们在服务器上 await 它。评论位于屏幕下方,优先级较低,因此我们在服务器上启动 promise,并在客户端使用 use API 等待它。这将在客户端挂起,而不会阻止 note 内容的渲染。

由于异步组件在客户端不受支持,因此我们使用 use 等待 promise。