03
Apr

Next 学习笔记(三):数据获取

avatar
ByKK
2025-04-03/7 Min Read
0
0
(AI总结) 在理解了服务端组件(RSC)之后,数据获取和提交的模式也随之发生了根本性的变化。旧的 useEffect + fetch 模式不再是首选,新的模式将数据获取和变更更多地放在服务端处理,以获得更好的性能和更简洁的代码。

服务端组件中的数据获取

  • 核心思想: 在服务端组件(RSC)中,可以直接使用 async/await 语法来获取数据。组件本身可以是一个异步函数。Next.js 扩展了原生的 fetch API,使其能自动处理请求的缓存和重新验证。

  • 与旧模式的对比:

    • 旧模式(客户端 useEffect): 页面 HTML 先加载 -> 页面 JS 再加载 -> useEffect 触发 -> fetch 开始 -> 渲染数据。这个过程会产生加载瀑布(request waterfall),用户会先看到一个 loading 状态。
    • 新模式(RSC async/await): 在服务端,数据获取和页面渲染可以并行发生。最终直接生成包含数据的完整 HTML 发送给浏览器。用户能更快地看到最终内容,对 SEO 也更友好。
  • 代码示例: 一个典型的 RSC 数据获取页面。

    // app/posts/page.tsx (这是一个服务端组件)
     
    interface Post {
      id: number;
      title: string;
    }
     
    // 组件函数本身就是 async
    export default async function PostsPage() {
      // 直接 await fetch 调用。就像在写后端代码一样。
      const res = await fetch('https://jsonplaceholder.typicode.com/posts');
      const posts: Post[] = await res.json();
     
      return (
        <div>
          <h1>Posts</h1>
          <ul>
            {posts.map(post => (
              <li key={post.id}>{post.title}</li>
            ))}
          </ul>
        </div>
      );
    }

缓存与重新验证 (Caching & Revalidation)

  • 核心概念: Next.js 扩展后的 fetch自动缓存所有请求。默认情况下,请求结果会被永久缓存,直到你手动让它失效。

  • 控制缓存行为: 可以通过 fetch 的第二个参数 options 对象来精细控制。

    1. 不缓存(动态渲染): 每次请求都重新获取数据,等同于旧的 getServerSideProps

      fetch('...', { cache: 'no-store' });
    2. 定时重新验证(ISR): 获取数据并缓存 n 秒。在 n 秒后,如果再有新请求,会先返回旧的缓存数据,同时在后台重新请求新数据,更新缓存。

      // 缓存 1 小时 (3600 秒)
      fetch('...', { next: { revalidate: 3600 } });
  • 按需重新验证(On-Demand Revalidation):

    • 问题场景: 在 CMS(内容管理系统)里发布了一篇新文章,希望博客立即显示出来,而不是等待 revalidate 的时间。

    • 解决方案: revalidateTagrevalidatePath。可以在一个服务端操作(如 API 路由或 Server Action)中调用这些函数,来精确地清除指定标签或路径的缓存。

    • 示例 (revalidateTag)

      1. fetch 时给数据打上标签:

        fetch('https://my-cms/posts', { next: { tags: ['posts'] } });
      2. 创建一个 API 路由,用于接收 CMS 的 webhook 通知:

        // app/api/revalidate/route.ts
        import { NextRequest, NextResponse } from 'next/server';
        import { revalidateTag } from 'next/cache';
         
        export async function POST(request: NextRequest) {
          const tag = request.nextUrl.searchParams.get('tag');
         
          if (!tag) {
            return NextResponse.json({ error: 'Tag is required' }, { status: 400 });
          }
         
          revalidateTag(tag); // 清除所有带 'posts' 标签的缓存
          return NextResponse.json({ revalidated: true, now: Date.now() });
        }

      现在,只要请求 POST /api/revalidate?tag=posts,所有相关页面的缓存就会立即失效,下次访问时会获取最新数据。

