服务端与客户端组件

深入解析服务端组件与客户端组件的区别、使用场景及渲染模式。

📚 学习目标

学完这篇文章后,你将能够:

  • 理解 Server Components 和 Client Components 的区别
  • 掌握 'use client' 指令的使用
  • 学会根据需求选择合适的组件类型
  • 了解 React Hooks 的使用限制
  • 掌握性能优化最佳实践

前置知识

在开始学习之前,建议先阅读:

你需要了解:

  • React 组件基础
  • React Hooks(useState、useEffect 等)

1️⃣ Server Components vs Client Components

1.1 核心区别对比

特性Server ComponentsClient Components
默认类型✅ 默认❌ 需要标记 'use client'
渲染位置服务器浏览器
使用 Hooks❌ 不支持✅ 支持
浏览器 API❌ 不支持✅ 支持
事件处理❌ 不支持✅ 支持
状态管理❌ 不支持✅ 支持
包大小0(不发送到客户端)包含在 bundle 中
SEO✅ 优秀⚠️ 较差
性能✅ 更好(减少 JS)⚠️ 较差(更多 JS)

1.2 渲染流程图

1.3 什么时候使用哪个?

使用 Server Components(默认)

推荐场景

  • 📄 静态内容(博客文章、文档)
  • 🔍 SEO 重要页面(首页、着陆页)
  • 💾 数据获取(直接查询数据库)
  • 🎨 布局和包装组件
  • 📦 减少客户端 JavaScript

示例

// app/layout.tsx - Server Component(默认)
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="zh-CN">
      <body>
        {children}
      </body>
    </html>
  );
}

使用 Client Components

必须场景

  • 🖱️ 事件处理(onClick、onChange 等)
  • 🔄 状态管理(useState、useReducer)
  • ⏱️ 生命周期(useEffect、useCallback)
  • 🌐 浏览器 API(localStorage、window 等)
  • 🎯 动态交互(路由跳转、表单提交)

示例

'use client'; // 必须标记

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0); // 需要 Hooks

  return (
    <button onClick={() => setCount(c => c + 1)}>
      点击了 {count} 
    </button>
  );
}

2️⃣ Server Components 详解

2.1 基本语法

Server Components 是 Next.js 的默认类型,不需要任何标记。

