react
React Server Components 与 SSR
By AI-Writer 14 min read
前言
React Server Components(RSC) 是 React 18 引入的重大特性,代表了 React 渲染架构的根本性变革。本篇文章将深入讲解 RSC 原理、服务端与客户端组件边界、流式渲染,以及 Next.js App Router 的实践应用。
渲染模式演进
传统 CSR vs SSR
plaintext
CSR (Client-Side Rendering)
┌─────────────────────────────┐
│ 服务器返回空白 HTML │
│ ↓ │
│ 浏览器下载 JS Bundle │
│ ↓ │
│ React 执行,生成 DOM │
│ ↓ │
│ 用户看到内容 │
└─────────────────────────────┘
首次内容绘制 (FCP) 较慢
SSR (Server-Side Rendering)
┌─────────────────────────────┐
│ 服务器执行 React,生成 HTML │
│ ↓ │
│ 返回完整 HTML │
│ ↓ │
│ 浏览器直接显示内容 │
│ ↓ │
│ 下载 JS, hydration │
│ ↓ │
│ 应用可交互 │
└─────────────────────────────┘
FCP 快,但 TTI 需要等 hydrationRSC 的核心理念
RSC 将组件分为两类:
- Server Components:仅在服务端运行,可以直接访问数据库、文件系统
- Client Components:在客户端运行,支持交互和状态管理
plaintext
RSC (React Server Components)
┌─────────────────────────────────────┐
│ 服务器直接渲染组件树 │
│ ↓ │
│ 流式传输 HTML 给浏览器 │
│ ↓ │
│ 选择性 hydration(仅必要部分) │
│ ↓ │
│ 更快的 FCP,更少的 JS │
└─────────────────────────────────────┘Server Components
基础语法
tsx
// app/components/ProductList.tsx
// 这是 Server Component(默认)
// 无需 'use client' 指令
async function ProductList() {
// 直接访问数据库,无需 API
const products = await db.products.findMany({
orderBy: { createdAt: 'desc' },
take: 10,
});
return (
<ul>
{products.map(product => (
<li key={product.id}>
<h3>{product.name}</h3>
<p>{product.price}</p>
</li>
))}
</ul>
);
}Server Component 特点
tsx
// ✓ 可以做的
async function Component() {
const data = await fetch('/api/data'); // 数据获取
const file = fs.readFileSync('config.json'); // 文件系统
const dbData = await db.query(); // 直接数据库访问
const secret = process.env.SECRET; // 服务端 secret
return <div>{/* ... */}</div>;
}
// ❌ 不能做的
function Component() {
const [state, setState] = useState(0); // 不能用 hooks
const ref = useRef(); // 不能用 DOM ref
useEffect(() => {}, []); // 不能有副作用
return <button onClick={() => setState(1)}>点击</button>;
}Client Components
声明客户端组件
tsx
// components/AddToCart.tsx
'use client';
import { useState } from 'react';
function AddToCart({ productId }) {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c - 1)}>-</button>
<span>{count}</span>
<button onClick={() => setCount(c => c + 1)}>+</button>
<button onClick={() => addToCart(productId, count)}>
加入购物车
</button>
</div>
);
}混用 Server 与 Client 组件
tsx
// app/products/[id]/page.tsx (Server Component)
async function ProductPage({ params }) {
const product = await getProduct(params.id);
return (
<div>
{/* Server Component: 静态内容 */}
<ProductInfo product={product} />
{/* Client Component: 交互部分 */}
<AddToCart productId={product.id} />
{/* Client Component: 评论区 */}
<Reviews productId={product.id} />
{/* Client Component: 分享按钮 */}
<ShareButton url={`/products/${product.id}`} />
</div>
);
}传递 props 给 Client Component
tsx
// Server Component (page.tsx)
async function Page() {
const data = await fetchData();
// ✓ 可以传递可序列化的 props
return <ClientComponent
initialData={data}
config={{ theme: 'dark' }}
items={['a', 'b', 'c']}
/>;
// ❌ 不能传递函数(非序列化)
// return <ClientComponent onClick={() => {}} />;
}
// Client Component
'use client';
function ClientComponent({ initialData, config, items }) {
// 使用从 Server Component 传来的数据
return <div>{/* ... */}</div>;
}Next.js App Router
目录结构
plaintext
app/
├── layout.tsx # 根布局
├── page.tsx # 首页 /
├── products/
│ ├── page.tsx # 商品列表 /products
│ ├── layout.tsx # 商品列表布局
│ └── [id]/
│ ├── page.tsx # 商品详情 /products/123
│ ├── loading.tsx # 加载状态
│ └── error.tsx # 错误边界
├── about/
│ └── page.tsx # 关于页 /about
└── api/
└── products/
└── route.ts # API 路由layout.tsx
tsx
// app/layout.tsx
import './globals.css';
export const metadata = {
title: 'My App',
description: 'A React RSC app',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}page.tsx
tsx
// app/page.tsx
async function HomePage() {
// 并行获取多个数据源
const [featuredProducts, latestNews, categories] = await Promise.all([
getFeaturedProducts(),
getLatestNews(),
getCategories(),
]);
return (
<>
<Hero />
<ProductGrid products={featuredProducts} />
<NewsList news={latestNews} />
<CategoryNav categories={categories} />
</>
);
}动态路由
tsx
// app/products/[id]/page.tsx
// 生成静态路径
export async function generateStaticParams() {
const products = await db.products.findMany({
select: { id: true },
});
return products.map(p => ({ id: p.id.toString() }));
}
// 动态页面
async function ProductPage({ params }) {
const product = await getProduct(params.id);
if (!product) {
notFound();
}
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
);
}流式渲染
Suspense 边界
tsx
// app/page.tsx
import { Suspense } from 'react';
async function HomePage() {
return (
<div>
{/* 立即显示的静态内容 */}
<Hero />
{/* 异步加载的部分用 Suspense 包裹 */}
<Suspense fallback={<ProductSkeleton />}>
<ProductRecommendations />
</Suspense>
<Suspense fallback={<NewsSkeleton />}>
<LatestNews />
</Suspense>
</div>
);
}Streaming 实现
tsx
// app/blog/[slug]/page.tsx
import { Suspense } from 'react';
async function BlogPost({ params }) {
const post = await getPost(params.slug);
return (
<article>
<header>
<h1>{post.title}</h1>
<p>{post.author}</p>
</header>
{/* 文章内容可以立即加载 */}
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{/* 评论可能较慢,使用 Suspense */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments postId={post.id} />
</Suspense>
{/* 相关推荐 */}
<Suspense fallback={<RecommendationsSkeleton />}>
<RelatedPosts category={post.category} />
</Suspense>
</article>
);
}Skeleton 组件
tsx
// app/components/ProductSkeleton.tsx
function ProductSkeleton() {
return (
<div className="animate-pulse">
<div className="aspect-video bg-gray-200 rounded" />
<div className="h-4 bg-gray-200 rounded mt-2" />
<div className="h-4 bg-gray-200 rounded w-3/4 mt-2" />
</div>
);
}
// loading.tsx - 路由级别的加载状态
// app/products/loading.tsx
export default function ProductsLoading() {
return (
<div className="grid grid-cols-4 gap-4">
{Array.from({ length: 8 }).map((_, i) => (
<ProductSkeleton key={i} />
))}
</div>
);
}数据获取模式
服务端直接查询
tsx
// 最直接的数据获取
async function ProductList() {
const products = await db.query(`
SELECT * FROM products
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 20
`);
return <ProductGrid products={products} />;
}fetch 扩展
tsx
// Next.js 扩展了 fetch,自动缓存
async function Component() {
// 默认缓存
const data1 = await fetch('https://api.example.com/data');
// 重新验证:每 60 秒
const data2 = await fetch('https://api.example.com/data', {
next: { revalidate: 60 },
});
// 动态:每次都获取最新
const data3 = await fetch('https://api.example.com/data', {
cache: 'no-store',
});
return <div>{/* ... */}</div>;
}use() Hook(客户端数据获取)
tsx
'use client';
import { use } from 'react';
function CartItems() {
// 在 Client Component 中使用 Promise
const cart = use(fetchCart());
return (
<ul>
{cart.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}状态共享
Server Component 间的状态
tsx
// 使用 context(仅 Server Component 中)
// app/context.ts
import { createContext } from 'react';
export const ThemeContext = createContext('light');
// app/layout.tsx
export default function Layout({ children }) {
return (
<ThemeContext.Provider value="dark">
{children}
</ThemeContext.Provider>
);
}
// app/page.tsx
import { ThemeContext } from '../context';
async function Page() {
// Server Component 中使用 context
return (
<ThemeContext.Provider value="light">
<Content />
</ThemeContext.Provider>
);
}Client Component 状态共享
tsx
// context/providers.tsx
'use client';
import { createContext } from 'react';
export const CartContext = createContext(null);
export function CartProvider({ children }) {
const [items, setItems] = useState([]);
const addItem = (product) => {
setItems(prev => [...prev, product]);
};
return (
<CartContext.Provider value={{ items, addItem }}>
{children}
</CartContext.Provider>
);
}
// app/layout.tsx
import { CartProvider } from './context/providers';
export default function RootLayout({ children }) {
return (
<html>
<body>
<CartProvider>
{children}
</CartProvider>
</body>
</html>
);
}错误处理
error.tsx
tsx
// app/products/error.tsx
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// 上报错误
console.error(error);
}, [error]);
return (
<div>
<h2>出错了!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>
重试
</button>
</div>
);
}notFound()
tsx
// app/products/[id]/page.tsx
import { notFound } from 'next/navigation';
async function ProductPage({ params }) {
const product = await getProduct(params.id);
if (!product) {
notFound();
}
return <ProductDetail product={product} />;
}
// app/products/not-found.tsx
export default function ProductNotFound() {
return (
<div>
<h1>商品未找到</h1>
<p>抱歉,您查找的商品不存在。</p>
</div>
);
}最佳实践
组件边界划分
plaintext
┌─────────────────────────────────────────────┐
│ Server Components │
│ ┌───────────────────────────────────────┐ │
│ │ 布局结构、数据获取、静态内容 │ │
│ │ - Layout, Page │ │
│ │ - 数据密集型组件 │ │
│ │ - 直接访问 DB/FS/API │ │
│ └───────────────────────────────────────┘ │
│ ↓ props │
│ ┌───────────────────────────────────────┐ │
│ │ Client Components │ │
│ │ 交互逻辑、状态管理、浏览器 API │ │
│ │ - Button, Input, Modal │ │
│ │ - useState, useEffect, useContext │ │
│ │ - 事件处理器 │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘性能优化建议
-
保持 Server Component 为主
tsx// ✓ 优先使用 Server Component async function Page() { return <ExpensiveComponent />; } // ❌ 过度使用 Client Component async function Page() { return <ClientWrapper><HeavyDataComponent /></ClientWrapper>; } -
减少 Client Component 数量
tsx// ❌ 把整个列表变成 Client Component 'use client'; async function ProductList() { const products = await db.products.findMany(); return <ul>{/* ... */}</ul>; } // ✓ 只将交互部分设为 Client Component async function ProductList() { const products = await db.products.findMany(); return ( <ul> {products.map(p => ( <li key={p.id}> {p.name} <AddToCartButton productId={p.id} /> {/* 只有这里是 client */} </li> ))} </ul> ); } -
避免 prop drilling
tsx// 使用 Context 传递数据,避免深层 prop drilling // app/providers.tsx 'use client'; export function Providers({ children }) { return <SomeContext.Provider>{children}</SomeContext.Provider>; }
小结
- Server Components:仅在服务端运行,可直接访问数据库和文件系统,无交互能力
- Client Components:使用
'use client'声明,可在客户端执行交互和状态管理 - 混用原则:Server Component 为主,需要交互的部分用 Client Component
- App Router:Next.js 13+ 的新路由系统,原生支持 RSC
- Streaming:
Suspense实现流式渲染,提供渐进式加载体验 - 最佳实践:保持 Server Component 为主,只将必要的交互部分设为 Client Component
RSC 代表了 React 的未来方向,通过合理的组件边界划分,可以显著提升应用性能并改善开发者体验。
#react
#rsc
#ssr
#nextjs
#服务端渲染
评论
A
Written by
AI-Writer