Appearance
Server Components限制与注意事项
学习目标
通过本章学习,你将掌握:
- Server Components的技术限制
- 常见错误和解决方案
- 性能陷阱
- 安全注意事项
- 最佳实践
- 调试技巧
- 常见反模式
- 迁移注意事项
第一部分:技术限制
1.1 不能使用Hooks
jsx
// ❌ 错误:Server Component不能使用Hooks
async function BadServerComponent() {
// 所有Hooks都不能使用
const [state, setState] = useState(0); // 错误!
const ref = useRef(null); // 错误!
const context = useContext(MyContext); // 错误!
useEffect(() => { // 错误!
console.log('effect');
}, []);
const memoized = useMemo(() => {}, []); // 错误!
const callback = useCallback(() => {}, []); // 错误!
return <div>{state}</div>;
}
// ✅ 正确:将需要Hooks的部分提取到Client Component
async function GoodServerComponent() {
const data = await fetchData();
return (
<div>
<h1>Server Content</h1>
<p>{data.content}</p>
{/* 使用Client Component处理交互 */}
<ClientInteractive data={data} />
</div>
);
}
'use client';
function ClientInteractive({ data }) {
const [selected, setSelected] = useState(null);
return (
<div>
{/* 这里可以使用Hooks */}
<button onClick={() => setSelected(data.id)}>
Select
</button>
</div>
);
}1.2 不能使用浏览器API
jsx
// ❌ 错误:Server Component不能使用浏览器API
async function BadServerComponent() {
// 浏览器API在服务器不存在
const width = window.innerWidth; // 错误!window未定义
const stored = localStorage.getItem('key'); // 错误!localStorage未定义
document.title = 'Title'; // 错误!document未定义
navigator.geolocation.getCurrentPosition(); // 错误!navigator未定义
return <div>{width}</div>;
}
// ✅ 正确:在Client Component中使用
'use client';
function GoodClientComponent() {
const [dimensions, setDimensions] = useState({
width: 0,
height: 0
});
useEffect(() => {
const handleResize = () => {
setDimensions({
width: window.innerWidth,
height: window.innerHeight
});
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<div>
窗口大小: {dimensions.width} x {dimensions.height}
</div>
);
}1.3 不能使用事件处理
jsx
// ❌ 错误:Server Component不能有事件处理
async function BadServerComponent() {
const handleClick = () => {
console.log('clicked');
};
return (
<div>
<button onClick={handleClick}> {/* 错误! */}
Click Me
</button>
<input
onChange={(e) => console.log(e.target.value)} {/* 错误! */}
/>
</div>
);
}
// ✅ 正确:事件处理放在Client Component
async function GoodServerComponent() {
const data = await fetchData();
return (
<div>
<h1>{data.title}</h1>
<ClientButton />
</div>
);
}
'use client';
function ClientButton() {
const handleClick = () => {
console.log('clicked');
};
return (
<button onClick={handleClick}>
Click Me
</button>
);
}1.4 Props必须可序列化
jsx
// ❌ 错误:不能传递不可序列化的值
async function BadServerComponent() {
// 函数
const handleClick = () => console.log('click');
// Class实例
const instance = new MyClass();
// Symbol
const sym = Symbol('key');
// Date(会被序列化为字符串)
const date = new Date();
return (
<ClientComponent
onClick={handleClick} // ❌ 函数不可序列化
instance={instance} // ❌ Class实例不可序列化
symbol={sym} // ❌ Symbol不可序列化
date={date} // ⚠️ 会变成字符串
/>
);
}
// ✅ 正确:只传递可序列化的数据
async function GoodServerComponent() {
const data = {
id: 1,
name: 'Test',
items: [1, 2, 3],
meta: {
created: new Date().toISOString() // 字符串
}
};
return <ClientComponent data={data} />;
}
// 如果需要传递函数,使用Server Actions
import { handleAction } from './actions';
async function ServerComponent() {
return (
<ClientComponent
action={handleAction} // ✅ Server Action可以传递
/>
);
}1.5 不能使用Context Provider
jsx
// ❌ 错误:Server Component不能是Context Provider
async function BadServerComponent() {
const value = await fetchData();
return (
<MyContext.Provider value={value}> {/* 错误! */}
<ChildComponent />
</MyContext.Provider>
);
}
// ✅ 正确:在Client Component中提供Context
// layout.jsx (Server)
async function RootLayout({ children }) {
const initialData = await fetchData();
return (
<ClientProvider initialData={initialData}>
{children}
</ClientProvider>
);
}
// provider.jsx (Client)
'use client';
import { createContext, useState } from 'react';
const DataContext = createContext(null);
export function ClientProvider({ initialData, children }) {
const [data, setData] = useState(initialData);
return (
<DataContext.Provider value={{ data, setData }}>
{children}
</DataContext.Provider>
);
}第二部分:常见错误
2.1 组件边界错误
jsx
// ❌ 错误:Client Component不能直接导入Server Component
'use client';
import ServerComponent from './ServerComponent'; // 错误!
function BadClientComponent() {
return (
<div>
<ServerComponent /> {/* 会变成Client Component! */}
</div>
);
}
// ✅ 正确:通过props.children传递
// Parent.jsx (Server)
import ClientWrapper from './ClientWrapper';
import ServerContent from './ServerContent';
async function Parent() {
return (
<ClientWrapper>
<ServerContent /> {/* 作为children传递 */}
</ClientWrapper>
);
}
// ClientWrapper.jsx (Client)
'use client';
function ClientWrapper({ children }) {
const [visible, setVisible] = useState(true);
return (
<div>
<button onClick={() => setVisible(!visible)}>
Toggle
</button>
{visible && children}
</div>
);
}2.2 忘记'use client'声明
jsx
// ❌ 错误:使用了Client特性但没有声明'use client'
function BadComponent() {
// 这看起来像Client Component但没有声明
const [count, setCount] = useState(0); // 错误!
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
// ✅ 正确:添加'use client'声明
'use client';
function GoodComponent() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}2.3 async Client Component
jsx
// ❌ 错误:Client Component函数不能是async
'use client';
async function BadClientComponent() { // 错误!
const data = await fetchData();
return <div>{data}</div>;
}
// ✅ 正确:在useEffect中获取数据
'use client';
function GoodClientComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
if (!data) return <div>Loading...</div>;
return <div>{data}</div>;
}
// ✅ 更好:让Server Component获取数据
async function ServerParent() {
const data = await fetchData();
return <ClientChild data={data} />;
}
'use client';
function ClientChild({ data }) {
return <div>{data}</div>;
}2.4 环境变量暴露
jsx
// ❌ 危险:敏感环境变量可能暴露
'use client';
function BadComponent() {
// 客户端代码会包含这个值!
const apiKey = process.env.NEXT_PUBLIC_API_KEY; // 会暴露
return <div>Key: {apiKey}</div>;
}
// ✅ 正确:敏感操作在Server Component或Server Actions
async function GoodServerComponent() {
// 这个密钥只在服务器使用
const secretKey = process.env.SECRET_API_KEY; // 安全
const data = await fetch('https://api.example.com/data', {
headers: {
'Authorization': `Bearer ${secretKey}`
}
}).then(r => r.json());
return <div>{data.content}</div>;
}第三部分:性能陷阱
3.1 过度串行请求
jsx
// ❌ 问题:串行请求导致瀑布流
async function SlowPage({ userId }) {
// 请求1
const user = await fetchUser(userId);
// 等待user完成后才开始请求2
const posts = await fetchPosts(user.id);
// 等待posts完成后才开始请求3
const comments = await fetchComments(posts[0].id);
// 总时间 = 请求1 + 请求2 + 请求3
return <div>...</div>;
}
// ✅ 解决:并行请求
async function FastPage({ userId }) {
// 同时启动所有请求
const [user, posts, comments] = await Promise.all([
fetchUser(userId),
fetchPosts(userId), // 如果不依赖user,直接用userId
fetchComments(userId)
]);
// 总时间 = max(请求1, 请求2, 请求3)
return <div>...</div>;
}3.2 过度查询数据库
jsx
// ❌ 问题:N+1查询
async function PostsList() {
const posts = await db.posts.findMany();
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<Post post={post} />
</li>
))}
</ul>
);
}
async function Post({ post }) {
// 每个post都查询一次author!
const author = await db.users.findUnique({
where: { id: post.authorId }
});
return (
<div>
<h3>{post.title}</h3>
<p>作者: {author.name}</p>
</div>
);
}
// ✅ 解决:预加载关联数据
async function PostsList() {
const posts = await db.posts.findMany({
include: {
author: true // 一次查询包含所有author
}
});
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<div>
<h3>{post.title}</h3>
<p>作者: {post.author.name}</p>
</div>
</li>
))}
</ul>
);
}3.3 过度获取数据
jsx
// ❌ 问题:获取不需要的数据
async function UserCard({ userId }) {
// 获取了用户的所有字段,包括password等
const user = await db.users.findUnique({
where: { id: userId }
});
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
// ✅ 解决:只选择需要的字段
async function UserCard({ userId }) {
const user = await db.users.findUnique({
where: { id: userId },
select: {
id: true,
name: true,
email: true
// 不包含password等敏感字段
}
});
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}3.4 不合理的缓存策略
jsx
// ❌ 问题:动态数据使用了长期缓存
async function UserStats({ userId }) {
// 用户统计是实时变化的,不应该缓存太久
const stats = await fetch(`/api/stats/${userId}`, {
next: { revalidate: 86400 } // 缓存1天!
});
return <div>{stats.count}</div>;
}
// ✅ 解决:根据数据特性设置合适的缓存
async function UserStats({ userId }) {
// 实时数据不缓存
const liveStats = await fetch(`/api/stats/${userId}`, {
cache: 'no-store'
});
// 或短期缓存
const recentStats = await fetch(`/api/stats/${userId}`, {
next: { revalidate: 60 } // 1分钟
});
return <div>{liveStats.count}</div>;
}第四部分:安全注意事项
4.1 始终验证权限
jsx
// ❌ 危险:没有权限验证
'use server';
export async function deletePost(postId) {
// 任何人都可以删除任何文章!
await db.posts.delete({
where: { id: postId }
});
}
// ✅ 安全:验证权限
'use server';
export async function deletePost(postId) {
// 获取当前用户
const session = await getSession();
if (!session) {
throw new Error('未登录');
}
// 检查文章所有权
const post = await db.posts.findUnique({
where: { id: postId }
});
if (!post) {
throw new Error('文章不存在');
}
if (post.authorId !== session.userId && !session.isAdmin) {
throw new Error('无权限删除此文章');
}
// 验证通过,执行删除
await db.posts.delete({
where: { id: postId }
});
}4.2 验证和清理输入
jsx
// ❌ 危险:直接使用用户输入
'use server';
export async function createPost(formData) {
const title = formData.get('title');
const content = formData.get('content');
// 没有验证就直接创建!
await db.posts.create({
data: { title, content }
});
}
// ✅ 安全:验证和清理输入
'use server';
import { z } from 'zod';
import sanitizeHtml from 'sanitize-html';
const postSchema = z.object({
title: z.string().min(3).max(200),
content: z.string().min(10).max(10000)
});
export async function createPost(formData) {
const title = formData.get('title');
const content = formData.get('content');
// 验证
const result = postSchema.safeParse({ title, content });
if (!result.success) {
return {
error: '输入不合法',
details: result.error.issues
};
}
// 清理HTML(防止XSS)
const sanitizedContent = sanitizeHtml(content, {
allowedTags: ['p', 'b', 'i', 'em', 'strong'],
allowedAttributes: {}
});
await db.posts.create({
data: {
title: result.data.title,
content: sanitizedContent
}
});
}4.3 防止SQL注入
jsx
// ❌ 危险:直接拼接SQL(如果使用原始查询)
'use server';
export async function searchUsers(query) {
// 极其危险!可能导致SQL注入
const sql = `SELECT * FROM users WHERE name LIKE '%${query}%'`;
const users = await db.$queryRawUnsafe(sql);
return users;
}
// ✅ 安全:使用参数化查询
'use server';
export async function searchUsers(query) {
// 使用Prisma的安全API
const users = await db.users.findMany({
where: {
name: {
contains: query,
mode: 'insensitive'
}
}
});
return users;
}
// 或使用参数化的原始查询
export async function searchUsersRaw(query) {
const users = await db.$queryRaw`
SELECT * FROM users
WHERE name LIKE ${`%${query}%`}
`;
return users;
}4.4 速率限制
jsx
// ✅ 实现速率限制
'use server';
import { rateLimit } from '@/lib/rate-limit';
const limiter = rateLimit({
interval: 60 * 1000, // 1分钟
uniqueTokenPerInterval: 500
});
export async function sendEmail(formData) {
try {
// 基于IP限制
await limiter.check(10, 'SEND_EMAIL'); // 每分钟最多10次
const email = formData.get('email');
const message = formData.get('message');
await sendEmailService(email, message);
return { success: true };
} catch (error) {
if (error.message === 'Rate limit exceeded') {
return {
error: '操作过于频繁,请稍后再试'
};
}
throw error;
}
}注意事项
1. 开发vs生产环境
jsx
// 开发环境可能隐藏一些问题
// 生产环境要注意:
// ✅ 使用环境变量
const apiUrl = process.env.API_URL || 'http://localhost:3000';
// ✅ 错误处理要完善
try {
const data = await fetchData();
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('Debug:', error);
}
// 生产环境返回通用错误
throw new Error('数据获取失败');
}
// ✅ 性能监控
if (process.env.NODE_ENV === 'production') {
// 记录性能指标
}2. 类型安全
jsx
// ✅ 使用TypeScript确保类型安全
// actions.ts
'use server';
interface CreatePostInput {
title: string;
content: string;
}
interface CreatePostResult {
success: boolean;
post?: {
id: string;
title: string;
};
error?: string;
}
export async function createPost(
formData: FormData
): Promise<CreatePostResult> {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// TypeScript会检查类型
const post = await db.posts.create({
data: { title, content }
});
return {
success: true,
post: {
id: post.id,
title: post.title
}
};
}3. 测试
jsx
// ✅ 测试Server Components和Server Actions
// __tests__/actions.test.ts
import { createPost } from '@/app/actions';
describe('createPost', () => {
it('should create a post', async () => {
const formData = new FormData();
formData.set('title', 'Test Post');
formData.set('content', 'Test Content');
const result = await createPost(formData);
expect(result.success).toBe(true);
expect(result.post).toBeDefined();
});
it('should validate input', async () => {
const formData = new FormData();
formData.set('title', 'Ab'); // 太短
formData.set('content', 'Test');
const result = await createPost(formData);
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
});常见问题
Q1: 如何调试Server Components?
A: 使用console.log(只在服务器控制台显示)和React DevTools。
Q2: Server Components会增加服务器负载吗?
A: 会,但可以通过缓存和优化减少。
Q3: 如何处理大量并发请求?
A: 使用数据库连接池、缓存、CDN和负载均衡。
总结
核心限制
❌ 不能使用Hooks
❌ 不能使用浏览器API
❌ 不能有事件处理
❌ Props必须可序列化
❌ 不能是Context Provider安全要点
✅ 总是验证权限
✅ 验证和清理输入
✅ 使用参数化查询
✅ 实现速率限制
✅ 保护敏感信息性能要点
✅ 避免串行请求
✅ 预加载关联数据
✅ 只查询需要的字段
✅ 合理设置缓存
✅ 使用流式渲染理解并遵守这些限制和注意事项,才能安全高效地使用Server Components!