Skip to content

第7章:数据获取(Next.js 核心难点,实战高频)

7.1 服务端数据获取(Server Components 中使用,推荐)

async/await 直接获取数据(无需 useEffect)

在 Server Components 中,可以直接使用 async/await 获取数据,无需使用 useEffect

示例

jsx
// app/blog/page.js
// 服务端组件
export default async function BlogPage() {
  // 直接在服务端获取数据
  const response = await fetch('https://api.example.com/posts');
  const posts = await response.json();
  
  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.excerpt}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

数据获取缓存策略(缓存、重新验证、无缓存)

Next.js 提供了灵活的缓存策略,用于控制数据获取的缓存行为。

缓存(默认)

默认情况下,Next.js 会缓存 fetch 请求的结果。

示例

jsx
// 缓存数据(默认)
const response = await fetch('https://api.example.com/posts');

重新验证

可以使用 next: { revalidate: number } 选项设置重新验证时间。

示例

jsx
// 每 60 秒重新验证一次
const response = await fetch('https://api.example.com/posts', {
  next: {
    revalidate: 60,
  },
});

无缓存

可以使用 cache: 'no-store' 选项禁用缓存。

示例

jsx
// 禁用缓存
const response = await fetch('https://api.example.com/posts', {
  cache: 'no-store',
});

强制缓存

可以使用 cache: 'force-cache' 选项强制缓存。

示例

jsx
// 强制缓存
const response = await fetch('https://api.example.com/posts', {
  cache: 'force-cache',
});

7.2 客户端数据获取(Client Components 中使用)

useEffect + fetch/axios 获取数据

在 Client Components 中,可以使用 useEffectfetchaxios 获取数据。

示例

jsx
// app/components/ClientData.jsx
'use client';

import { useState, useEffect } from 'react';

export default function ClientData() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/data');
        if (!response.ok) {
          throw new Error('Failed to fetch data');
        }
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
  }, []);
  
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  
  return (
    <div>
      <h1>Client Data</h1>
      <p>{data.message}</p>
    </div>
  );
}

SWR/React Query 集成(数据缓存、自动刷新,实战推荐)

SWR 和 React Query 是两个流行的数据获取库,提供了数据缓存、自动刷新等功能。

SWR 集成

安装

bash
npm install swr

示例

jsx
// app/components/SWRData.jsx
'use client';

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then((res) => res.json());

export default function SWRData() {
  const { data, error, isLoading } = useSWR('https://api.example.com/data', fetcher);
  
  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  
  return (
    <div>
      <h1>SWR Data</h1>
      <p>{data.message}</p>
    </div>
  );
}

React Query 集成

安装

bash
npm install @tanstack/react-query

配置

jsx
// app/providers.jsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

export default function Providers({ children }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

// app/layout.js
import Providers from './providers';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

示例

jsx
// app/components/ReactQueryData.jsx
'use client';

import { useQuery } from '@tanstack/react-query';

const fetchData = async () => {
  const response = await fetch('https://api.example.com/data');
  if (!response.ok) {
    throw new Error('Failed to fetch data');
  }
  return response.json();
};

export default function ReactQueryData() {
  const { data, error, isLoading } = useQuery({
    queryKey: ['data'],
    queryFn: fetchData,
  });
  
  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  
  return (
    <div>
      <h1>React Query Data</h1>
      <p>{data.message}</p>
    </div>
  );
}

7.3 接口请求封装(统一请求实例、请求拦截器、响应拦截器)

统一请求实例

创建一个统一的请求实例,方便管理 API 请求。

示例

javascript
// lib/api.js
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://api.example.com';

class Api {
  constructor() {
    this.baseURL = API_BASE_URL;
  }
  
  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    
    const defaultOptions = {
      headers: {
        'Content-Type': 'application/json',
      },
    };
    
    const mergedOptions = {
      ...defaultOptions,
      ...options,
      headers: {
        ...defaultOptions.headers,
        ...options.headers,
      },
    };
    
    try {
      const response = await fetch(url, mergedOptions);
      
      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw new Error(errorData.message || `Request failed with status ${response.status}`);
      }
      
      return await response.json();
    } catch (error) {
      console.error('API request error:', error);
      throw error;
    }
  }
  
  get(endpoint, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'GET',
    });
  }
  
  post(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'POST',
      body: JSON.stringify(data),
    });
  }
  
  put(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }
  
  delete(endpoint, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'DELETE',
    });
  }
}

export default new Api();

使用封装的 API

示例

jsx
// app/blog/page.js
import api from '@/lib/api';

export default async function BlogPage() {
  // 使用封装的 API 获取数据
  const posts = await api.get('/posts');
  
  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.excerpt}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

// app/components/CommentForm.jsx
'use client';

import { useState } from 'react';
import api from '@/lib/api';

