Skip to content

server_client_components.tsx

文件信息

  • 📄 原文件:02_server_client_components.tsx
  • 🔤 语言:TypeScript (Next.js / React)

Next.js App Router 引入了 React Server Components(RSC),从根本上改变了组件的渲染方式和数据获取模式。理解 Server Component 和 Client Component 的边界是掌握 App Router 的关键。

完整代码

tsx
/**
 * ============================================================
 *            Next.js 服务端组件与客户端组件
 * ============================================================
 * Next.js App Router 引入了 React Server Components(RSC),
 * 从根本上改变了组件的渲染方式和数据获取模式。
 * 理解 Server Component 和 Client Component 的边界是掌握
 * App Router 的关键。
 *
 * 适用版本:Next.js 14 / 15 (App Router)
 * ============================================================
 */

import { Suspense } from 'react';
import Link from 'next/link';
import { cookies, headers } from 'next/headers';
import { notFound } from 'next/navigation';

// ============================================================
//                    1. 服务端组件基础
// ============================================================

/**
 * 【什么是 Server Component】
 *
 * 在 App Router 中,所有组件默认是 Server Component:
 * - 在服务器上渲染,不会被发送到客户端
 * - 可以直接访问后端资源(数据库、文件系统、环境变量)
 * - 可以使用 async/await 直接获取数据
 * - 不会增加客户端 JavaScript 包体积
 * - 不能使用浏览器 API(window、document)
 * - 不能使用 React Hook(useState、useEffect 等)
 * - 不能使用事件处理器(onClick、onChange 等)
 *
 * 【Server Component 的优势】
 *
 * 1. 零客户端 JavaScript
 *    → 组件代码不发送到浏览器,减少包体积
 * 2. 直接访问后端
 *    → 无需创建 API 路由,直接查询数据库
 * 3. 自动代码分割
 *    → 每个 Server Component 自动进行代码分割
 * 4. 流式渲染
 *    → 可以逐步发送 HTML,提升首屏速度
 * 5. 安全性
 *    → 敏感逻辑和密钥留在服务端,不会泄露
 */

// --- 基本的 Server Component ---
// app/users/page.tsx(默认就是 Server Component,无需声明)
async function UsersPage() {
    // 直接在组件中获取数据 — 这在客户端组件中不可能做到
    const users = await fetch('https://api.example.com/users', {
        // Next.js 扩展的 fetch 选项
        cache: 'force-cache',  // 静态数据:构建时获取并缓存
    }).then(r => r.json());

    return (
        <div>
            <h1>用户列表</h1>
            <ul>
                {users.map((user: { id: number; name: string }) => (
                    <li key={user.id}>{user.name}</li>
                ))}
            </ul>
        </div>
    );
}

// --- 直接访问数据库 ---
// 在 Server Component 中可以直接查询数据库
async function ProductsPage() {
    // 模拟直接数据库查询(如 Prisma、Drizzle 等 ORM)
    // const products = await db.product.findMany();
    const products = await getProductsFromDB();

    return (
        <div>
            <h1>商品列表</h1>
            {products.map((product) => (
                <div key={product.id}>
                    <h2>{product.name}</h2>
                    <p>价格: ¥{product.price}</p>
                </div>
            ))}
        </div>
    );
}

// --- 访问服务端专有 API ---
async function ServerOnlyPage() {
    // 读取 HTTP 请求头
    const headersList = await headers();
    const userAgent = headersList.get('user-agent') || '';

    // 读取 Cookie
    const cookieStore = await cookies();
    const theme = cookieStore.get('theme')?.value || 'light';

    // 使用环境变量(服务端专用,不会泄露到客户端)
    const apiSecret = process.env.API_SECRET_KEY;

    return (
        <div>
            <p>用户代理: {userAgent.slice(0, 50)}...</p>
            <p>主题偏好: {theme}</p>
            {/* apiSecret 永远不会出现在客户端 */}
        </div>
    );
}