Server Actions: 服务端的数据变更

  • 核心概念: Server Action 是一些只能在服务端运行的函数,但它可以被客户端组件直接调用(例如,通过表单提交或按钮点击),而无需手动创建 API 路由。这是对数据变更(mutations)流程的巨大简化。

  • 如何定义: 在一个异步函数内部或一个独立文件的顶部加上 'use server'; 指令。

  • 示例:表单提交

    // 1. 在 Server Component 中定义 Action
    // app/page.tsx
    import { revalidatePath } from 'next/cache';
     
    export default function Page() {
      async function createItem(formData: FormData) {
        'use server'; // 声明这是一个 Server Action
     
        const itemName = formData.get('itemName') as string;
        // ... 假设这里是保存到数据库的逻辑
        await db.items.create({ name: itemName });
     
        // 清除当前页面的缓存,以便显示新添加的项
        revalidatePath('/');
      }
     
      return (
        <form action={createItem}>
          <input type="text" name="itemName" />
          <button type="submit">Add Item</button>
        </form>
      );
    }

    在这个例子中,当表单提交时,createItem 函数会在服务端执行,完成数据库操作,然后刷新页面数据,整个过程没有写一行 fetch 或 API 路由代码。

表单交互增强:useActionState

  • 问题场景: 当 Server Action 运行时,如何处理加载状态(pending),以及如何从服务端返回成功或错误消息并显示在界面上?

  • 解决方案: 使用 React 19 新增的 useActionState Hook(它取代了旧的 useFormState)。

  • 代码示例:带状态反馈的表单

    1. 修改 Action 函数

      Action 函数现在可以接收前一个状态,并返回新的状态。

      // lib/actions.ts
      'use server';
       
      export async function updateUser(previousState: any, formData: FormData) {
        const username = formData.get('username') as string;
       
        try {
          await db.user.update({ name: username });
          revalidatePath('/profile');
          return { status: 'success', message: 'Profile updated successfully!' };
        } catch (e) {
          return { status: 'error', message: 'Failed to update profile.' };
        }
      }
    2. 在客户端组件中使用 useActionState

      // components/ProfileForm.tsx
      'use client';
       
      import { useActionState } from 'react';
      import { updateUser } from '@/lib/actions';
       
      const initialState = { status: '', message: '' };
       
      export function ProfileForm() {
        // useActionState 返回 [state, formAction, isPending]
        const [state, formAction, isPending] = useActionState(updateUser, initialState);
       
        return (
          <form action={formAction}>
            <input type="text" name="username" defaultValue="KK" />
            <button type="submit" disabled={isPending}>
              {isPending ? 'Saving...' : 'Save'}
            </button>
       
            {state.status === 'success' && <p className="text-green-500">{state.message}</p>}
            {state.status === 'error' && <p className="text-red-500">{state.message}</p>}
          </form>
        );
      }

    现在,这个表单拥有了自动的 pending 状态管理和来自服务端的状态消息反馈,而无需手写任何 useStatefetch 逻辑。

🚀 数据获取最佳实践

并行数据获取

// ❌ 串行获取(慢)
export default async function PostPage({ params }: { params: { id: string } }) {
  const post = await fetch(`/api/posts/${params.id}`).then(r => r.json());
  const comments = await fetch(`/api/posts/${params.id}/comments`).then(r => r.json());
  
  return <PostWithComments post={post} comments={comments} />;
}
 
// ✅ 并行获取(快)
export default async function PostPage({ params }: { params: { id: string } }) {
  const [post, comments] = await Promise.all([
    fetch(`/api/posts/${params.id}`).then(r => r.json()),
    fetch(`/api/posts/${params.id}/comments`).then(r => r.json())
  ]);
  
  return <PostWithComments post={post} comments={comments} />;
}

错误处理与重试

// 健壮的数据获取函数
async function fetchWithRetry(url: string, retries = 3): Promise<any> {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      
      return await response.json();
    } catch (error) {
      console.warn(`Attempt ${i + 1} failed:`, error);
      
      if (i === retries - 1) throw error; // 最后一次重试失败时抛出错误
      
      // 指数退避
      await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
    }
  }
}
 
// 在组件中使用
export default async function ReliableDataPage() {
  try {
    const data = await fetchWithRetry('https://api.example.com/data');
    return <DataDisplay data={data} />;
  } catch (error) {
    return <ErrorMessage error={error.message} />;
  }
}

数据库查询优化

// ❌ N+1 查询问题
export default async function PostsPage() {
  const posts = await db.post.findMany();
  
  const postsWithAuthors = await Promise.all(
    posts.map(async (post) => ({
      ...post,
      author: await db.user.findUnique({ where: { id: post.authorId } })
    }))
  );
  
  return <PostsList posts={postsWithAuthors} />;
}
 
