Server Components
Server Components 是一种新的组件类型,它会在打包之前提前渲染,并且运行在与你的客户端应用或 SSR 服务器分离的环境中。
这个独立的环境就是 React Server Components 中的“服务器”。Server Components 可以在 CI 服务器上于构建时运行一次,也可以使用 Web 服务器在每个请求时运行。
- 没有服务器的 Server Components
- 带有服务器的 Server Components
- 为 Server Components 添加交互性
- 使用 Server Components 的异步组件
没有服务器的 Server Components
Server components 可以在构建时运行,从文件系统读取内容或获取静态内容,因此并不需要 Web 服务器。例如,你可能希望从内容管理系统中读取静态数据。
如果没有 Server Components,通常会在客户端通过 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('');
// 注意:在第一次页面渲染之后才加载。
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(gzipped)的库,并且在页面加载后还要等待第二次请求来获取数据,仅仅是为了渲染生命周期内不会变化的静态内容。
使用 Server Components,你可以在构建时一次性渲染这些组件:
import marked from 'marked'; // 不包含在 bundle 中
import sanitizeHtml from 'sanitize-html'; // 不包含在 bundle 中
async function Page({page}) {
// 注意:在应用构建时于渲染期间加载。
const content = await file.readFile(`${page}.md`);
return <div>{sanitizeHtml(marked(content))}</div>;
}然后,渲染结果可以被服务端渲染(SSR)为 HTML,并上传到 CDN。当应用加载时,客户端不会看到原始的 Page 组件,也不会看到用于渲染 markdown 的昂贵库。客户端只会看到渲染后的输出:
<div><!-- markdown 的 HTML --></div>这意味着内容会在首次页面加载时就可见,而且 bundle 中不包含渲染静态内容所需的昂贵库。
带有服务器的 Server Components
Server Components 也可以在请求页面时运行在 Web 服务器上,让你无需构建 API 就能访问数据层。它们会在你的应用被打包之前渲染,并且可以将数据和 JSX 作为 props 传递给 Client Components。
如果没有 Server Components,通常会在客户端通过 Effect 获取动态数据:
// bundle.js
function Note({id}) {
const [note, setNote] = useState('');
// 注意:在第一次渲染之后才加载。
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 渲染之后才加载。
// 这会导致昂贵的客户端到服务器瀑布请求。
useEffect(() => {
fetch(`/api/authors/${id}`).then(data => {
setAuthor(data.author);
});
}, [id]);
return <span>作者:{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});
});使用 Server Components,你可以在组件中读取数据并进行渲染:
import db from './database';
async function Note({id}) {
// 注意:在渲染期间加载。
const note = await db.notes.get(id);
return (
<div>
<Author id={note.authorId} />
<p>{note}</p>
</div>
);
}
async function Author({id}) {
// 注意:在 Note 之后加载,
// 但如果数据是同地存放的,则会很快。
const author = await db.authors.get(id);
return <span>作者:{author.name}</span>;
}然后,打包器会将数据、渲染后的 Server Components 以及动态 Client Components 合并到一个 bundle 中。可选地,这个 bundle 还可以再进行服务端渲染(SSR),为页面生成初始 HTML。当页面加载时,浏览器不会看到原始的 Note 和 Author 组件;只有渲染后的输出会发送给客户端:
<div>
<span>作者:React 团队</span>
<p>React 19 是...</p>
</div>Server Components 可以通过从服务器重新获取它们来变为动态化,在服务器上它们可以再次访问数据并重新渲染。这个新的应用架构将服务端中心的多页应用简单的“请求/响应”心智模型,与客户端中心的单页应用无缝交互性结合在一起,为你提供两全其美的体验。
为 Server Components 添加交互性
Server Components 不会发送到浏览器,因此它们不能使用 useState 之类的交互式 API。要为 Server Components 添加交互性,你可以使用 "use client" 指令将它们与 Client Component 组合起来。
在下面的示例中,Notes 这个 Server Component 导入了一个 Expandable Client Component,它使用 state 来切换其 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)}
>
切换
</button>
{expanded && children}
</div>
)
}其工作方式是先将 Notes 作为 Server Component 渲染,然后指示打包器为 Client Component Expandable 创建一个 bundle。在浏览器中,Client Components 会接收到作为 props 传入的 Server Components 输出:
<head>
<!-- Client Components 的 bundle -->
<script src="bundle.js" />
</head>
<body>
<div>
<Expandable key={1}>
<p>这是第一条笔记</p>
</Expandable>
<Expandable key={2}>
<p>这是第二条笔记</p>
</Expandable>
<!--...-->
</div>
</body>使用 Server Components 的异步组件
Server Components 引入了一种使用 async/await 编写组件的新方式。当你在异步组件中 await 时,React 会挂起并等待 promise 解析后再继续渲染。借助 Suspense 的流式支持,这种方式可以跨越 server/client 边界工作。
你甚至可以在服务端创建一个 promise,然后在客户端 await 它:
// Server Component
import db from './database';
async function Page({id}) {
// 将会挂起 Server Component。
const note = await db.notes.get(id);
// 注意:没有 await,会从这里开始,并在客户端 await。
const commentsPromise = db.comments.get(note.id);
return (
<div>
{note}
<Suspense fallback={<p>正在加载评论...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</div>
);
}// Client Component
"use client";
import {use} from 'react';
function Comments({commentsPromise}) {
// 注意:这会恢复来自服务器的 promise。
// 它会一直挂起,直到数据可用。
const comments = use(commentsPromise);
return comments.map(comment => <p>{comment}</p>);
}note 内容是页面渲染所需的重要数据,所以我们在服务端 await 它。评论位于首屏下方,优先级更低,所以我们在服务端启动这个 promise,然后在客户端使用 use API 等待它。这会在客户端触发 Suspense,而不会阻塞 note 内容的渲染。
由于客户端不支持异步组件,我们使用 use 来 await 这个 promise。