// ============================================================
//                    2. 客户端组件
// ============================================================

/**
 * 【什么是 Client Component】
 *
 * 通过在文件顶部添加 'use client' 指令声明客户端组件:
 * - 在客户端(浏览器)上执行交互逻辑
 * - 可以使用 React Hook(useState、useEffect 等)
 * - 可以使用事件处理器(onClick、onChange 等)
 * - 可以访问浏览器 API(window、localStorage 等)
 * - 仍然会在服务端进行初始 HTML 渲染(SSR)
 *
 * 【'use client' 的工作原理】
 *
 * 'use client' 是一个声明"入口点"的指令:
 * - 标记了服务端和客户端的边界
 * - 该文件及其所有导入的模块都成为客户端代码
 * - 不需要在每个客户端组件文件中都添加
 * - 只需在边界入口点添加即可
 *
 * 【注意】
 * Client Component 并不意味着"只在客户端渲染":
 * - 初始 HTML 仍然在服务端生成(SSR)
 * - 然后在客户端进行"水合"(hydration)
 * - 水合后才能响应用户交互
 */

// --- 客户端组件基础 ---
// components/Counter.tsx
// 'use client';  // 实际文件中需要放在最顶部

function Counter() {
    // useState 只能在客户端组件中使用
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>计数: {count}</p>
            {/* onClick 事件处理器需要客户端组件 */}
            <button onClick={() => setCount(count + 1)}>+1</button>
            <button onClick={() => setCount(count - 1)}>-1</button>
        </div>
    );
}

// --- 需要浏览器 API 的组件 ---
// components/ThemeToggle.tsx
// 'use client';

function ThemeToggle() {
    const [theme, setTheme] = useState<'light' | 'dark'>('light');

    // useEffect 只能在客户端组件中使用
    useEffect(() => {
        // 访问 localStorage — 浏览器 API
        const saved = localStorage.getItem('theme');
        if (saved === 'light' || saved === 'dark') {
            setTheme(saved);
        }
    }, []);

    function toggleTheme() {
        const newTheme = theme === 'light' ? 'dark' : 'light';
        setTheme(newTheme);
        localStorage.setItem('theme', newTheme);
        // 修改 DOM — 浏览器 API
        document.documentElement.classList.toggle('dark', newTheme === 'dark');
    }

    return (
        <button onClick={toggleTheme}>
            当前主题: {theme === 'light' ? '浅色' : '深色'}
        </button>
    );
}

// --- 表单交互组件 ---
// components/SearchInput.tsx
// 'use client';

function SearchInput({ onSearch }: { onSearch?: (query: string) => void }) {
    const [query, setQuery] = useState('');
    const [results, setResults] = useState<string[]>([]);

    // 防抖搜索
    useEffect(() => {
        if (!query.trim()) {
            setResults([]);
            return;
        }

        const timer = setTimeout(async () => {
            const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
            const data = await res.json();
            setResults(data.results);
        }, 300);

        return () => clearTimeout(timer);
    }, [query]);

    return (
        <div>
            <input
                value={query}
                onChange={(e) => setQuery(e.target.value)}
                placeholder="搜索..."
            />
            <ul>
                {results.map((result, i) => (
                    <li key={i}>{result}</li>
                ))}
            </ul>
        </div>
    );
}


// ============================================================
//                    3. 渲染模式对比
// ============================================================