// ✅ 使用 include 一次性获取关联数据
export default async function PostsPage() {
  const postsWithAuthors = await db.post.findMany({
    include: {
      author: true,
      comments: {
        take: 5, // 只获取前5条评论
        orderBy: { createdAt: 'desc' }
      }
    }
  });
  
  return <PostsList posts={postsWithAuthors} />;
}

🛠️ Server Actions 进阶技巧

表单验证

// lib/validations.ts
import { z } from 'zod';
 
export const CreatePostSchema = z.object({
  title: z.string().min(1, 'Title is required').max(100),
  content: z.string().min(10, 'Content too short'),
  tags: z.string().optional()
});
 
// lib/actions.ts
'use server';
import { CreatePostSchema } from './validations';
 
export async function createPost(previousState: any, formData: FormData) {
  const rawData = {
    title: formData.get('title') as string,
    content: formData.get('content') as string,
    tags: formData.get('tags') as string
  };
  
  // 验证数据
  const validation = CreatePostSchema.safeParse(rawData);
  
  if (!validation.success) {
    return {
      status: 'error',
      errors: validation.error.flatten().fieldErrors,
      message: 'Validation failed'
    };
  }
  
  try {
    await db.post.create({ data: validation.data });
    revalidatePath('/posts');
    return { status: 'success', message: 'Post created!' };
  } catch (error) {
    return { status: 'error', message: 'Failed to create post' };
  }
}

渐进式增强

// components/DeleteButton.tsx
'use client';
import { useActionState, useOptimistic } from 'react';
import { deletePost } from '@/lib/actions';
 
export function DeleteButton({ postId, onDelete }: { postId: string, onDelete?: () => void }) {
  const [state, formAction, isPending] = useActionState(deletePost, { status: '', message: '' });
  
  // 乐观更新:立即响应用户操作
  const [optimisticDeleted, setOptimisticDeleted] = useOptimistic(false);
  
  if (optimisticDeleted) {
    return <span className="text-gray-500">Deleted...</span>;
  }
  
  return (
    <form action={async (formData) => {
      setOptimisticDeleted(true);
      onDelete?.(); // 立即从 UI 中移除
      await formAction(formData);
    }}>
      <input type="hidden" name="postId" value={postId} />
      <button 
        type="submit" 
        disabled={isPending}
        className="text-red-500 hover:text-red-700"
      >
        {isPending ? 'Deleting...' : 'Delete'}
      </button>
    </form>
  );
}

⚠️ 常见问题与调试

问题1:缓存过度积极

现象: 数据更新了但页面没有反映 调试:

// 在开发环境禁用缓存来测试
const data = await fetch('/api/data', { 
  cache: process.env.NODE_ENV === 'development' ? 'no-store' : 'default' 
});

问题2:Server Action 不生效

现象: 表单提交没有执行 Action 检查清单:

  • 确保函数标记了 'use server'
  • 确保在 form 的 action 属性中使用
  • 检查 Next.js 版本(需要 13.4+)

问题3:数据获取时间过长

调试工具:

export default async function SlowPage() {
  const start = Date.now();
  
  try {
    const data = await fetch('/api/slow-endpoint');
    const duration = Date.now() - start;
    
    console.log(`Data fetching took ${duration}ms`);
    return <DataDisplay data={data} />;
  } catch (error) {
    console.error('Data fetching failed:', error);
    throw error;
  }
}

💡 性能监控

添加性能追踪

// lib/performance.ts
export function withPerformanceTracking<T extends any[], R>(
  fn: (...args: T) => Promise<R>,
  name: string
) {
  return async (...args: T): Promise<R> => {
    const start = performance.now();
    try {
      const result = await fn(...args);
      const duration = performance.now() - start;
      
      // 发送到监控服务
      console.log(`[Performance] ${name}: ${duration.toFixed(2)}ms`);
      
      return result;
    } catch (error) {
      console.error(`[Performance] ${name} failed:`, error);
      throw error;
    }
  };
}
 
// 使用示例
const trackedFetch = withPerformanceTracking(
  async (url: string) => {
    const response = await fetch(url);
    return response.json();
  },
  'API_FETCH'
);

📝 最佳实践检查表

  • 优先使用 RSC 中的 async/await 进行数据获取
  • 合理设置缓存策略revalidate, no-store, tags
  • 并行获取相互独立的数据
  • 在 Server Actions 中添加错误处理和验证
  • 使用 useActionState 处理表单状态
  • 监控数据获取性能
  • 设置合理的超时时间
  • 为慢查询添加 loading 状态