服务器组件
服务器组件是一种新型组件,它在捆绑之前提前渲染,在与客户端应用程序或 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>
可以通过从服务器重新获取服务器组件来使其动态化,服务器组件可以在其中访问数据并再次渲染。这种新的应用程序架构将服务器中心多页应用程序的简单“请求/响应”思维模型与客户端中心单页应用程序的无缝交互性相结合,让您同时获得两者的优势。
在服务器组件中添加交互性
服务器组件不会发送到浏览器,因此它们无法使用交互式 API,例如 useState
。要在服务器组件中添加交互性,可以使用 "use client"
指令将其与客户端组件组合。
在下面的示例中,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。