服务端与客户端组件
深入解析服务端组件与客户端组件的区别、使用场景及渲染模式。
📚 学习目标
学完这篇文章后,你将能够:
- 理解 Server Components 和 Client Components 的区别
- 掌握
'use client'指令的使用 - 学会根据需求选择合适的组件类型
- 了解 React Hooks 的使用限制
- 掌握性能优化最佳实践
前置知识
在开始学习之前,建议先阅读:
你需要了解:
- React 组件基础
- React Hooks(useState、useEffect 等)
1️⃣ Server Components vs Client Components
1.1 核心区别对比
| 特性 | Server Components | Client 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 的优势
-
减少客户端 JavaScript
- 不发送组件代码到浏览器
- 更小的 bundle size
- 更快的页面加载
-
直接访问数据库
- 可以在服务器上查询数据库
- 保护敏感数据(不暴露给客户端)
-
更好的 SEO
- HTML 在服务器上生成
- 搜索引擎可以抓取内容
-
更快的首屏渲染
- 服务器渲染完整的 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 的优势
-
交互性
- 支持事件处理
- 状态管理
- 生命周期
-
动态内容
- 实时更新
- 用户输入响应
- 动画效果
-
浏览器 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 | 记忆值 | ✅ 是 |
useRouter | Next.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.tsx | Server | 全局布局,SEO 重要 |
app/page.tsx | Client | 聊天界面,大量交互 |
components/MessageList.tsx | Client | 实时更新消息 |
components/ChatInput.tsx | Client | 表单输入 |
components/SessionSidebar.tsx | Client | 会话管理交互 |
components/ProtectedRoute.tsx | Client | 路由保护逻辑 |
components/canvas/CanvasPanel.tsx | Client | 代码预览交互 |
💡 练习题
-
选择题:以下哪个组件必须是 Client Component?
- A. 显示用户信息的静态页面
- B. 带有点赞按钮的文章列表
- C. 博客文章详情页
- D. 页面布局组件
-
代码题:指出以下代码的错误并修正:
export default function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(c => c + 1)}>{count}</button>; } -
分析题:查看本项目的 app/page.tsx,说明为什么它必须使用
'use client'指令。 -
实践题:创建一个博客应用,包含:
- Server Component:博客列表页(文章列表)
- Server Component:文章详情页(SEO 优化)
- Client Component:评论输入框
- Client Component:点赞按钮
📚 参考资源
官方文档
本项目相关文件
- app/layout.tsx - Server Component
- app/page.tsx - Client Component
- app/hooks/useSessionManager.ts - 自定义 Hook
- app/hooks/useChatMessages.ts - 消息状态 Hook
✅ 总结
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 Componentapp/page.tsx→ Client Component(大量交互)app/components/*→ Client Componentsapp/hooks/*→ 封装可复用的 Client 逻辑
下一步:阅读下一篇文章《数据获取模式》,学习 Next.js 中的数据获取最佳实践。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。