/**
 * 【Next.js 渲染模式全景】
 *
 * 1. SSG(静态站点生成 — Static Site Generation)
 *    - 构建时生成 HTML
 *    - 适用于不经常变化的内容(博客、文档)
 *    - App Router 中:默认行为(当没有动态数据时)
 *    - 最快的加载速度,可部署到 CDN
 *
 * 2. SSR(服务端渲染 — Server-Side Rendering)
 *    - 每次请求时在服务端生成 HTML
 *    - 适用于个性化内容(仪表盘、用户首页)
 *    - App Router 中:使用动态函数(cookies、headers)或
 *      设置 { cache: 'no-store' } 时自动启用
 *
 * 3. CSR(客户端渲染 — Client-Side Rendering)
 *    - 在浏览器中渲染(传统 SPA 模式)
 *    - 适用于高度交互的组件
 *    - App Router 中:Client Component + useEffect 获取数据
 *
 * 4. ISR(增量静态再生 — Incremental Static Regeneration)
 *    - 静态生成 + 定时重新验证
 *    - App Router 中:fetch 的 next.revalidate 选项
 *
 * 5. Streaming(流式渲染)
 *    - 逐步发送 HTML 到客户端
 *    - 配合 Suspense 使用,先显示骨架再填充内容
 *    - App Router 的核心特性之一
 *
 * 【App Router 中的缓存策略】
 *
 * fetch('url', { cache: 'force-cache' })     → 静态(SSG)
 * fetch('url', { cache: 'no-store' })         → 动态(SSR)
 * fetch('url', { next: { revalidate: 60 } })  → ISR(60秒重新验证)
 */

// --- SSG: 静态生成 ---
// 没有动态数据的页面自动成为静态页面
async function StaticPage() {
    // force-cache 是默认行为,构建时获取数据
    const data = await fetch('https://api.example.com/static-content', {
        cache: 'force-cache',
    }).then(r => r.json());

    return <div>{data.content}</div>;
}

// --- SSR: 每次请求动态渲染 ---
async function DynamicPage() {
    // no-store 表示每次请求都重新获取
    const data = await fetch('https://api.example.com/realtime-data', {
        cache: 'no-store',
    }).then(r => r.json());

    // 或者使用动态函数(自动触发动态渲染)
    const cookieStore = await cookies();
    const userId = cookieStore.get('userId')?.value;

    return <div>用户 {userId} 的数据: {JSON.stringify(data)}</div>;
}

// --- ISR: 增量静态再生 ---
async function ISRPage() {
    // 每 60 秒重新验证一次
    const data = await fetch('https://api.example.com/products', {
        next: { revalidate: 60 },
    }).then(r => r.json());

    return (
        <div>
            <p>数据每 60 秒更新一次</p>
            <pre>{JSON.stringify(data, null, 2)}</pre>
        </div>
    );
}

// --- CSR: 客户端渲染 ---
// 'use client';
function CSRComponent() {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        fetch('/api/client-data')
            .then(r => r.json())
            .then(d => {
                setData(d);
                setLoading(false);
            });
    }, []);

    if (loading) return <p>加载中...</p>;

    return <div>{JSON.stringify(data)}</div>;
}


// ============================================================
//                    4. 组件组合模式
// ============================================================

/**
 * 【Server 包裹 Client 模式】
 *
 * 最常见的组合模式:Server Component 作为父组件,
 * 在服务端获取数据后通过 props 传递给 Client Component。
 *
 * Server Component 可以:
 * - 导入和渲染 Client Component ✅
 * - 将可序列化的数据作为 props 传递 ✅
 * - 将 Server Component 作为 children 传递给 Client Component ✅
 *
 * Client Component 不能:
 * - 直接导入 Server Component ❌
 *   (但可以通过 children 或其他 ReactNode prop 接收)
 *
 * 【组合模式的意义】
 *
 * 将交互逻辑下推到叶子节点(Leaf),
 * 让尽可能多的组件树保持在服务端,
 * 只在真正需要交互的地方使用 Client Component。
 */

// --- 模式1:Server 获取数据,Client 处理交互 ---

// Server Component(无需 'use client')
async function ProductList() {
    // 在服务端获取数据
    const products = await fetch('https://api.example.com/products', {
        next: { revalidate: 3600 },
    }).then(r => r.json());

    return (
        <div>
            <h1>商品列表</h1>
            {/* 将数据传递给客户端组件 */}
            {products.map((product: Product) => (
                <ProductCard key={product.id} product={product} />
            ))}
        </div>
    );
}

