数据获取模式

学习 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 架构模式

本项目使用 三层架构进行数据获取:

流程

  1. Client Component 使用 Hook
  2. Hook 调用内部 API (/api/xxx)
  3. API Route 调用 Service Layer
  4. Service Layer 调用 Database Layer
  5. 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>
  );
}

Hookapp/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 Routeapp/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 Layerapp/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 本项目为什么选择这种模式?

  1. 安全性

    • API 密钥保存在服务器
    • 认证逻辑在服务器
    • 用户只能访问自己的数据
  2. 灵活性

    • Client Component 可以实时更新
    • 支持用户交互
    • 复杂的状态管理
  3. 一致性

    • 统一的数据访问接口
    • 复用 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,
  };
}

💡 练习题

  1. 选择题:以下哪种数据获取方式性能更好?

    • A. Client Components 直接调用外部 API
    • B. Server Components 直接调用外部 API
    • C. Client Components 通过 API Routes
    • D. Server Components 通过 API Routes
  2. 代码题:创建一个 Server Component,显示用户列表,并设置 1 小时的缓存。

  3. 分析题:查看本项目的 app/hooks/useSessionManager.ts,说明它如何获取和管理会话数据。

  4. 实践题:实现一个博客应用的数据获取:

    • Server Component:文章列表页(缓存 5 分钟)
    • Client Component:文章详情页(不缓存)
    • Hook:封装文章数据获取逻辑

📚 参考资源

官方文档

本项目相关文件


✅ 总结

Server Components 数据获取

  • ✅ 直接调用 API/数据库
  • ✅ 在服务器上执行
  • ✅ 默认缓存
  • ✅ 更好的性能和 SEO
  • ❌ 不支持交互

Client Components 数据获取

  • ✅ 通过 API Routes
  • ✅ 在浏览器上执行
  • ✅ 支持实时更新和交互
  • ✅ 灵活的状态管理
  • ❌ 需要手动实现缓存

本项目模式

  • Client Component → Hook → API Route → Service → Database
  • 安全、灵活、一致性好

下一步:阅读下一篇文章《流式响应实现》,学习 AI 应用中常用的流式输出技术。

登录以继续阅读

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

立即登录

On this page