React Server Components与客户端组件的本质区别
React Server Components(RSC)是React 18引入的渲染模型,将组件分为Server Components和Client Components两类。Server Components在服务端渲染,输出序列化的React树,不包含任何JavaScript代码,不会增加客户端bundle体积。Client Components通过"use client"指令声明,渲染逻辑在服务端和客户端同时执行,包含交互逻辑(onClick、useState等)。
Next.js App Router基于RSC构建,默认所有组件为Server Components,仅在需要交互时添加"use client"。这种模型将数据获取移至服务端,避免了传统SPA中客户端获取数据的瀑布式请求问题。Server Components可直接访问数据库、文件系统和后端API,无需额外构建API层。
App Router路由结构与数据获取
Next.js App Router使用文件系统路由,app目录下的page.tsx定义页面,layout.tsx定义布局,loading.tsx定义加载状态,error.tsx定义错误边界。
// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import { Suspense } from "react";
import { db } from "@/lib/db";
// Server Component: 服务端获取数据
async function getPost(slug: string) {
const post = await db.query.posts.findFirst({
where: (posts, { eq }) => eq(posts.slug, slug),
});
if (!post) notFound();
return post;
}
async function getComments(postId: number) {
return db.query.comments.findMany({
where: (comments, { eq }) => eq(comments.postId, postId),
limit: 20,
});
}
// 页面组件: 并行数据获取
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{/* Suspense包裹异步组件实现流式渲染 */}
<Suspense fallback={<CommentSkeleton />}>
<CommentsSection postId={post.id} />
</Suspense>
</article>
);
}
// 评论区域: 独立异步组件,被Suspense包裹后流式渲染
async function CommentsSection({ postId }: { postId: number }) {
const comments = await getComments(postId);
return (
<section>
<h2>评论 ({comments.length})</h2>
{comments.map((c) => (
<CommentItem key={c.id} comment={c} />
))}
</section>
);
}
// Client Component: 交互逻辑
"use client";
function LikeButton({ postId }: { postId: number }) {
const [liked, setLiked] = useState(false);
const [count, setCount] = useState(0);
const handleClick = async () => {
setLiked(!liked);
setCount(c => c + (liked ? -1 : 1));
await fetch(`/api/posts/${postId}/like`, { method: "POST" });
};
return (
<button onClick={handleClick}>
{count} 赞
</button>
);
}
Suspense配合Server Components实现流式渲染:页面主体内容立即返回,被Suspense包裹的异步组件在数据就绪后通过流式响应逐步填充。用户无需等待所有数据加载完成即可看到页面骨架和已有内容。
Server Actions与表单提交
Server Actions是Next.js 14引入的特性,允许在Server Components中直接定义服务端执行函数,通过HTTP POST请求触发,无需编写API路由。表单提交场景下,Server Actions替代传统的fetch + API route模式,减少代码量和状态管理复杂度。
// app/blog/[slug]/page.tsx
import { revalidatePath } from "next/cache";
// Server Action: 服务端表单处理
async function submitComment(formData: FormData) {
"use server";
const postId = Number(formData.get("postId"));
const content = formData.get("content") as string;
const author = formData.get("author") as string;
if (!content || content.trim().length < 1) {
return { error: "评论内容不能为空" };
}
await db.insert(comments).values({
postId,
content: content.trim(),
author,
createdAt: new Date(),
});
// 触发页面ISR重新生成
revalidatePath(`/blog/${slug}`);
return { success: true };
}
// 表单组件
export default async function CommentForm({ postId }: { postId: number }) {
return (
<form action={submitComment}>
<input type="hidden" name="postId" value={postId} />
<input
type="text"
name="author"
placeholder="昵称"
required
className="border p-2 rounded"
/>
<textarea
name="content"
placeholder="写下你的评论..."
required
className="border p-2 rounded w-full"
rows={4}
/>
<button type="submit">
提交评论
</button>
</form>
);
}
"use server"指令标记函数为Server Action,Next.js自动生成对应的HTTP端点和安全校验逻辑。表单使用action={submitComment}替代onSubmit,提交时浏览器发送POST请求到Server Action端点。表单在无JavaScript的情况下也能工作(渐进增强),因为action属性回退为标准HTML表单提交。
缓存策略与ISR按需重新验证
Next.js App Router提供四级缓存机制:Request Memoization(单次请求内去重)、Data Cache(跨请求数据缓存)、Full Route Cache(路由级缓存)、Router Cache(客户端路由缓存)。Server Components中获取的数据默认被缓存,通过配置控制缓存行为:
// app/blog/page.tsx
// 方式1: 静态生成 + 定时ISR
export const revalidate = 3600; // 1小时后重新生成
// 方式2: 动态渲染(每次请求都重新获取)
export const dynamic = "force-dynamic";
// 方式3: 搜索参数驱动的动态页面
export default async function BlogList({
searchParams,
}: {
searchParams: Promise<{ page?: string; tag?: string }>;
}) {
const { page = "1", tag } = await searchParams;
const posts = await db.query.posts.findMany({
where: tag ? (posts, { eq }) => eq(posts.tag, tag) : undefined,
limit: 10,
offset: (Number(page) - 1) * 10,
});
return (
<div>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
使用searchParams、cookies()、headers()等动态API的页面自动切换为动态渲染。静态生成和ISR适合内容不频繁变更的页面(博客文章、产品详情),动态渲染适合用户个性化内容(搜索结果、个人主页)。
按需重新验证通过revalidatePath和revalidateTag实现。当文章通过CMS发布或评论提交时,触发对应路径的缓存失效:
// 获取数据时打标签
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] },
});
return res.json();
}
// 按标签批量失效缓存
import { revalidateTag } from "next/cache";
async function publishPost(formData: FormData) {
"use server";
await db.insert(posts).values({
title: formData.get("title"),
content: formData.get("content"),
});
// 失效所有标记为"posts"的缓存数据
revalidateTag("posts");
// 失效特定路径
revalidatePath("/blog");
revalidatePath("/blog/[slug]", "page");
}
Server与Client组件边界划分
RSC的核心约束是Server Components不能向Client Components传递不可序列化的数据(如函数、Class实例、Date对象等)。组件边界的划分原则:交互逻辑和浏览器API在Client Components中,数据获取和静态渲染在Server Components中。两者通过props传递可序列化数据。
// Server Component - 获取数据
async function ProductList() {
const products = await getProducts();
return (
<div>
{products.map((p) => (
// 将序列化数据传递给Client Component
<ProductCard
key={p.id}
product={p}
// 不能传Date对象,传ISO字符串
createdAt={p.createdAt.toISOString()}
/>
))}
</div>
);
}
// Client Component - 交互逻辑
"use client";
function ProductCard({
product,
createdAt,
}: {
product: Product;
createdAt: string;
}) {
const [isFavorited, setIsFavorited] = useState(false);
// 浏览器API在Client Component中使用
const handleClick = () => {
localStorage.setItem(`fav-${product.id}`, "true");
setIsFavorited(true);
};
return (
<div onClick={handleClick}>
<h3>{product.name}</h3>
<span>{new Date(createdAt).toLocaleDateString()}</span>
{isFavorited && <span>已收藏</span>}
</div>
);
}
传Date对象会触发序列化错误,需转为ISO字符串在Client Component中重新构造。Map、Set等非JSON原生类型同样需要转换。Context Provider必须放在Client Component中,但Consumer可以在Server Components中通过children prop穿透接收。这种模式称为”composition pattern”,避免将整棵组件树标记为Client Component。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/reactservercomponents-shi-zhan-nextjsapprouter-shu-ju-huo/