// Client Component — 处理交互逻辑
// 'use client';
function ProductCard({ product }: { product: Product }) {
    const [isWished, setIsWished] = useState(false);

    return (
        <div className="product-card">
            <h3>{product.name}</h3>
            <p>¥{product.price}</p>
            <button onClick={() => setIsWished(!isWished)}>
                {isWished ? '已收藏' : '收藏'}
            </button>
        </div>
    );
}

// --- 模式2:通过 children 传递 Server Component ---

// Client Component — 提供交互式布局
// 'use client';
function InteractivePanel({ children, title }: {
    children: React.ReactNode;
    title: string;
}) {
    const [isExpanded, setIsExpanded] = useState(true);

    return (
        <div className="panel">
            <div
                className="panel-header"
                onClick={() => setIsExpanded(!isExpanded)}
            >
                <h2>{title}</h2>
                <span>{isExpanded ? '收起' : '展开'}</span>
            </div>

            {/* children 可以是 Server Component! */}
            {isExpanded && (
                <div className="panel-content">
                    {children}
                </div>
            )}
        </div>
    );
}

// Server Component — 在服务端获取和渲染
async function ServerContent() {
    const data = await fetch('https://api.example.com/content').then(r => r.json());
    return <div>{data.html}</div>;
}

// 组合使用(在 Server Component 中)
function PageWithPanel() {
    return (
        <InteractivePanel title="服务端内容面板">
            {/* ServerContent 是 Server Component */}
            {/* 通过 children 传入 Client Component 中 */}
            <ServerContent />
        </InteractivePanel>
    );
}

// --- 模式3:多个 slot 组合 ---

// Client Component — 带标签页的容器
// 'use client';
function TabContainer({
    tabs,
}: {
    tabs: { label: string; content: React.ReactNode }[];
}) {
    const [activeTab, setActiveTab] = useState(0);

    return (
        <div>
            <div className="tab-bar">
                {tabs.map((tab, index) => (
                    <button
                        key={index}
                        className={activeTab === index ? 'active' : ''}
                        onClick={() => setActiveTab(index)}
                    >
                        {tab.label}
                    </button>
                ))}
            </div>
            <div className="tab-content">
                {tabs[activeTab].content}
            </div>
        </div>
    );
}

// Server Component — 组合使用
async function DashboardPage() {
    return (
        <TabContainer
            tabs={[
                {
                    label: '概览',
                    content: <OverviewPanel />,   // Server Component
                },
                {
                    label: '统计',
                    content: <StatsPanel />,       // Server Component
                },
            ]}
        />
    );
}


// ============================================================
//                    5. 数据序列化
// ============================================================

/**
 * 【可序列化 Props 限制】
 *
 * Server Component 传递给 Client Component 的 props
 * 必须是可序列化的(能被 JSON.stringify 处理)。
 *
 * ✅ 可以传递:
 * - 基本类型:string, number, boolean, null, undefined
 * - 纯对象和数组(值也需要可序列化)
 * - Date 对象(会被序列化为字符串)
 * - Map 和 Set(React 支持的序列化类型)
 * - TypedArray 和 ArrayBuffer
 * - React 元素(JSX)/ ReactNode
 * - Promise(配合 use() Hook)
 *
 * ❌ 不能传递:
 * - 函数 / 方法
 * - 类实例(会丢失原型链和方法)
 * - DOM 节点
 * - Symbol
 * - 循环引用的对象
 *
 * 【常见错误与解决方案】
 *
 * 错误:将回调函数作为 prop 传递给 Client Component
 * 解决:使用 Server Action 替代,或在客户端定义函数
 */

// --- 正确:传递可序列化数据 ---

interface UserData {
    id: number;
    name: string;
    email: string;
    createdAt: string;  // Date 序列化为字符串
}

