React 服务器组件
服务器组件是一种新型组件,它在打包之前,在与客户端应用或 SSR 服务器分离的环境中提前渲染。
这个独立的环境就是 React 服务器组件中的“服务器”。服务器组件可以在构建时在 CI 服务器上运行一次,也可以使用 Web 服务器为每个请求运行。
没有服务器的服务器组件
服务器组件可以在构建时运行以从文件系统读取或获取静态内容,因此不需要 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>
这意味着内容在首次加载页面时可见,并且捆绑包不包括渲染静态内容所需的昂贵库。
带有服务器的服务器组件
服务器组件还可以在页面请求期间在 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。当页面加载时,浏览器不会看到原始的 Note
和 Author
组件;只有渲染的输出发送到客户端
<div>
<span>By: The React Team</span>
<p>React 19 is...</p>
</div>
可以通过从服务器重新获取服务器组件来使其动态化,在服务器上它们可以访问数据并再次渲染。这种新的应用程序架构将以服务器为中心的多页应用程序的简单“请求/响应”心智模型与以客户端为中心的单页应用程序的无缝交互性相结合,为您提供两全其美的优势。
向服务器组件添加交互性
服务器组件不会发送到浏览器,因此它们不能使用像 useState
这样的交互式 API。要向服务器组件添加交互性,您可以使用 "use client"
指令将它们与客户端组件组合。
在以下示例中,Notes
服务器组件导入了一个 Expandable
客户端组件,该组件使用状态来切换其 expanded
状态
// 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,并在客户端 await 它
// 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
来 await promise。