Next.js 14 App Router数据获取开发手册

数据获取开发手册

本文档说明在 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>;
}

📚 相关资源

相关推荐
king王一帅20 小时前
Incremark Solid 版本上线:Vue/React/Svelte/Solid 四大框架,统一体验
前端·javascript·人工智能
SmartRadio1 天前
CH585M+MK8000、DW1000 (UWB)+W25Q16的低功耗室内定位设计
c语言·开发语言·uwb
rfidunion1 天前
QT5.7.0编译移植
开发语言·qt
rit84324991 天前
MATLAB对组合巴克码抗干扰仿真的实现方案
开发语言·matlab
智航GIS1 天前
10.4 Selenium:Web 自动化测试框架
前端·python·selenium·测试工具
前端工作日常1 天前
我学习到的A2UI概念
前端
大、男人1 天前
python之asynccontextmanager学习
开发语言·python·学习
hqwest1 天前
码上通QT实战08--导航按钮切换界面
开发语言·qt·slot·信号与槽·connect·signals·emit
徐同保1 天前
为什么修改 .gitignore 后还能提交
前端
一只小bit1 天前
Qt 常用控件详解:按钮类 / 显示类 / 输入类属性、信号与实战示例
前端·c++·qt·gui