// Server Component
async function UserProfile() {
    const user: UserData = await fetchUser(1);

    // ✅ 传递纯数据对象
    return <UserCard user={user} />;
}

// Client Component
// 'use client';
function UserCard({ user }: { user: UserData }) {
    const [isEditing, setIsEditing] = useState(false);
    return (
        <div>
            <h2>{user.name}</h2>
            <p>{user.email}</p>
            <button onClick={() => setIsEditing(!isEditing)}>
                {isEditing ? '取消' : '编辑'}
            </button>
        </div>
    );
}

// --- 错误示范:不能传递函数 ---

/**
 * ❌ 以下代码会报错:
 *
 * // Server Component
 * function ParentServer() {
 *     function handleClick() {
 *         console.log('clicked');
 *     }
 *     // 错误!不能将函数传递给 Client Component
 *     return <ChildClient onClick={handleClick} />;
 * }
 *
 * ✅ 正确做法 — 使用 Server Action:
 */

// Server Component + Server Action
async function FormPage() {
    // Server Action — 在服务端执行的函数
    async function submitForm(formData: FormData) {
        'use server';
        const name = formData.get('name') as string;
        // 直接操作数据库
        // await db.user.create({ data: { name } });
        console.log('服务端处理:', name);
    }

    return (
        <form action={submitForm}>
            <input name="name" placeholder="用户名" />
            <SubmitButton />
        </form>
    );
}

// Client Component — 处理表单交互状态
// 'use client';
function SubmitButton() {
    // useFormStatus 需要在 <form> 内的 Client Component 中使用
    // const { pending } = useFormStatus();

    return (
        <button type="submit">
            提交
        </button>
    );
}

// --- 传递 React 元素(可序列化) ---

// Server Component
async function CardWithActions() {
    const data = await fetchData();

    // ✅ JSX 元素是可序列化的
    return (
        <ClientWrapper
            header={<h1>{data.title}</h1>}
            footer={<p>最后更新: {data.updatedAt}</p>}
        >
            <p>{data.description}</p>
        </ClientWrapper>
    );
}

// Client Component
// 'use client';
function ClientWrapper({
    header,
    footer,
    children,
}: {
    header: React.ReactNode;
    footer: React.ReactNode;
    children: React.ReactNode;
}) {
    const [showFooter, setShowFooter] = useState(true);

    return (
        <div className="card">
            {header}
            {children}
            <button onClick={() => setShowFooter(!showFooter)}>
                {showFooter ? '隐藏' : '显示'}页脚
            </button>
            {showFooter && footer}
        </div>
    );
}


// ============================================================
//                    6. 流式渲染
// ============================================================

/**
 * 【流式渲染(Streaming SSR)】
 *
 * 传统 SSR:服务端必须完成所有数据获取后,
 * 才能发送完整的 HTML → 用户看到白屏等待。
 *
 * 流式渲染:服务端可以逐步发送 HTML 片段:
 * 1. 先发送页面骨架(布局、静态内容)
 * 2. 异步数据就绪后,发送对应的 HTML 片段
 * 3. 客户端逐步填充内容,用户更早看到页面
 *
 * 【Suspense Boundary】
 *
 * React <Suspense> 是流式渲染的关键:
 * - 包裹异步组件
 * - fallback 属性提供加载占位符
 * - 异步内容就绪后自动替换占位符
 * - 可以嵌套多层,实现精细化的加载控制
 *
 * 【loading.tsx 与 Suspense 的关系】
 *
 * loading.tsx 本质上就是自动创建的 Suspense boundary:
 *
 * // Next.js 内部等价于:
 * <Layout>
 *   <Suspense fallback={<Loading />}>
 *     <Page />
 *   </Suspense>
 * </Layout>
 *
 * 手动使用 <Suspense> 可以实现更精细的控制。
 */