export default function CommentForm({ postId }) {
  const [comment, setComment] = useState('');
  const [submitting, setSubmitting] = useState(false);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setSubmitting(true);
    
    try {
      await api.post('/comments', { postId, comment });
      setComment('');
      alert('Comment submitted successfully!');
    } catch (error) {
      alert(`Error: ${error.message}`);
    } finally {
      setSubmitting(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={comment}
        onChange={(e) => setComment(e.target.value)}
        placeholder="Write a comment..."
        rows={4}
      />
      <button type="submit" disabled={submitting}>
        {submitting ? 'Submitting...' : 'Submit Comment'}
      </button>
    </form>
  );
}

7.4 跨域问题解决方案(Next.js 内置代理配置)

Next.js 提供了内置的 API 路由,可以作为代理解决跨域问题。

创建 API 代理

示例

javascript
// app/api/proxy/[...path]/route.js
import { NextResponse } from 'next/server';

const API_BASE_URL = process.env.API_BASE_URL || 'https://api.example.com';

export async function GET(request, { params }) {
  const { path } = params;
  const url = `${API_BASE_URL}/${path.join('/')}`;
  
  try {
    const response = await fetch(url, {
      headers: {
        'Content-Type': 'application/json',
      },
    });
    
    const data = await response.json();
    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: error.message },
      { status: 500 }
    );
  }
}

export async function POST(request, { params }) {
  const { path } = params;
  const url = `${API_BASE_URL}/${path.join('/')}`;
  const body = await request.json();
  
  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });
    
    const data = await response.json();
    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: error.message },
      { status: 500 }
    );
  }
}

// 可以根据需要添加 PUT、DELETE 等方法

使用代理 API

示例

jsx
// app/blog/page.js
import api from '@/lib/api';

export default async function BlogPage() {
  // 使用代理 API 获取数据
  const posts = await api.get('/api/proxy/posts');
  
  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.excerpt}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

7.5 静态生成(SSG)与服务端渲染(SSR)、增量静态再生(ISR)

三种渲染模式核心区别与适用场景

渲染模式生成时机优势适用场景
静态生成(SSG)构建时极致性能,无需服务器内容不频繁变化的页面,如博客文章、产品详情页
服务端渲染(SSR)请求时实时数据,更好的 SEO内容频繁变化的页面,如首页、仪表盘
增量静态再生(ISR)构建时 + 请求时结合 SSG 和 SSR 的优点内容定期更新的页面,如新闻列表、产品列表

静态生成(generateStaticParams 函数使用)

在动态路由中,使用 generateStaticParams 函数生成静态路径。

示例

jsx
// app/blog/[id]/page.js
export async function generateStaticParams() {
  // 获取所有博客文章 ID
  const response = await fetch('https://api.example.com/posts');
  const posts = await response.json();
  
  return posts.map((post) => ({
    id: post.id.toString(),
  }));
}

export default async function BlogPost({ params }) {
  const { id } = params;
  
  const response = await fetch(`https://api.example.com/posts/${id}`);
  const post = await response.json();
  
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

增量静态再生(revalidate 配置,优化静态页面更新)

使用 revalidate 选项实现增量静态再生。

示例

jsx
// app/blog/page.js
export default async function BlogPage() {
  // 每 60 秒重新验证一次
  const response = await fetch('https://api.example.com/posts', {
    next: {
      revalidate: 60,
    },
  });
  
  const posts = await response.json();
  
  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.excerpt}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

小结

本章介绍了 Next.js 中的数据获取方法,包括服务端数据获取、客户端数据获取、接口请求封装、跨域问题解决方案以及三种渲染模式。通过本章的学习,你应该已经掌握了:

  1. 服务端数据获取:在 Server Components 中使用 async/await 直接获取数据
  2. 数据获取缓存策略:缓存、重新验证、无缓存
  3. 客户端数据获取:使用 useEffect + fetch/axios 或 SWR/React Query
  4. 接口请求封装:创建统一的请求实例,添加请求和响应拦截器
  5. 跨域问题解决方案:使用 Next.js 内置的 API 路由作为代理
  6. 三种渲染模式:静态生成(SSG)、服务端渲染(SSR)、增量静态再生(ISR)

数据获取是 Next.js 开发中的核心难点之一,选择合适的数据获取方式和渲染模式对于构建高性能的 Next.js 应用至关重要。在实际开发中,建议:

  • 对于无交互、数据展示的页面,使用服务端数据获取
  • 对于需要交互的组件,使用客户端数据获取
  • 对于内容不频繁变化的页面,使用静态生成(SSG)
  • 对于内容频繁变化的页面,使用服务端渲染(SSR)
  • 对于内容定期更新的页面,使用增量静态再生(ISR)

接下来,我们将学习 Next.js 的状态管理,包括客户端状态管理和服务端状态管理。

© 2026 编程马·菜鸟教程 版权所有