数据获取模式
学习 Next.js 中的多种数据获取模式,包括 fetch、缓存及数据重新验证。
📚 学习目标
学完这篇文章后,你将能够:
- 理解 Server Components 和 Client Components 的数据获取差异
- 掌握 Next.js 中 fetch API 的增强功能
- 学会使用缓存和重验证策略
- 了解本项目的数据获取模式
- 掌握数据获取的最佳实践
前置知识
在开始学习之前,建议先阅读:
你需要了解:
- 异步编程(async/await)
- HTTP 请求基础
- React Hooks(useEffect)
1️⃣ 数据获取概述
1.1 Next.js 中的数据获取方式
| 组件类型 | 数据获取方式 | 位置 | 缓存 |
|---|---|---|---|
| Server Components | 直接调用 API/数据库 | 服务器 | ✅ 默认缓存 |
| Client Components | 通过 API Routes | 浏览器 | ❌ 需要手动实现 |
1.2 两种方式对比
Server Components 数据获取(推荐)
// app/page.tsx - Server Component
export default async function UserList() {
// 在服务器上直接获取数据
const response = await fetch('https://api.example.com/users', {
cache: 'force-cache', // 强制缓存
next: { revalidate: 3600 }, // 1小时后重新验证
});
const users = await response.json();
return (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}优势:
- ✅ 更快的数据获取(服务器到服务器)
- ✅ 保护 API 密钥(不暴露给客户端)
- ✅ 默认缓存
- ✅ SEO 友好
Client Components 数据获取
// app/components/UserList.tsx - Client Component
'use client';
import { useState, useEffect } from 'react';
export default function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetch('/api/users') // 调用内部 API
.then(res => res.json())
.then(data => setUsers(data.users))
.finally(() => setLoading(false));
}, []);
if (loading) return <div>加载中...</div>;
return (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}优势:
- ✅ 支持实时更新
- ✅ 用户交互触发更新
- ✅ 灵活的状态管理
2️⃣ Server Components 数据获取
2.1 基本语法
Server Components 可以使用 async/await 直接获取数据:
// app/users/page.tsx
export default async function UsersPage() {
// 直接调用外部 API
const response = await fetch('https://api.example.com/users');
const users = await response.json();
return (
<div>
<h1>用户列表</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}2.2 数据库查询
// app/users/[id]/page.tsx
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY!
);
export default async function UserPage({
params,
}: {
params: { id: string }
}) {
// 直接查询数据库
const { data: user } = await supabase
.from('users')
.select('*')
.eq('id', params.id)
.single();
if (!user) {
return <div>用户不存在</div>;
}
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}2.3 错误处理
// app/users/page.tsx
export default async function UsersPage() {
try {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error('获取用户失败');
}
const users = await response.json();
return (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
} catch (error) {
return <div>出错了: {error.message}</div>;
}
}3️⃣ Client Components 数据获取
3.1 使用 useEffect
'use client';
import { useState, useEffect } from 'react';
export default function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUsers = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('获取用户失败');
}
const data = await response.json();
setUsers(data.users);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}3.2 封装自定义 Hook
查看本项目的实现:app/hooks/useSessionManager.ts
'use client';
import { useState, useEffect } from 'react';
import type { Session } from '@/app/database/sessions';
export function useSessionManager() {
const [sessions, setSessions] = useState<Session[]>([]);
const [loading, setLoading] = useState(false);
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);
}
};
useEffect(() => {
loadSessions();
}, []);
return { sessions, loading, loadSessions };
}使用示例:
'use client';
import { useSessionManager } from '@/app/hooks/useSessionManager';
export default function SessionList() {
const { sessions, loading } = useSessionManager();
if (loading) return <div>加载中...</div>;
return (
<ul>
{sessions.map(session => (
<li key={session.id}>{session.name}</li>
))}
</ul>
);
}4️⃣ Next.js 的 fetch 增强
4.1 缓存策略
Next.js 扩展了 Web fetch API,添加了缓存功能:
// 默认缓存(可配置)
fetch('https://api.example.com/data', {
cache: 'force-cache', // 强制使用缓存
});
// 不缓存
fetch('https://api.example.com/data', {
cache: 'no-store', // 禁用缓存
});
// 缓存但在每次请求时重新验证
fetch('https://api.example.com/data', {
cache: 'no-cache',
});4.2 重新验证
时间基础重新验证
fetch('https://api.example.com/data', {
next: {
revalidate: 3600, // 1小时后重新验证
},
});标签基础重新验证
// 获取数据时指定标签
fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
});
// 重新验证
import { revalidateTag } from 'next/cache';
export async function createPost(data: FormData) {
// 创建帖子
await fetch('https://api.example.com/posts', {
method: 'POST',
body: data,
});
// 重新验证缓存
revalidateTag('posts');
}5️⃣ 本项目的数据获取模式
5.1 架构模式
本项目使用 三层架构进行数据获取:
流程:
- Client Component 使用 Hook
- Hook 调用内部 API (
/api/xxx) - API Route 调用 Service Layer
- Service Layer 调用 Database Layer
- Database Layer 执行 CRUD 操作
5.2 实战示例
会话管理数据获取
Client Component:
'use client';
import { useSessionManager } from '@/app/hooks/useSessionManager';
export default function SessionSidebar() {
const { sessions, loading, createSession } = useSessionManager();
return (
<div>
<button onClick={() => createSession('新会话', 'chat')}>
新建会话
</button>
{loading ? (
<div>加载中...</div>
) : (
<ul>
{sessions.map(session => (
<li key={session.id}>{session.name}</li>
))}
</ul>
)}
</div>
);
}Hook:app/hooks/useSessionManager.ts
export function useSessionManager() {
const [sessions, setSessions] = useState<Session[]>([]);
const loadSessions = async () => {
const response = await fetch('/api/chat/sessions');
const data = await response.json();
setSessions(data.sessions);
};
useEffect(() => {
loadSessions();
}, []);
return { sessions, loadSessions };
}API Route:app/api/chat/sessions/route.ts
export const GET = withAuth(async (request: NextRequest, auth) => {
const sessions = await sessionService.getAllSessions(auth.client);
return NextResponse.json({ sessions });
});Service Layer:app/services/session.service.ts
export class SessionService {
async getAllSessions(client: any, type?: SessionType) {
// 业务逻辑
const { data, error } = await client
.from('sessions')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false });
if (error) throw error;
return data;
}
}5.3 本项目为什么选择这种模式?
-
安全性:
- API 密钥保存在服务器
- 认证逻辑在服务器
- 用户只能访问自己的数据
-
灵活性:
- Client Component 可以实时更新
- 支持用户交互
- 复杂的状态管理
-
一致性:
- 统一的数据访问接口
- 复用 Service Layer
- 易于测试和维护
6️⃣ 最佳实践
6.1 数据获取原则
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 静态内容 | Server Components + 缓存 | 更快、SEO 友好 |
| 用户特定数据 | API Routes | 安全、灵活 |
| 实时更新 | Client Components + polling/WebSocket | 及时性 |
| 列表数据 | Server Components + 分页 | 性能好 |
| 详情数据 | API Routes + SWR/React Query | 缓存、同步 |
6.2 性能优化
避免重复请求
// ❌ 不好的做法
function PostList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch('/api/posts').then(res => res.json()).then(setPosts);
}, []); // 每次组件挂载都请求
return <div>{/* ... */}</div>;
}
// ✅ 好的做法
function PostList() {
const { posts, loading } = usePosts(); // Hook 内部处理缓存
if (loading) return <div>加载中...</div>;
return <div>{/* ... */}</div>;
}使用 SWR/React Query
'use client';
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then((res) => res.json());
export function usePosts() {
const { data, error, isLoading } = useSWR('/api/posts', fetcher);
return {
posts: data?.posts,
loading: isLoading,
error,
};
}💡 练习题
-
选择题:以下哪种数据获取方式性能更好?
- A. Client Components 直接调用外部 API
- B. Server Components 直接调用外部 API
- C. Client Components 通过 API Routes
- D. Server Components 通过 API Routes
-
代码题:创建一个 Server Component,显示用户列表,并设置 1 小时的缓存。
-
分析题:查看本项目的 app/hooks/useSessionManager.ts,说明它如何获取和管理会话数据。
-
实践题:实现一个博客应用的数据获取:
- Server Component:文章列表页(缓存 5 分钟)
- Client Component:文章详情页(不缓存)
- Hook:封装文章数据获取逻辑
📚 参考资源
官方文档
本项目相关文件
- app/hooks/useSessionManager.ts
- app/hooks/useChatMessages.ts
- app/api/chat/sessions/route.ts
- app/services/session.service.ts
✅ 总结
Server Components 数据获取:
- ✅ 直接调用 API/数据库
- ✅ 在服务器上执行
- ✅ 默认缓存
- ✅ 更好的性能和 SEO
- ❌ 不支持交互
Client Components 数据获取:
- ✅ 通过 API Routes
- ✅ 在浏览器上执行
- ✅ 支持实时更新和交互
- ✅ 灵活的状态管理
- ❌ 需要手动实现缓存
本项目模式:
- Client Component → Hook → API Route → Service → Database
- 安全、灵活、一致性好
下一步:阅读下一篇文章《流式响应实现》,学习 AI 应用中常用的流式输出技术。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。