// --- 基本流式渲染 ---
// app/dashboard/page.tsx
async function StreamingDashboard() {
    return (
        <div className="dashboard">
            <h1>仪表盘</h1>

            {/* 快速内容:立即显示 */}
            <WelcomeMessage />

            {/* 慢速内容:包裹在 Suspense 中,逐步加载 */}
            <Suspense fallback={<p>加载统计数据...</p>}>
                <SlowStatistics />
            </Suspense>

            <Suspense fallback={<p>加载最近活动...</p>}>
                <SlowRecentActivity />
            </Suspense>

            <Suspense fallback={<p>加载推荐内容...</p>}>
                <SlowRecommendations />
            </Suspense>
        </div>
    );
}

// 快速组件 — 不需要异步数据
function WelcomeMessage() {
    return <p>欢迎回来!以下是您的仪表盘概览。</p>;
}

// 慢速组件 — 需要从 API 获取数据
async function SlowStatistics() {
    // 模拟慢速 API 调用
    const stats = await fetch('https://api.example.com/stats', {
        cache: 'no-store',
    }).then(r => r.json());

    return (
        <div className="stats-grid">
            <div>总用户: {stats.totalUsers}</div>
            <div>今日活跃: {stats.dailyActive}</div>
            <div>收入: ¥{stats.revenue}</div>
        </div>
    );
}

async function SlowRecentActivity() {
    const activities = await fetch('https://api.example.com/activities').then(r => r.json());

    return (
        <ul>
            {activities.map((a: { id: number; text: string }) => (
                <li key={a.id}>{a.text}</li>
            ))}
        </ul>
    );
}

async function SlowRecommendations() {
    const recs = await fetch('https://api.example.com/recommendations').then(r => r.json());

    return (
        <div className="recommendations">
            {recs.map((r: { id: number; title: string }) => (
                <div key={r.id}>{r.title}</div>
            ))}
        </div>
    );
}

// --- 嵌套 Suspense(细粒度控制)---
async function ProductDetailPage({
    params,
}: {
    params: Promise<{ id: string }>;
}) {
    const { id } = await params;

    return (
        <div>
            {/* 第一层:产品基本信息先加载 */}
            <Suspense fallback={<ProductSkeleton />}>
                <ProductInfo id={id} />

                {/* 第二层:评论在产品信息之后加载 */}
                <Suspense fallback={<p>加载评论...</p>}>
                    <ProductReviews id={id} />
                </Suspense>

                {/* 第二层:推荐商品独立加载 */}
                <Suspense fallback={<p>加载推荐...</p>}>
                    <RelatedProducts id={id} />
                </Suspense>
            </Suspense>
        </div>
    );
}

function ProductSkeleton() {
    return (
        <div className="skeleton">
            <div className="skeleton-image" />
            <div className="skeleton-title" />
            <div className="skeleton-price" />
        </div>
    );
}


// ============================================================
//                    7. 选择策略
// ============================================================

/**
 * 【何时使用 Server Component】
 *
 * 使用场景:
 * - 数据获取(直接查询数据库或调用 API)
 * - 访问后端资源(文件系统、环境变量)
 * - 保护敏感信息(API 密钥、数据库连接)
 * - 渲染大量静态内容(减少客户端 JS)
 * - 使用服务端专用库(如 Node.js 原生模块)
 *
 * 【何时使用 Client Component】
 *
 * 使用场景:
 * - 需要用户交互(onClick、onChange 等事件)
 * - 需要状态管理(useState、useReducer)
 * - 需要生命周期/副作用(useEffect)
 * - 需要浏览器 API(localStorage、geolocation)
 * - 使用依赖状态/效果的自定义 Hook
 * - 使用 React Class Component
 *
 * 【决策流程图】
 *
 * 组件是否需要交互?
 * ├── 否 → Server Component ✅
 * └── 是 → 能否将交互部分提取为子组件?
 *     ├── 能 → 父组件 Server + 子组件 Client ✅
 *     └── 不能 → Client Component ✅
 *
 * 【常见组件的选择建议】
 *
 * | 组件类型       | 推荐     | 原因                   |
 * |---------------|---------|----------------------|
 * | 页面布局       | Server  | 纯展示,无需交互          |
 * | 导航栏         | Client  | 可能有菜单展开、活跃状态    |
 * | 数据表格       | Server  | 服务端获取并渲染          |
 * | 排序/筛选按钮   | Client  | 需要交互                |
 * | 文章内容       | Server  | 大量静态内容             |
 * | 评论表单       | Client  | 需要输入和提交           |
 * | 侧边栏         | 混合    | 布局 Server + 折叠 Client |
 * | 图片轮播       | Client  | 需要滑动交互             |
 * | SEO 元数据     | Server  | 服务端生成              |
 */