// 默认就是 Server Component
export default function UserProfile({ userId }: { userId: string }) {
  // 可以直接在服务器上获取数据
  const user = await fetchUser(userId);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

async function fetchUser(userId: string) {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

2.2 本项目的 Server Components

查看全局布局:app/layout.tsx

import type { Metadata } from "next";
import "./globals.css";
import { AuthProvider } from "./contexts/AuthContext";

export const metadata: Metadata = {
  title: "LangGraph Chat App",
  description: "Chat application powered by LangGraph",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="zh-CN">
      <body className="bg-[#050509] text-slate-200 antialiased selection:bg-blue-500/30 overflow-hidden">
        <AuthProvider>
          {children}
        </AuthProvider>
      </body>
    </html>
  );
}

说明

  • 这是 Server Component(默认)
  • 设置全局 metadata(SEO)
  • 提供全局布局和认证上下文
  • 不使用任何 React Hooks

2.3 Server Components 的优势

  1. 减少客户端 JavaScript

    • 不发送组件代码到浏览器
    • 更小的 bundle size
    • 更快的页面加载
  2. 直接访问数据库

    • 可以在服务器上查询数据库
    • 保护敏感数据(不暴露给客户端)
  3. 更好的 SEO

    • HTML 在服务器上生成
    • 搜索引擎可以抓取内容
  4. 更快的首屏渲染

    • 服务器渲染完整的 HTML
    • 客户端立即显示内容

2.4 Server Components 的限制

不能使用

  • React Hooks(useState、useEffect、useCallback 等)
  • 浏览器 API(window、document、localStorage 等)
  • 事件处理(onClick、onChange 等)
  • 状态管理

可以使用

  • async/await
  • 数据获取(fetch、数据库查询)
  • 其他 Server Components
  • Client Components(作为子组件)

3️⃣ Client Components 详解

3.1 基本语法

使用 'use client' 指令标记 Client Components。

'use client';

import { useState, useEffect } from 'react';

export default function Clock() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    const timer = setInterval(() => {
      setTime(new Date());
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  return (
    <div>
      当前时间: {time.toLocaleTimeString()}
    </div>
  );
}

3.2 本项目的 Client Components

查看主页:app/page.tsx

'use client';

import { useRef, useMemo, useState, useEffect } from 'react';
import type { CanvasArtifact } from './canvas/canvas-types';

// 导入组件
import { ProtectedRoute } from './components/ProtectedRoute';
import SessionSidebar from './components/SessionSidebar';
import { ChatHeader } from './components/ChatHeader';
import { MessageList } from './components/MessageList';
import { ChatInput, type ChatInputHandle } from './components/ChatInput';

// 导入自定义 Hooks
import { useChatMessages } from './hooks/useChatMessages';
import { useSessionManager } from './hooks/useSessionManager';
import { useChatHistory } from './hooks/useChatHistory';
import { useSendMessage } from './hooks/useSendMessage';
import { canvasStore } from './hooks/useCanvasArtifacts';
import { useToolSelection } from './hooks/useToolSelection';

/**
 * 聊天页面主组件
 *
 * 该组件是聊天应用的主页面,负责:
 * 1. 整合所有子组件(头部、侧边栏、消息列表、输入框)
 * 2. 管理聊天消息状态
 * 3. 管理会话(session)状态
 * 4. 处理消息发送和历史记录加载
 */
export default function ChatPage() {
  const chatInputRef = useRef<ChatInputHandle>(null);

  // Canvas Panel 状态
  const [activeArtifact, setActiveArtifact] = useState<CanvasArtifact | null>(
    null,
  );
  const [isCanvasVisible, setIsCanvasVisible] = useState(false);

  // 工具选择状态(持久化到 localStorage)
  const { selectedTools, setSelectedTools } = useToolSelection();

  // 消息管理
  const { messages, addMessage, updateMessage, clearMessages } =
    useChatMessages();

  // 会话管理
  const {
    sessions,
    currentSession,
    loading: sessionsLoading,
    createSession,
    deleteSession,
    renameSession,
    switchSession,
  } = useSessionManager();

  // 消息发送
  const { sendMessage, loading: sending } = useSendMessage({
    addMessage,
    updateMessage,
    clearMessages,
    activeArtifact,
    setActiveArtifact,
    selectedTools,
  });

  // 加载历史记录
  useChatHistory({
    currentSession,
    addMessage,
    clearMessages,
  });

  // ... 组件渲染逻辑
}

为什么必须使用 Client Component?

  • ✅ 使用了 React Hooks(useState、useRef、useEffect)
  • ✅ 有交互逻辑(发送消息、切换会话)
  • ✅ 需要管理本地状态
  • ✅ 使用了浏览器 API(localStorage)

3.3 Client Components 的优势

  1. 交互性

    • 支持事件处理
    • 状态管理
    • 生命周期
  2. 动态内容

    • 实时更新
    • 用户输入响应
    • 动画效果
  3. 浏览器 API

    • localStorage、sessionStorage
    • window、document
    • 地理位置、摄像头等

3.4 Client Components 的注意事项

⚠️ 需要考虑

  • 增加 JavaScript bundle 大小
  • 可能影响首屏加载速度
  • 对 SEO 不友好(内容是动态生成的)

优化建议

  • 尽量减少 Client Components 的数量
  • 只在需要交互的组件使用 'use client'
  • 使用 React.lazy() 懒加载组件
  • 代码分割(dynamic imports)

4️⃣ 混合使用 Server 和 Client Components

4.1 嵌套规则

Server Component
└── Server Component ✅
└── Client Component ✅

Client Component
└── Client Component ✅
└── Server Component ❌ (不能直接嵌套)

解决方法:将 Server Component 的内容通过 props 传递给 Client Component

4.2 示例:混合使用

// app/page.tsx - Server Component
import UserProfile from './components/UserProfile';

export default function Page({ userId }: { userId: string }) {
  // 在服务器上获取数据
  const user = await fetchUser(userId);

  // 将数据传递给 Client Component
  return (
    <div>
      <UserProfile user={user} />
    </div>
  );
}

// components/UserProfile.tsx - Client Component
'use client';

export default function UserProfile({ user }: { user: { name: string; email: string } }) {
  const [isEditing, setIsEditing] = useState(false);

  return (
    <div>
      {isEditing ? (
        <input defaultValue={user.name} />
      ) : (
        <h1>{user.name}</h1>
      )}
      <p>{user.email}</p>
      <button onClick={() => setIsEditing(!isEditing)}>
        {isEditing ? '保存' : '编辑'}
      </button>
    </div>
  );
}

4.3 本项目的混合使用模式

布局层(Server) + 交互层(Client)

// app/layout.tsx - Server Component
export default function RootLayout({ children }) {
  return (
    <html lang="zh-CN">
      <body>
        <AuthProvider>
          {children}
        </AuthProvider>
      </body>
    </html>
  );
}

// app/page.tsx - Client Component
'use client';

export default function ChatPage() {
  // 所有交互逻辑都在这里
  return (
    <div>
      <SessionSidebar />
      <ChatHeader />
      <MessageList />
      <ChatInput />
    </div>
  );
}

5️⃣ React Hooks 使用限制

5.1 Hooks 只能在 Client Components 中使用

错误示例

// Server Component(默认)
export default function Counter() {
  const [count, setCount] = useState(0); // ❌ 错误!不能在 Server Component 中使用

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

正确示例

'use client'; // 必须标记

export default function Counter() {
  const [count, setCount] = useState(0); // ✅ 正确!

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

5.2 常用 Hooks 及适用场景

Hook用途只能在 Client Components
useState状态管理✅ 是
useEffect副作用✅ 是
useContext上下文✅ 是
useRef引用✅ 是
useCallback记忆函数✅ 是
useMemo记忆值✅ 是
useRouterNext.js 路由✅ 是
useParams路由参数✅ 是
useSearchParams查询参数✅ 是

5.3 本项目的 Hooks 使用

查看会话管理 Hook:app/hooks/useSessionManager.ts

'use client';

import { useState, useEffect } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import type { Session } from '@/app/database/sessions';

export function useSessionManager() {
  const [sessions, setSessions] = useState<Session[]>([]);
  const [currentSession, setCurrentSession] = useState<Session | null>(null);
  const [loading, setLoading] = useState(false);

  const router = useRouter();
  const pathname = usePathname();

  // 加载会话列表
  const loadSessions = async () => {
    setLoading(true);
    try {
      const response = await fetch('/api/chat/sessions');
      const data = await response.json();
      setSessions(data.sessions);
    } catch (e) {
      console.error('加载会话失败:', e);
    } finally {
      setLoading(false);
    }
  };

  // 创建会话
  const createSession = async (name: string, type: 'chat' | 'deepresearch') => {
    const response = await fetch('/api/chat/sessions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name, type }),
    });
    const data = await response.json();
    await loadSessions();
    return data;
  };

  // 删除会话
  const deleteSession = async (id: string) => {
    const response = await fetch('/api/chat/sessions', {
      method: 'DELETE',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id }),
    });
    await loadSessions();
  };

  // 重命名会话
  const renameSession = async (id: string, name: string) => {
    const response = await fetch('/api/chat/sessions', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id, name }),
    });
    await loadSessions();
  };

  // 切换会话
  const switchSession = (session: Session) => {
    setCurrentSession(session);
    router.push(`/${session.type === 'deepresearch' ? 'deepresearch' : ''}`);
  };

  useEffect(() => {
    loadSessions();
  }, []);

  return {
    sessions,
    currentSession,
    loading,
    loadSessions,
    createSession,
    deleteSession,
    renameSession,
    switchSession,
  };
}

