后端转全栈之Next.js数据获取与缓存

本文总结:

  • 服务端组件 → 直接 fetch 或 ORM 查询。
  • 客户端组件 → 用 use Hook 或 swr/react-query。
  • 缓存 → 四种机制:请求记忆、数据缓存、完整路由缓存、客户端路由缓存。
  • 流式渲染 → 用 Suspenseloading.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四种缓存

文档:nextjs.org/docs/app/gu...

机制 缓存什么 在哪里 目的 期间
请求记忆 函数的返回值 服务器 在 React 组件树中重用数据 每个请求的生命周期
数据缓存 数据 服务器 跨用户请求和部署存储数据 持久性(可重新验证)
完整路由缓存 HTML 和 RSC payload 服务器 降低渲染成本并提高性能 持久性(可重新验证)
客户端路由缓存 RSC payload 客户 减少导航时的服务器请求 用户会话或基于时间

请求记忆

Next.js 扩展了fetchAPI,可以自动记忆具有相同 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 扩展了原生fetchAPI,允许服务器上的每个请求设置自己的持久缓存语义。

可以使用cachenext.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 在构建的时候会自动渲染和缓存路由,这样当访问路由的时候,可以直接使用缓存中的路由而不用从零开始在服务端渲染,从而加快页面加载速度。需要了解以下几个原理:

  1. 服务端React渲染:在服务器上,Next.js 使用 React 的 API 来编排渲染。渲染工作被拆分成多个块:通过单独的路由和 Suspense 边界。第一步把React服务端组件渲染为RSC payload,第二步使用rsc payload和js渲染html,实现不用等待内容全渲染完,可以流式传输响应
  2. Next.js服务端缓存:完整路由缓存,缓存的是HTML和RSC payload
  3. 客户端 React水合:主要有三步:第一步立即显示HTML和服务端组件的非交互预览,第二步rsc payload更新客户端DOM,第三步js水合客户端组件,使之具有交互性
  4. Next.js客户端缓存:rsc payload 会缓存在客户端内存中,用于路由导航等内容
  5. 后续导航:先检查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 的办法:

  1. 在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
}
  1. 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>
    </>
  )
}
相关推荐
小仙女喂得猪5 小时前
2025 Android原生开发者角度的React/ReactNative 笔记整理
react native·react.js
艾小码6 小时前
为什么你的页面会闪烁?useLayoutEffect和useEffect的区别藏在这里!
前端·javascript·react.js
骑自行车的码农6 小时前
【React用到的一些算法】游标和栈
算法·react.js
小高0076 小时前
🔍说说对React的理解?有哪些特性?
前端·javascript·react.js
Mintopia7 小时前
Next 全栈之 API 测试:Supertest 与 MSW 双雄记 🥷⚔️
前端·javascript·next.js
江城开朗的豌豆8 小时前
从生命周期到useEffect:我的React函数组件进化之旅
前端·javascript·react.js
江城开朗的豌豆8 小时前
React组件传值:轻松掌握React组件通信秘籍
前端·javascript·react.js
可乐爱宅着18 小时前
全栈框架next.js入手指南
前端·next.js
遂心_1 天前
深入理解 React Hook:useEffect 完全指南
前端·javascript·react.js