// --- 示例:混合组件设计 ---

// Server Component — 文章页面
async function ArticlePage({
    params,
}: {
    params: Promise<{ slug: string }>;
}) {
    const { slug } = await params;
    const article = await fetchArticle(slug);

    if (!article) notFound();

    return (
        <article>
            {/* 静态内容 — Server Component */}
            <h1>{article.title}</h1>
            <p className="meta">
                作者: {article.author} | 发布: {article.date}
            </p>
            <div className="content">{article.content}</div>

            {/* 交互部分 — Client Component */}
            <LikeButton articleId={article.id} initialCount={article.likes} />

            {/* 评论区 — Server 获取数据 + Client 处理交互 */}
            <Suspense fallback={<p>加载评论...</p>}>
                <CommentsSection articleId={article.id} />
            </Suspense>
        </article>
    );
}

// Client Component — 点赞按钮
// 'use client';
function LikeButton({
    articleId,
    initialCount,
}: {
    articleId: number;
    initialCount: number;
}) {
    const [likes, setLikes] = useState(initialCount);
    const [liked, setLiked] = useState(false);

    async function handleLike() {
        setLiked(!liked);
        setLikes(prev => liked ? prev - 1 : prev + 1);

        // 调用 API
        await fetch(`/api/articles/${articleId}/like`, {
            method: 'POST',
        });
    }

    return (
        <button onClick={handleLike} className={liked ? 'liked' : ''}>
            {liked ? '已赞' : '点赞'} ({likes})
        </button>
    );
}

// Server Component — 获取评论数据
async function CommentsSection({ articleId }: { articleId: number }) {
    const comments = await fetch(
        `https://api.example.com/articles/${articleId}/comments`,
        { cache: 'no-store' }
    ).then(r => r.json());

    return (
        <div className="comments">
            <h3>评论 ({comments.length})</h3>
            {comments.map((c: Comment) => (
                <div key={c.id} className="comment">
                    <strong>{c.author}</strong>
                    <p>{c.text}</p>
                </div>
            ))}
            {/* 评论表单需要客户端交互 */}
            <CommentForm articleId={articleId} />
        </div>
    );
}


// ============================================================
//                    8. 最佳实践
// ============================================================

/**
 * 【Server Component 与 Client Component 最佳实践】
 *
 * ✅ 推荐做法:
 * 1. 默认使用 Server Component,只在需要交互时才使用 Client Component
 * 2. 将交互逻辑下推到组件树的叶子节点
 * 3. 通过 children/ReactNode props 在 Client 中渲染 Server 内容
 * 4. 使用 Server Action 处理表单提交和数据变更
 * 5. 利用 Suspense 实现流式渲染,提升用户体验
 * 6. 敏感数据(API 密钥、数据库 URL)只在 Server Component 中使用
 * 7. 大型依赖库(markdown 解析、语法高亮)放在 Server Component 中
 * 8. 合理使用 fetch 缓存策略(force-cache / no-store / revalidate)
 * 9. 为异步组件提供有意义的 loading 状态(骨架屏优于 spinner)
 *
 * ❌ 避免做法:
 * 1. 在 Client Component 中获取可以在服务端获取的数据
 *    → 增加客户端 JS 体积和请求瀑布
 * 2. 将整个页面标记为 'use client'
 *    → 丧失 Server Component 的所有优势
 * 3. 向 Client Component 传递不可序列化的 props(函数、类实例)
 *    → 会导致运行时错误
 * 4. 在 Server Component 中使用 useState / useEffect
 *    → 这些 Hook 只能在客户端使用
 * 5. 在 Client Component 中使用 cookies() / headers()
 *    → 这些是服务端专用 API
 * 6. 过度使用 'use client',给不需要的文件都加上该指令
 *    → 分析真正需要交互的组件,精准标记
 * 7. 忽略流式渲染和 Suspense
 *    → 用户会面对长时间白屏
 * 8. 在 Server Component 之间通过全局变量共享状态
 *    → 每个请求应该是独立的
 */

