Appearance
第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 中,可以使用 useEffect 和 fetch 或 axios 获取数据。
示例:
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 中的数据获取方法,包括服务端数据获取、客户端数据获取、接口请求封装、跨域问题解决方案以及三种渲染模式。通过本章的学习,你应该已经掌握了:
- 服务端数据获取:在 Server Components 中使用 async/await 直接获取数据
- 数据获取缓存策略:缓存、重新验证、无缓存
- 客户端数据获取:使用 useEffect + fetch/axios 或 SWR/React Query
- 接口请求封装:创建统一的请求实例,添加请求和响应拦截器
- 跨域问题解决方案:使用 Next.js 内置的 API 路由作为代理
- 三种渲染模式:静态生成(SSG)、服务端渲染(SSR)、增量静态再生(ISR)
数据获取是 Next.js 开发中的核心难点之一,选择合适的数据获取方式和渲染模式对于构建高性能的 Next.js 应用至关重要。在实际开发中,建议:
- 对于无交互、数据展示的页面,使用服务端数据获取
- 对于需要交互的组件,使用客户端数据获取
- 对于内容不频繁变化的页面,使用静态生成(SSG)
- 对于内容频繁变化的页面,使用服务端渲染(SSR)
- 对于内容定期更新的页面,使用增量静态再生(ISR)
接下来,我们将学习 Next.js 的状态管理,包括客户端状态管理和服务端状态管理。
