数据获取
深入理解 Next.js 数据获取,包括 Server Components、Client Components、流式传输、fetch API 扩展和数据安全最佳实践
Server Components 数据获取
Server Components 可以使用任何异步 I/O 来获取数据,包括:
fetchAPI- ORM 或数据库
- 使用 Node.js API(如
fs)从文件系统读取
使用 fetch API
将组件转换为异步函数,并 await fetch 调用:
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}fetch 缓存行为
fetch 响应默认不会被缓存。但是,Next.js 会预渲染路由,输出会被缓存以提高性能。如果要选择动态渲染,使用 { cache: 'no-store' } 选项:
const data = await fetch('https://api.vercel.app/blog', {
cache: 'no-store'
})fetch 选项
Next.js 扩展了 Web fetch() API,允许每个请求设置自己的持久化缓存和重新验证语义。
options.cache
配置请求如何与 Next.js Data Cache 交互:
fetch('https://...', { cache: 'force-cache' | 'no-store' })auto no cache(默认):在开发环境中每次请求都从远程服务器获取资源,但在next build期间只获取一次,因为路由将被静态预渲染force-cache:Next.js 在 Data Cache 中查找匹配的请求- 如果有匹配且新鲜,将从缓存返回
- 如果没有匹配或陈旧,Next.js 将从远程服务器获取资源并使用下载的资源更新缓存
no-store:Next.js 在每次请求时从远程服务器获取资源,不查看缓存,也不使用下载的资源更新缓存
options.next.revalidate
设置资源的缓存生命周期(以秒为单位):
fetch('https://...', { next: { revalidate: 3600 } })false:无限期缓存资源,语义上等同于revalidate: Infinity0:防止资源被缓存number:指定资源的缓存生命周期应为最多 n 秒
options.next.tags
设置资源的缓存标签,然后可以使用 revalidateTag 按需重新验证数据:
fetch('https://...', { next: { tags: ['collection'] } })最大标签长度为 256 个字符,最大标签项数为 128。
使用 ORM 或数据库
可以直接在 Server Components 中调用数据库或 ORM:
import { db } from '@/lib/db'
export default async function Page() {
const users = await db.user.findMany()
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}并行和顺序数据获取
顺序数据获取
如果需要一个请求的结果来进行另一个请求,可以按顺序获取数据:
export default async function Page({
params,
}: {
params: Promise<{ username: string }>
}) {
const { username } = await params
// 等待 artist 数据
const artist = await getArtist(username)
// 使用 artist.id 获取 albums
const albums = await getAlbums(artist.id)
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
)
}并行数据获取
为了最小化瀑布流,可以并行获取数据:
export default async function Page({
params,
}: {
params: Promise<{ username: string }>
}) {
const { username } = await params
// 并行启动两个请求
const artistData = getArtist(username)
const albumsData = getAlbums(username)
// 等待两个 Promise 解决
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
)
}Client Components 数据获取
在 Client Components 中获取数据有两种推荐方式:
使用第三方库
推荐使用 SWR 或 TanStack Query 等库。这些库提供了自己的 API 用于记忆请求、缓存、重新验证和变更数据。
SWR 示例
'use client'
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then((r) => r.json())
export default function Page() {
const { data, error, isLoading } = useSWR(
'https://api.vercel.app/blog',
fetcher
)
if (isLoading) return <p>Loading...</p>
if (error) return <p>Failed to load.</p>
return (
<ul>
{data.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}TanStack Query 示例
'use client'
import { useQuery } from '@tanstack/react-query'
export default function Page() {
const { data, error, isLoading } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const res = await fetch('https://api.vercel.app/blog')
return res.json()
},
})
if (isLoading) return <p>Loading...</p>
if (error) return <p>Failed to load.</p>
return (
<ul>
{data.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}使用 React 的 use hook
React 的 use hook 接受一个 Promise,并在 Promise 解决之前显示 Suspense 后备内容:
'use client'
import { use, Suspense } from 'react'
function Posts({ postsPromise }: { postsPromise: Promise<any[]> }) {
const posts = use(postsPromise)
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export default function Page() {
const postsPromise = fetch('https://api.vercel.app/blog').then((res) =>
res.json()
)
return (
<Suspense fallback={<p>Loading posts...</p>}>
<Posts postsPromise={postsPromise} />
</Suspense>
)
}流式传输(Streaming)
流式传输允许你逐步从服务器渲染 UI。工作被分成块,并在准备就绪时流式传输到客户端。这使用户可以在整个内容完成渲染之前立即看到页面的部分内容。
使用 loading.js
loading.js 文件帮助你使用 React Suspense 创建有意义的加载 UI。通过此约定,你可以在路由段的内容流式传输时从服务器显示即时加载状态。新内容在完成后自动交换。
export default function Loading() {
return <p>Loading...</p>
}即时加载状态
即时加载状态是在导航时立即显示的后备 UI。你可以预渲染加载指示器,如骨架屏和旋转器。
export default function Loading() {
return <DashboardSkeleton />
}使用 Suspense 手动流式传输
除了 loading.js,你还可以为自己的 UI 组件手动创建 Suspense 边界:
import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
export default function Posts() {
return (
<section>
<Suspense fallback={<p>Loading feed...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>
<Weather />
</Suspense>
</section>
)
}使用 Suspense 的好处:
- 流式服务器渲染:从服务器到客户端逐步渲染 HTML
- 选择性水合:React 根据用户交互优先考虑哪些组件首先变为交互式
使用 use hook 流式传输数据
use hook 可以与 Suspense 结合使用来流式传输数据:
import { Suspense } from 'react'
import { use } from 'react'
async function fetchPosts() {
const res = await fetch('https://api.vercel.app/blog')
return res.json()
}
function Posts({ postsPromise }: { postsPromise: Promise<any[]> }) {
const posts = use(postsPromise)
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export default function Page() {
const postsPromise = fetchPosts()
return (
<Suspense fallback={<p>Loading posts...</p>}>
<Posts postsPromise={postsPromise} />
</Suspense>
)
}数据获取模式
在服务器上并行获取数据
为了最小化客户端-服务器瀑布流,建议在服务器上并行获取数据:
import Albums from './albums'
async function getArtist(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getAlbums(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`)
return res.json()
}
export default async function Page({
params,
}: {
params: Promise<{ username: string }>
}) {
const { username } = await params
const artistData = getArtist(username)
const albumsData = getAlbums(username)
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
)
}预加载数据
防止瀑布流的另一种方法是使用预加载模式,创建一个实用函数,你可以在阻塞请求之前急切地调用它:
import { getItem } from '@/utils/get-item'
export async function Item({ id }: { id: string }) {
const result = await getItem(id)
return <div>{result.name}</div>
}import { cache } from 'react'
import 'server-only'
export const preload = (id: string) => {
void getItem(id)
}
export const getItem = cache(async (id: string) => {
const res = await fetch(`https://api.example.com/items/${id}`)
return res.json()
})import { preload, getItem } from '@/utils/get-item'
import Item from '@/components/Item'
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
// 开始加载项目数据
preload(id)
// 执行另一个异步任务
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}使用 React cache 缓存数据
React 的 cache 函数允许你记忆函数的返回值,使你可以多次调用同一个函数但只执行一次:
import { cache } from 'react'
import 'server-only'
export const getItem = cache(async (id: string) => {
const res = await fetch(`https://api.example.com/items/${id}`)
return res.json()
})现在可以在多个组件中调用 getItem,但只会向数据库发出一次请求:
import { getItem } from '@/utils/get-item'
export default async function Layout({
params,
children,
}: {
params: Promise<{ id: string }>
children: React.ReactNode
}) {
const { id } = await params
const item = await getItem(id)
return (
<>
<h1>{item.name}</h1>
{children}
</>
)
}import { getItem } from '@/utils/get-item'
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
// 这不会发出第二次请求
const item = await getItem(id)
return <div>{item.description}</div>
}数据安全
React Server Components 改进了性能并简化了数据获取,但也改变了数据访问的位置和方式,改变了处理前端应用程序数据的一些传统安全假设。
数据获取方法
根据项目的规模和年龄,推荐三种主要的数据获取方法:
1. 外部 HTTP API
适用于现有的大型应用程序和组织。在采用 Server Components 时,应遵循零信任模型。可以继续从 Server Components 调用现有的 API 端点(如 REST 或 GraphQL):
import { cookies } from 'next/headers'
export default async function Page() {
const cookieStore = await cookies()
const token = cookieStore.get('AUTH_TOKEN')?.value
const res = await fetch('https://api.example.com/profile', {
headers: {
Cookie: `AUTH_TOKEN=${token}`,
},
})
const data = await res.json()
return <div>{data.name}</div>
}这种方法适用于:
- 已经有安全实践
- 独立的后端团队使用其他语言或独立管理 API
2. 数据访问层(推荐用于新项目)
数据访问层(DAL)是一个集中式的位置,用于与数据库或其他数据源交互。它提供了一个单一的位置来添加授权检查和数据验证。
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/lib/session'
import { db } from '@/lib/db'
export async function verifySession() {
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
if (!session?.userId) {
throw new Error('Unauthorized')
}
return { userId: session.userId }
}
export async function getUser() {
const session = await verifySession()
try {
const user = await db.user.findUnique({
where: { id: session.userId },
select: { id: true, name: true, email: true },
})
return user
} catch (error) {
console.error('Failed to fetch user')
throw error
}
}在组件中使用:
import { getUser } from '@/lib/dal'
export default async function Page() {
const user = await getUser()
return <div>Hello, {user.name}</div>
}3. 组件级数据访问
适用于原型和学习。可以直接在组件中访问数据,但应该意识到安全风险:
import { db } from '@/lib/db'
export default async function Page() {
const user = await db.user.findFirst()
return <div>Hello, {user.name}</div>
}最佳实践
使用 server-only 包
使用 server-only 包确保服务器端代码不会意外地在客户端运行:
import 'server-only'
export async function getUser() {
// 服务器端代码
}避免在 searchParams 中使用敏感操作
不要使用 searchParams 来处理变更操作:
// 错误:使用 searchParams 处理变更
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ logout?: string }>
}) {
const params = await searchParams
if (params.get('logout')) {
cookies().delete('AUTH_TOKEN')
}
return <UserProfile />
}应该使用 Server Actions 来处理变更:
// 正确:使用 Server Actions 处理变更
import { logout } from './actions'
export default function Page() {
return (
<>
<UserProfile />
<form action={logout}>
<button type="submit">Logout</button>
</form>
</>
)
}类型安全
确保组件 props 的类型不会暴露敏感数据:
// 错误:类型过于宽泛
interface UserProps {
user: {
id: string
name: string
email: string
password: string // 不应该传递给客户端
}
}
// 正确:只包含必要的字段
interface UserProps {
user: {
id: string
name: string
}
}开发环境优化
HMR 缓存
在本地开发中,Server Components 的更改会导致整个页面重新渲染以显示新更改,包括为组件获取新数据。
serverComponentsHmrCache 选项允许你在本地开发中跨 HMR 刷新缓存 Server Components 中的 fetch 响应:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
serverComponentsHmrCache: true,
},
}
export default nextConfig这将导致更快的响应并降低计费 API 调用的成本。
平台支持
流式传输支持
| 部署选项 | 支持 |
|---|---|
| Node.js 服务器 | 是 |
| Docker 容器 | 是 |
| 静态导出 | 否 |
| 适配器 | 取决于平台 |
总结
Next.js 提供了灵活而强大的数据获取方式:
- Server Components:使用
fetch、ORM 或数据库直接获取数据,支持并行和顺序获取 - Client Components:使用 SWR、TanStack Query 或 React 的
usehook - 流式传输:使用
loading.js或<Suspense>提供即时加载状态 - 数据安全:使用数据访问层、
server-only包和 Server Actions 保护数据 - 性能优化:使用 React
cache、预加载模式和 HMR 缓存提高性能
通过合理使用这些特性,可以构建既快速又安全的现代 Web 应用。
上次更新于