// --- 示例:组件边界划分 ---

/**
 * 一个典型的电商产品页面组件树:
 *
 * ProductPage (Server)            ← 获取产品数据
 * ├── ProductBreadcrumb (Server)  ← 纯展示
 * ├── ProductImages (Client)      ← 图片轮播需要交互
 * ├── ProductInfo (Server)        ← 纯展示
 * │   ├── ProductTitle (Server)   ← 纯展示
 * │   ├── ProductPrice (Server)   ← 纯展示
 * │   └── AddToCart (Client)      ← 按钮交互 + 状态
 * ├── ProductTabs (Client)        ← 标签切换需要交互
 * │   ├── Description (Server)    ← 通过 children 传入
 * │   ├── Specifications (Server) ← 通过 children 传入
 * │   └── Reviews (Server)        ← 通过 children 传入
 * │       └── ReviewForm (Client) ← 表单交互
 * └── RelatedProducts (Server)    ← 服务端获取推荐数据
 *     └── ProductCard (Client)    ← 收藏按钮需要交互
 */


// ============================================================
//                    辅助类型与函数(示例用)
// ============================================================

// 引入客户端 Hook(实际使用时通过 'use client' 指令启用)
import { useState, useEffect } from 'react';

// 类型定义
interface Product {
    id: number;
    name: string;
    price: number;
    description: string;
}

interface Comment {
    id: number;
    author: string;
    text: string;
}

interface Article {
    id: number;
    title: string;
    author: string;
    date: string;
    content: string;
    likes: number;
}

// 模拟数据获取函数
async function getProductsFromDB(): Promise<Product[]> {
    return [
        { id: 1, name: '机械键盘', price: 299, description: 'Cherry MX 轴体' },
        { id: 2, name: '无线鼠标', price: 199, description: '人体工学设计' },
    ];
}

async function fetchUser(id: number): Promise<UserData> {
    return { id, name: '张三', email: '[email protected]', createdAt: '2024-01-01' };
}

async function fetchData(): Promise<{ title: string; description: string; updatedAt: string; html: string }> {
    return { title: '标题', description: '描述', updatedAt: '2024-12-01', html: '<p>内容</p>' };
}

async function fetchArticle(slug: string): Promise<Article | null> {
    return { id: 1, title: `文章: ${slug}`, author: '作者', date: '2024-12-01', content: '内容...', likes: 42 };
}

// 占位组件
async function ProductInfo({ id }: { id: string }) {
    return <div>产品信息 #{id}</div>;
}

async function ProductReviews({ id }: { id: string }) {
    return <div>产品评论 #{id}</div>;
}

async function RelatedProducts({ id }: { id: string }) {
    return <div>相关产品 #{id}</div>;
}

async function OverviewPanel() {
    return <div>概览面板</div>;
}

async function StatsPanel() {
    return <div>统计面板</div>;
}

function CommentForm({ articleId }: { articleId: number }) {
    return <div>评论表单 #{articleId}</div>;
}

💬 讨论

使用 GitHub 账号登录后即可参与讨论

基于 MIT 许可发布