React Server Components实战:Next.js App Router数据获取与流式渲染机制详解

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>
    );
}

使用searchParamscookies()headers()等动态API的页面自动切换为动态渲染。静态生成和ISR适合内容不频繁变更的页面(博客文章、产品详情),动态渲染适合用户个性化内容(搜索结果、个人主页)。

按需重新验证通过revalidatePathrevalidateTag实现。当文章通过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/

(0)
小编小编
上一篇 1小时前
下一篇 1小时前

相关推荐