数据获取开发手册
本文档说明在 Next.js 14 App Router 中如何发送请求、获取数据,以及服务端和客户端的差异与实现方式。
📋 目录
核心概念
服务端组件 vs 客户端组件
| 特性 | 服务端组件 | 客户端组件 |
|---|---|---|
| 执行环境 | 服务器 | 浏览器 |
是否需要 'use client' |
❌ 不需要 | ✅ 必须 |
| 可以使用 Hooks | ❌ 不可以 | ✅ 可以 |
| 可以直接使用 async/await | ✅ 可以 | ❌ 不可以(需要封装) |
| SEO 友好 | ✅ 是 | ⚠️ 需要额外处理 |
| 首屏加载速度 | ✅ 快 | ⚠️ 可能较慢 |
| 可以访问环境变量 | ✅ 所有环境变量 | ⚠️ 仅 NEXT_PUBLIC_* |
| 可以访问数据库/API | ✅ 可以 | ⚠️ 需要通过 API Route |
何时使用服务端组件
✅ 适合使用服务端组件:
- 需要 SEO 优化的页面
- 需要快速首屏加载
- 需要访问服务器资源(数据库、文件系统等)
- 需要访问敏感信息(API keys、数据库凭证等)
- 静态内容或变化不频繁的内容
✅ 适合使用客户端组件:
- 需要交互性(onClick、onChange 等事件)
- 需要使用 React Hooks(useState、useEffect 等)
- 需要使用浏览器 API(localStorage、window 等)
- 需要实时更新数据
- 需要用户交互触发的数据获取
服务端数据获取
基本用法
服务端组件可以直接使用 async/await 来获取数据:
tsx
// app/page.tsx - 服务端组件(默认)
import { post } from '@/utils/request';
// 使用 cache 包装函数以启用请求去重和缓存
import { cache } from 'react';
const getData = cache(async () => {
const response = await post('/api/data', { id: 1 });
return response.data;
});
export default async function Page() {
// 直接使用 await
const data = await getData();
return (
<div>
<h1>{data.title}</h1>
</div>
);
}
实际示例
示例 1: 获取模板详情
tsx
// app/template/[id]/page.tsx
import { post } from '@/utils/request';
import { cache } from 'react';
import { notFound } from 'next/navigation';
const getTemplateDetail = cache(async (dataId: string, locale: string) => {
const data = {
space_id: process.env.NEXT_PUBLIC_TEMPLATE_SERVICE_SPACE_ID,
project_id: process.env.NEXT_PUBLIC_TEMPLATE_SERVICE_PROJECT_ID,
form_id: process.env.NEXT_PUBLIC_TEMPLATE_SERVICE_FORM_ID,
data_id: dataId,
locale: locale
};
const targetUrl = `${process.env.NEXT_PUBLIC_TEMPLATE_SERVICE_HOST}/api/v1/data/view`;
try {
const response = await post(targetUrl, data, {
options: {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.NEXT_PUBLIC_TEMPLATE_SERVICE_TOKEN}`
}
}
});
if (response.success && response.data) {
return response.data;
}
return null;
} catch (error) {
console.error('获取模板详情失败:', error);
return null;
}
});
export default async function TemplatePage({ params }: { params: { id: string; locale: string } }) {
const { id, locale } = params;
const templateData = await getTemplateDetail(id, locale);
if (!templateData) {
return notFound();
}
return (
<div>
<h1>{templateData.title}</h1>
<p>{templateData.description}</p>
</div>
);
}
示例 2: 并行获取多个数据
tsx
import { post } from '@/utils/request';
import { cache } from 'react';
const getTags = cache(async () => {
const response = await post('/api/tags', {});
return response.data;
});
const getCategories = cache(async () => {
const response = await post('/api/categories', {});
return response.data;
});
export default async function Page() {
// 并行获取数据(使用 Promise.all)
const [tags, categories] = await Promise.all([
getTags(),
getCategories()
]);
return (
<div>
<TagsList tags={tags} />
<CategoriesList categories={categories} />
</div>
);
}
示例 3: 在 generateMetadata 中获取数据
tsx
import { Metadata } from 'next';
import { post } from '@/utils/request';
import { cache } from 'react';
const getTemplateDetail = cache(async (id: string) => {
const response = await post('/api/template', { id });
return response.data;
});
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
const template = await getTemplateDetail(params.id);
return {
title: template?.title || '默认标题',
description: template?.description || '默认描述'
};
}
export default async function Page({ params }: { params: { id: string } }) {
const template = await getTemplateDetail(params.id);
return <div>{/* 渲染内容 */}</div>;
}
使用 cache 函数
使用 cache 函数可以:
- 请求去重:相同参数的请求只会执行一次
- 自动缓存:在同一个渲染周期内复用结果
tsx
import { cache } from 'react';
import { post } from '@/utils/request';
// ✅ 推荐:使用 cache 包装
const getData = cache(async (id: string) => {
const response = await post('/api/data', { id });
return response.data;
});
// ❌ 不推荐:直接定义函数
// const getData = async (id: string) => { ... }
错误处理
tsx
import { post } from '@/utils/request';
import { notFound, redirect } from 'next/navigation';
export default async function Page({ params }: { params: { id: string } }) {
const response = await post('/api/data', { id: params.id });
// 处理请求失败
if (!response.success) {
if (response.response?.status === 404) {
return notFound(); // 显示 404 页面
}
if (response.response?.status === 401) {
redirect('/login'); // 重定向到登录页
}
// 显示错误信息
return <div>错误: {response.error}</div>;
}
return <div>{/* 渲染数据 */}</div>;
}
客户端数据获取
基本用法
客户端组件需要使用 React Hooks 来获取数据:
tsx
'use client';
import { useState, useEffect } from 'react';
import { post } from '@/utils/request';
export default function ClientComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await post('/api/data', { id: 1 });
if (response.success) {
setData(response.data);
setError(null);
} else {
setError(response.error);
}
} catch (err) {
setError('请求失败');
} finally {
setLoading(false);
}
};
fetchData();
}, []); // 空依赖数组表示只在组件挂载时执行
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error}</div>;
if (!data) return <div>暂无数据</div>;
return <div>{/* 渲染数据 */}</div>;
}
实际示例
示例 1: 基础数据获取
tsx
'use client';
import { useState, useEffect } from 'react';
import { post } from '@/utils/request';
interface User {
id: string;
name: string;
email: string;
}
export default function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await post<User>('/api/user', { id: userId });
if (response.success && response.data) {
setUser(response.data);
setError(null);
} else {
setError(response.error || '获取用户信息失败');
}
} catch (err) {
setError('请求失败');
} finally {
setLoading(false);
}
};
if (userId) {
fetchUser();
}
}, [userId]); // 当 userId 变化时重新获取
if (loading) return <div className="loading">加载中...</div>;
if (error) return <div className="error">错误: {error}</div>;
if (!user) return <div>用户不存在</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
示例 2: 用户交互触发的数据获取
tsx
'use client';
import { useState } from 'react';
import { post } from '@/utils/request';
export default function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const handleSearch = async () => {
if (!query.trim()) return;
try {
setLoading(true);
const response = await post('/api/search', { q: query });
if (response.success && response.data) {
setResults(response.data);
}
} catch (err) {
console.error('搜索失败:', err);
} finally {
setLoading(false);
}
};
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
/>
<button onClick={handleSearch} disabled={loading}>
{loading ? '搜索中...' : '搜索'}
</button>
<ul>
{results.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
示例 3: 可取消的请求
tsx
'use client';
import { useState, useEffect, useRef } from 'react';
import { post } from '@/utils/request';
import { Canceler } from 'axios';
export default function DataFetcher({ id }: { id: string }) {
const [data, setData] = useState(null);
const cancelRef = useRef<Canceler | null>(null);
useEffect(() => {
const fetchData = async () => {
const response = await post('/api/data', { id }, {
cancelExecutor: (canceler) => {
cancelRef.current = canceler;
}
});
if (response.success) {
setData(response.data);
}
};
fetchData();
// 清理函数:组件卸载时取消请求
return () => {
if (cancelRef.current) {
cancelRef.current('组件卸载,取消请求');
}
};
}, [id]);
return <div>{/* 渲染数据 */}</div>;
}
示例 4: 使用自定义 Hook
tsx
// hooks/useDataFetch.ts
'use client';
import { useState, useEffect } from 'react';
import { post } from '@/utils/request';
interface UseDataFetchResult<T> {
data: T | null;
loading: boolean;
error: string | null;
refetch: () => void;
}
export function useDataFetch<T>(
url: string,
params?: object,
options?: { enabled?: boolean }
): UseDataFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await post<T>(url, params);
if (response.success && response.data) {
setData(response.data);
} else {
setError(response.error || '请求失败');
}
} catch (err) {
setError('请求失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
if (options?.enabled !== false) {
fetchData();
}
}, [url, JSON.stringify(params)]);
return {
data,
loading,
error,
refetch: fetchData
};
}
// 使用自定义 Hook
'use client';
import { useDataFetch } from '@/hooks/useDataFetch';
export default function MyComponent({ id }: { id: string }) {
const { data, loading, error, refetch } = useDataFetch('/api/data', { id });
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error}</div>;
return (
<div>
<button onClick={refetch}>刷新</button>
{/* 渲染数据 */}
</div>
);
}
对比总结
代码对比
| 场景 | 服务端组件 | 客户端组件 |
|---|---|---|
| 基本结构 | tsx<br/>export default async function Page() {<br/> const data = await fetchData();<br/> return <div>{data}</div>;<br/>}<br/> |
tsx<br/>'use client';<br/>export default function Page() {<br/> const [data, setData] = useState(null);<br/> useEffect(() => {<br/> fetchData().then(setData);<br/> }, []);<br/> return <div>{data}</div>;<br/>}<br/> |
| 加载状态 | 自动处理(SSR) | 需要手动管理 loading 状态 |
| 错误处理 | 使用 notFound() 或 redirect() |
使用 error 状态和条件渲染 |
| 重新获取 | 页面刷新或重新渲染 | 使用 refetch 函数或更新依赖 |
性能对比
| 指标 | 服务端组件 | 客户端组件 |
|---|---|---|
| 首屏加载 | ✅ 快(SSR) | ⚠️ 较慢(需要等待 JS 加载和执行) |
| SEO | ✅ 友好 | ⚠️ 需要额外处理 |
| 交互性 | ❌ 无 | ✅ 有 |
| 数据实时性 | ⚠️ 需要刷新页面 | ✅ 可以实时更新 |
最佳实践
1. 优先使用服务端组件
tsx
// ✅ 推荐:服务端组件获取数据
export default async function Page() {
const data = await getData();
return <DataDisplay data={data} />;
}
// ⚠️ 仅在需要交互时使用客户端组件
'use client';
export default function InteractiveComponent() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
2. 组合使用服务端和客户端组件
tsx
// 服务端组件:获取数据
export default async function Page() {
const data = await getData();
return (
<div>
{/* 服务端渲染静态内容 */}
<StaticContent data={data} />
{/* 客户端组件处理交互 */}
<InteractiveComponent initialData={data} />
</div>
);
}
// 客户端组件:处理交互
'use client';
function InteractiveComponent({ initialData }: { initialData: any }) {
const [data, setData] = useState(initialData);
// ... 交互逻辑
}
3. 使用 cache 优化服务端请求
tsx
import { cache } from 'react';
// ✅ 使用 cache 避免重复请求
const getData = cache(async (id: string) => {
const response = await post('/api/data', { id });
return response.data;
});
4. 合理处理加载和错误状态
tsx
// 服务端组件
export default async function Page() {
const data = await getData();
if (!data) return notFound();
return <div>{/* 渲染 */}</div>;
}
// 客户端组件
'use client';
export default function Component() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// ... 获取数据逻辑
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!data) return <EmptyState />;
return <div>{/* 渲染数据 */}</div>;
}
5. 避免在客户端组件中直接访问环境变量
tsx
// ❌ 错误:客户端组件无法访问服务器环境变量
'use client';
export default function Component() {
const apiKey = process.env.API_KEY; // undefined
}
// ✅ 正确:使用 NEXT_PUBLIC_ 前缀
// .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
'use client';
export default function Component() {
const apiUrl = process.env.NEXT_PUBLIC_API_URL; // ✅ 可以访问
}
// ✅ 更好的方式:在服务端组件中获取,传递给客户端组件
export default async function Page() {
const apiKey = process.env.API_KEY; // ✅ 服务端可以访问
return <ClientComponent apiKey={apiKey} />;
}
常见问题
Q1: 什么时候应该使用服务端组件?
A: 当满足以下条件时使用服务端组件:
- 需要 SEO 优化
- 需要快速首屏加载
- 需要访问服务器资源
- 数据变化不频繁
Q2: 服务端组件可以使用 useState 和 useEffect 吗?
A: ❌ 不可以。服务端组件不能使用任何 React Hooks。如果需要使用 Hooks,必须使用客户端组件(添加 'use client')。
Q3: 如何在服务端组件中处理用户交互?
A: 将交互部分提取为客户端组件,服务端组件只负责数据获取:
tsx
// 服务端组件
export default async function Page() {
const data = await getData();
return <InteractiveButton data={data} />;
}
// 客户端组件
'use client';
function InteractiveButton({ data }: { data: any }) {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Q4: 客户端组件可以访问所有环境变量吗?
A: ❌ 不可以。客户端组件只能访问以 NEXT_PUBLIC_ 开头的环境变量。敏感信息(如 API keys)应该只在服务端组件中使用。
Q5: 如何实现数据刷新?
A:
- 服务端组件 :使用
revalidatePath()或revalidateTag(),或者让用户刷新页面 - 客户端组件 :使用
refetch函数或更新useEffect的依赖
tsx
// 客户端组件刷新示例
'use client';
export default function Component() {
const [data, setData] = useState(null);
const [refreshKey, setRefreshKey] = useState(0);
useEffect(() => {
fetchData().then(setData);
}, [refreshKey]); // 当 refreshKey 变化时重新获取
const handleRefresh = () => {
setRefreshKey(prev => prev + 1);
};
return <button onClick={handleRefresh}>刷新</button>;
}
Q6: 如何处理请求取消?
A: 在客户端组件中使用 cancelExecutor:
tsx
'use client';
import { useEffect, useRef } from 'react';
import { post } from '@/utils/request';
import { Canceler } from 'axios';
export default function Component() {
const cancelRef = useRef<Canceler | null>(null);
useEffect(() => {
post('/api/data', {}, {
cancelExecutor: (canceler) => {
cancelRef.current = canceler;
}
});
return () => {
if (cancelRef.current) {
cancelRef.current('组件卸载');
}
};
}, []);
return <div>...</div>;
}