说明

  • 文件顶部有 'use client' 标记
  • 使用了 useState、useEffect、useRouter、usePathname
  • 封装了会话管理的所有逻辑
  • 可以在任何 Client Component 中使用

6️⃣ 性能优化最佳实践

6.1 尽量使用 Server Components

优势

  • 减少 JavaScript bundle 大小
  • 更快的首屏加载
  • 更好的 SEO

示例

// ✅ 推荐:Server Component
export default function PostList() {
  const posts = await fetchPosts();

  return (
    <div>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

6.2 只在需要时使用 Client Components

原则:组件需要交互时才使用 'use client'

示例

// components/PostCard.tsx - 需要交互
'use client';

export default function PostCard({ post }: { post: Post }) {
  const [liked, setLiked] = useState(false);

  return (
    <div>
      <h2>{post.title}</h2>
      <p>{post.content}</p>
      <button onClick={() => setLiked(!liked)}>
        {liked ? '取消点赞' : '点赞'}
      </button>
    </div>
  );
}

6.3 使用 dynamic imports 懒加载 Client Components

示例

// app/page.tsx
import dynamic from 'next/dynamic';

// 懒加载组件
const InteractiveChart = dynamic(() => import('./components/InteractiveChart'), {
  loading: () => <div>加载中...</div>,
  ssr: false, // 不在服务器渲染
});

export default function Dashboard() {
  return (
    <div>
      <h1>仪表盘</h1>
      <InteractiveChart />
    </div>
  );
}

6.4 本项目的性能优化

分析

本项目的大部分组件都是 Client Components,这是合理的,因为:

  • ✅ 聊天应用需要大量交互
  • ✅ 实时更新消息状态
  • ✅ 用户输入和操作频繁
  • ✅ 不是 SEO 优先的应用

优化建议

  • 可以将一些静态组件(如页面 footer、侧边栏的部分内容)改为 Server Components
  • 使用 React.lazy() 懒加载大型组件(如 Canvas Panel)
  • 代码分割减少初始 bundle 大小

7️⃣ 实战案例:选择正确的组件类型

7.1 需求分析

为以下功能选择合适的组件类型:

功能组件类型原因
博客文章列表Server静态内容,需要 SEO
文章详情页Server主要显示内容,SEO 重要
评论输入框Client需要表单交互
点赞按钮Client需要状态和事件处理
用户头像图片Server静态内容
用户设置页Client需要表单交互
侧边栏导航Server静态内容(可部分 Client)
聊天界面Client大量交互和状态管理

7.2 本项目分析

组件类型原因
app/layout.tsxServer全局布局,SEO 重要
app/page.tsxClient聊天界面,大量交互
components/MessageList.tsxClient实时更新消息
components/ChatInput.tsxClient表单输入
components/SessionSidebar.tsxClient会话管理交互
components/ProtectedRoute.tsxClient路由保护逻辑
components/canvas/CanvasPanel.tsxClient代码预览交互

💡 练习题

  1. 选择题:以下哪个组件必须是 Client Component?

    • A. 显示用户信息的静态页面
    • B. 带有点赞按钮的文章列表
    • C. 博客文章详情页
    • D. 页面布局组件
  2. 代码题:指出以下代码的错误并修正:

    export default function Counter() {
      const [count, setCount] = useState(0);
      return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
    }
  3. 分析题:查看本项目的 app/page.tsx,说明为什么它必须使用 'use client' 指令。

  4. 实践题:创建一个博客应用,包含:

    • Server Component:博客列表页(文章列表)
    • Server Component:文章详情页(SEO 优化)
    • Client Component:评论输入框
    • Client Component:点赞按钮

📚 参考资源

官方文档

本项目相关文件


✅ 总结

Server Components(默认):

  • ✅ 不需要标记,就是默认类型
  • ✅ 在服务器上渲染
  • ✅ 减少 JavaScript bundle
  • ✅ 更好的 SEO
  • ❌ 不能使用 React Hooks
  • ❌ 不能使用浏览器 API
  • ❌ 不能处理事件

Client Components

  • ✅ 必须标记 'use client'
  • ✅ 在浏览器上渲染
  • ✅ 支持所有 React Hooks
  • ✅ 支持事件处理
  • ✅ 支持浏览器 API
  • ❌ 增加 JavaScript bundle
  • ❌ 对 SEO 不友好

选择原则

  • 需要交互 → Client Component
  • 静态内容 → Server Component
  • SEO 重要 → Server Component
  • 复杂状态管理 → Client Component

本项目的使用

  • app/layout.tsx → Server Component
  • app/page.tsx → Client Component(大量交互)
  • app/components/* → Client Components
  • app/hooks/* → 封装可复用的 Client 逻辑

下一步:阅读下一篇文章《数据获取模式》,学习 Next.js 中的数据获取最佳实践。

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page