本文总结:
- 服务端组件 → 直接
fetch
或 ORM 查询。 - 客户端组件 → 用
use
Hook 或 swr/react-query。 - 缓存 → 四种机制:请求记忆、数据缓存、完整路由缓存、客户端路由缓存。
- 流式渲染 → 用
Suspense
或loading.js
提高性能。 - Server Actions → 在服务端运行函数,配合表单或事件调用,同时能更新数据并触发缓存重新验证。
数据获取
服务端组件
官方文档data fetch:nextjs.org/docs/app/ge...
在服务端组件中,有两种方式,直接使用fetch和使用orm查询
第一种方式:直接在异步函数中获取数据:
javascript
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>
)
}
第二种方式:直接使用orm框架查询数据,不用调用第三方接口:
javascript
import { db, posts } from '@/lib/db'
export default async function Page() {
const allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
客户端组件
在客户端组件中,有两种推荐的方式:使用use Hook或者使用第三方库如swr和react-query
第一种方式:使用React的use Hook,可以使用 use
和流式数据获取,从服务端获取数据,然后传递到客户端,服务端代码如下:
javascript
import Posts from '@/app/ui/posts'
import { Suspense } from 'react'
export default function Page() {
// Don't await the data fetching function
const posts = getPosts()
return (
<Suspense fallback={<div>Loading...</div>}>
<Posts posts={posts} />
</Suspense>
)
}
客户端使用use去读Promise
typescript
'use client'
import { use } from 'react'
export default function Posts({
posts,
}: {
posts: Promise<{ id: string; title: string }[]>
}) {
const allPosts = use(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
第二种方式:使用第三方的请求库,比较好用的是swr和react-query,swr是vercel自己的,更推荐
javascript
'use client'
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((r) => r.json())
export default function BlogPage() {
const { data, error, isLoading } = useSWR(
'<https://api.vercel.app/blog>',
fetcher
)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
请求缓存
请求缓存就是删除重复的fetch请求,只需要调用一次,其他的拿缓存结果使用就行。
一种方法是请求记忆,在单个渲染过程中,fetch
使用GET
或 且具有相同 URL 和选项的调用将被合并为一个请求。还可以fetch
使用 Next.js 的数据缓存来删除重复请求,例如通过cache: 'force-cache'
进行设置fetch
。
另一种方法不使用fetch,比如使用ORM的场景,可以用 React的 的cache
来包裹函数
typescript
import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'
export const getPost = cache(async (id: string) => {
const post = await db.query.posts.findFirst({
where: eq(posts.id, parseInt(id)),
})
})
流式请求
当在服务端组件使用 async/await
,Next.js会启用 dynamic rendering.,每次用户请求时,都会在服务器上获取并渲染数据。如果有任何缓慢的数据请求,整个路由将被阻止渲染。为了解决这个问题,可以把HTML分成小的chunks,有两种方式实现,一个是用loading.js包裹,另一个使用Suspense组件包裹。
javascript
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
export default function BlogPage() {
return (
<div>
{/* This content will be sent to the client immediately */}
<header>
<h1>Welcome to the Blog</h1>
<p>Read the latest posts below.</p>
</header>
<main>
{/* Any content wrapped in a <Suspense> boundary will be streamed */}
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
)
}
Next.js四种缓存
机制 | 缓存什么 | 在哪里 | 目的 | 期间 |
---|---|---|---|---|
请求记忆 | 函数的返回值 | 服务器 | 在 React 组件树中重用数据 | 每个请求的生命周期 |
数据缓存 | 数据 | 服务器 | 跨用户请求和部署存储数据 | 持久性(可重新验证) |
完整路由缓存 | HTML 和 RSC payload | 服务器 | 降低渲染成本并提高性能 | 持久性(可重新验证) |
客户端路由缓存 | RSC payload | 客户 | 减少导航时的服务器请求 | 用户会话或基于时间 |
请求记忆
Next.js 扩展了fetch
API,可以自动记忆具有相同 URL 和选项的请求。这意味着你可以在 React 组件树的多个位置针对同一数据调用 fetch 函数,但只需执行一次。
例如,如果您需要跨路由使用相同的数据(例如在布局、页面和多个组件中),则无需在树的顶部获取数据,也无需在组件之间转发 props。相反,您可以在需要数据的组件中获取数据,而不必担心通过网络对同一数据进行多次请求所带来的性能影响。
csharp
async function getItem() {
// The `fetch` function is automatically memoized and the result
// is cached
const res = await fetch('https://.../item/1')
return res.json()
}
// This function is called twice, but only executed the first time
const item = await getItem() // cache MISS
// The second call could be anywhere in your route
const item = await getItem() // cache HIT
数据缓存
Next.js 具有内置数据缓存,可持久保存传入服务器请求 和部署的 数据提取结果。这是因为 Next.js 扩展了原生fetch
API,允许服务器上的每个请求设置自己的持久缓存语义。
可以使用cache
和next.revalidate
选项fetch
来配置缓存行为。
数据缓存和请求记忆的区别: 虽然两种缓存机制都可以通过重复使用缓存数据来提高性能,但数据缓存在传入的请求和部署中是持久的,而记忆仅持续请求的整个生命周期。
重新验证缓存,也就是缓存过期吧,有两种方式:
- 基于时间的重新验证:在设定的时间间隔后,数据会在下一次请求时重新验证。适用于变化不频繁、对实时性要求不高的场景。
- 按需重新验证:由事件触发的数据刷新,例如表单提交或 CMS 内容更新。可通过标签或路径的方式一次性刷新多组数据,确保页面尽快展示最新内容。
基于时间的重新验证:
可以使用next.revalidate
选项fetch
设置资源的缓存时间(以秒为单位)。
php
// Revalidate at most every hour
fetch('https://...', { next: { revalidate: 3600 } })
注意,例如设置了60s,那么在60秒内的请求都返回缓存数据,超过60s的第一个请求还是返回缓存数据,,第二个请求才是新的数据
按需重新验证:
可以根据需要通过路径 ( revalidatePath
) 或缓存标签 ( revalidateTag
) 重新验证数据。
scss
import { revalidatePath, revalidateTag } from "next/cache";
revalidatePath(path); // 按路径刷新
revalidateTag(tag); // 按标签刷新
如果不想缓存任何数据,可以使用:
csharp
let data = await fetch('<https://api.vercel.app/blog>', { cache: 'no-store' })
完整路由缓存
Next.js 在构建的时候会自动渲染和缓存路由,这样当访问路由的时候,可以直接使用缓存中的路由而不用从零开始在服务端渲染,从而加快页面加载速度。需要了解以下几个原理:
- 服务端React渲染:在服务器上,Next.js 使用 React 的 API 来编排渲染。渲染工作被拆分成多个块:通过单独的路由和 Suspense 边界。第一步把React服务端组件渲染为RSC payload,第二步使用rsc payload和js渲染html,实现不用等待内容全渲染完,可以流式传输响应
- Next.js服务端缓存:完整路由缓存,缓存的是HTML和RSC payload
- 客户端 React水合:主要有三步:第一步立即显示HTML和服务端组件的非交互预览,第二步rsc payload更新客户端DOM,第三步js水合客户端组件,使之具有交互性
- Next.js客户端缓存:rsc payload 会缓存在客户端内存中,用于路由导航等内容
- 后续导航:先检查rsc payload在不在缓存,如果在就不重新发送请求
如何不使用缓存?
使用dynamic = 'force-dynamic'或revalidate = 0路由段配置选项
客户端路由缓存
Next.js 有一个内存客户端路由器缓存,用于存储路由段的 RSC 有效负载,按布局、加载状态和页面拆分。当用户在路由之间导航时,Next.js 会缓存已访问的路由段,并预取用户可能导航到的路由。这样可以实现即时的前进/后退导航,导航之间无需重新加载整个页面,并且在共享布局中保留浏览器状态和 React 状态。
Server Actions
Server Actions 是指在服务端执行的异步函数,它们可以在服务端和客户端组件中使用,来处理 Next.js 应用中的数据提交和更改。
定义一个 Server Action 需要使用 React 的 "use server" 指令。use server 可以放到 async函数的顶部或者放在一个单独文件的顶部,下面是在服务端组件和客户端组件中的用法
服务端组件
服务端组件:两种方式都可以使用,例如:
javascript
// actions.ts
export async function createPost(formData: FormData) {
'use server'
const title = formData.get('title')
const content = formData.get('content')
// Update data
// Revalidate cache
}
export async function deletePost(formData: FormData) {
'use server'
const id = formData.get('id')
// Update data
// Revalidate cache
}
// page.tsx 直接在异步函数加 use server
export default function Page() {
// Server Action
async function createPost(formData: FormData) {
'use server'
// ...
}
return <></>
}
客户端组件
客户端组件中使用的时候,需要先创建一个文件,在顶部添加 "use server" 指令:
javascript
// 单独的文件
'use server'
export async function createPost() {}
// 客户端组件
'use client'
import { createPost } from '@/app/actions'
export function Button() {
return <button formAction={createPost}>Create</button>
}
也可以作为props传递给客户端组件:
javascript
<ClientComponent updateItemAction={updateItem} />
'use client'
export default function ClientComponent({
updateItemAction,
}: {
updateItemAction: (formData: FormData) => void
}) {
return <form action={updateItemAction}>{/* ... */}</form>
}
调用方式
有两种可以调用 server function 的办法:
- 在Forms里调用,React扩展了HTML的
<form>
,可以通过action
属性调用,调用的时候会自动接受FormData
对象
javascript
// form.tsx
import { createPost } from '@/app/actions'
export function Form() {
return (
<form action={createPost}>
<input type="text" name="title" />
<input type="text" name="content" />
<button type="submit">Create</button>
</form>
)
}
//action.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title')
const content = formData.get('content')
// Update data
// Revalidate cache
}
- Event Handlers 事件处理,例如onClick函数
javascript
'use client'
import { incrementLike } from './actions'
import { useState } from 'react'
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes)
return (
<>
<p>Total Likes: {likes}</p>
<button
onClick={async () => {
const updatedLikes = await incrementLike()
setLikes(updatedLikes)
}}
>
Like
</button>
</>
)
}