Next.js 学习笔记(四)——数据获取

数据获取

数据获取、缓存和重新验证

数据获取是任何应用程序的核心部分。本页介绍如何在 React 和 Next.js 中获取、缓存和重新验证数据。

有四种方法可以获取数据:

  1. 在服务器上,使用 fetch
  2. 在服务器上,使用第三方库
  3. 在客户端上,通过路由处理程序
  4. 在客户单上,使用第三方库

在服务器上使用 fetch 获取数据

Next.js 扩展了原生的 fetch Web API,允许你为服务器上的每个 fetch 请求配置缓存重新验证行为。React 扩展了 fetch,以便在渲染 React 组件树时自动存储 fetch 请求。

在服务器组件、路由处理程序服务器操作中,可以将 fetchasync/await 一起使用。

例如:

// app/page.tsx

async function getData() {
  const res = await fetch('https://api.example.com/...')
  // 返回值是 *not* 序列化的
  // 你可以返回 Date、Map、Set等
 
  if (!res.ok) {
    // 这将激活最接近的 `error.js` 错误边界
    throw new Error('Failed to fetch data')
  }
 
  return res.json()
}
 
export default async function Page() {
  const data = await getData()
 
  return <main></main>
}

需要知道

  • Next.js 提供了在服务器组件(如:cookiesheaders)中获取数据时可能需要的有用功能。这将导致路由被动态渲染,因为它们依赖于请求时间信息
  • 在路由处理程序中,由于路由处理程序不是 React 组件树的一部分,所有 fetch 请求不会被存储
  • 要在带有 TypeScript 的服务器组件中使用 async/await,你需要使用 TypeScript 5.1.3 或更高版本的 @types/react 18.2.8 或更高级别
缓存数据

缓存仓库数据,因此不需要在每次请求时都从数据源重新获取数据。

默认情况下,Next.js 会自动将 fetch 的返回值缓存在服务器上的数据缓存中。这意味着数据可以在构建时或请求时提取、缓存,并在每个数据请求中重用。

// 'force-cache' 是默认值,可以省略
fetch('https://...', { cache: 'force-cache' })

使用 POST 方法的 fetch 请求也会自动缓存。除非它在使用 POST 方法的路由处理程序中,否则它不会被缓存。

什么是数据缓存?

数据缓存是一个持久的 HTTP 缓存。根据你的平台,缓存可以自动扩展并在多个区域之间共享

了解有关数据缓存的更多信息。

重新验证数据

重新验证是清除数据缓存并重新回去数据的过程。当你的数据发送更改并且你希望确保显示最新信息时,这一点非常有用。

缓存数据可以通过两种方式重新验证:

  • 基于时间的重新验证:在经过一定时间后自动重新验证数据。这对于很少更改且新鲜度不那么重要的数据非常有用。
  • 按需重新验证:根据事件手动重新验证数据(例如:表单提交)。按需重新验证可以使用基于标记或基于路径的方法一次重新验证数据组。当你希望确保尽快显示最新数据时(例如:当无头 CMS 的内容更新时),这一点非常有用。
基于时间的重新验证

要按一定时间间隔重新验证数据,可以使用 fetchnext.revalidate 选项设置资源的缓存生存期(以秒为单位)。

fetch('https://...', { next: { revalidate: 3600 } })

或者,要重新验证路由段中的所有 fetch 请求,可以使用段配置选项

// layout.js | page.js

export const revalidate = 3600 // 最多每小时重新验证一次

如果在静态渲染的路由中有多个 fetch 请求,并且每个请求都有不同的重新验证频率。最低时间将用于所有请求。对于动态渲染的路由,每个 fetch 请求都将独立地重新验证。

了解有关基于时间的重新验证的更多信息。

按需重新验证

可以按需通过服务器操作路由处理程序中的路由(revalidatePath)或缓存标记(revalidateTag)重新验证数据。

Next.js 有一个缓存标记系统,用于使跨路由的 fetch 请求无效。

  1. 使用 fetch 时,可以选择使用一个或多个标记标记缓存条目
  2. 然后,你可以调用 revalidateTag 来重新验证与标记相关联的所有条目

例如,以下 fetch 请求会添加缓存标记 collection

// app/page.tsx

export default async function Page() {
  const res = await fetch('https://...', { next: { tags: ['collection'] } })
  const data = await res.json()
  // ...
}

然后,你可以通过在服务器操作中调用 revalidateTag 来重新验证此带有 collection 标记的 fetch 调用:

// app/actions.ts

'use server'
 
import { revalidateTag } from 'next/cache'
 
export default async function action() {
  revalidateTag('collection')
}

了解更多按需重新验证

错误处理和重新验证

如果在尝试重新验证数据时抛出错误,则将继续从缓存中提供最后一个成功生成的数据。在下一个后续请求中,Next.js 将重试重新验证数据。

选择退出数据缓存

如果出现以下情况,则不会缓存 fetch 请求:

  • cache: 'no-store' 被添加到 fetch 请求中
  • revalidate: 0 选项被添加到各个 fetch 请求中
  • fetch 请求位于使用 POST 方法的路由器处理程序中
  • fetch 请求是在使用 headerscookies 之后发出的
  • 使用了 const dynamic = 'force-dynamic' 路由段选项
  • fetchCache 路由段选项默认配置为跳过缓存
  • fetch 请求使用 AuthorizationCookie 头,并且在组件树中有一个未缓存的请求
单个 fetch 请求

要选择不缓存单个 fetch 请求,可以将 fetch 中的 cache 选项设置为 'no-store'。这将在每次请求时动态获取数据。

// layout.js | page.js

fetch('https://...', { cache: 'no-store' })

查看 fetch API 引用中的所有可用缓存选项。

多个 fetch 请求

如果在一个路由段(例如:布局或页面)中有多个 fetch 请求,则可以使用段配置选项配置该段中所有数据请求的缓存行为。

但是,我们建议单独配置每个 fetch 请求的缓存行为。这使你能够对缓存行为进行更精细的控制。

使用第三方库在服务器上获取数据

如果你使用的第三方库不支持或不公开 fetch(例如:数据库、CMS 或 ORM 客户端),则可以使用路由段配置选项和 React 的 cache 功能配置这些请求的缓存和重新验证行为。

是否缓存数据将取决于路由段是静态渲染还是动态渲染。如果段是静态的(默认),则请求的输出将被缓存并作为路由段的一部分重新验证。如果分段是动态的,则不会缓存请求的输出,并且在渲染分段时会在每个请求上重新获取该输出。

你还可以使用实验性的 unstable_cache API

例子

在以下示例中:

  • React cache 函数用于存储数据请求。

  • layout.tspage.ts 段中,revalidate 选项设置为 3600,这意味着数据将被缓存并最多每小时重新验证一次。

// app/utils.ts

import { cache } from 'react'
 
export const getItem = cache(async (id: string) => {
  const item = await db.item.findUnique({ id })
  return item
})

尽管 getItem 函数被调用了两次,但只会对数据库进行一次查询。

// app/item/[id]/layout.tsx

import { getItem } from '@/utils/get-item'
 
export const revalidate = 3600 // 最多每小时重新验证一次数据
 
export default async function Layout({
  params: { id },
}: {
  params: { id: string }
}) {
  const item = await getItem(id)
  // ...
}
// app/item/[id]/page.tsx

import { getItem } from '@/utils/get-item'
 
export const revalidate = 3600 // 最多每小时重新验证一次数据
 
export default async function Page({
  params: { id },
}: {
  params: { id: string }
}) {
  const item = await getItem(id)
  // ...
}

使用路由处理程序在客户端上获取数据

如果需要在客户端组件中获取数据,可以从客户端调用路由处理程序。路由处理程序在服务器上执行,并将数据返回给客户端。当您不想向客户端公开敏感信息(如:API 令牌)时,这很有用。

有关示例,请参阅路由处理程序文档。

服务器组件和路由处理程序

由于服务器组件在服务器上渲染,因此不需要从服务器组件调用路由处理程序来获取数据。相反,您可以直接在服务器组件内部获取数据。

使用第三方库在客户端上获取数据

您还可以使用第三方库(如:SWRReact Query)在客户端上获取数据。这些库提供了自己的 API,用于存储请求、缓存、重新验证和更改数据。

未来的 API:

use 是一个 React 函数,它接受并处理函数返回的 promise。目前不建议在客户端组件中使用在 use 中嵌套 fetch,并且可能会触发多次重新渲染。在 React 文档中了解更多有关 use

服务器操作和突变

服务器操作是在服务器上执行的异步函数。它们可以在服务器和客户端组件中用于处理 Next.js 应用程序中的表单提交和数据突变。

通过服务器操作了解有关形式和突变的更多信息→ YouTube(10分钟)

约定

服务器操作可以用 React "use server" 定义指令。你可以将该指令放在 async 函数的顶部,以将该函数标记为服务器操作,也可以放在单独文件的顶部,将该文件的所有导出标记为服务器动作。

服务器组件

服务器组件可以使用内联功能级别或模块级别的 "use server" 指令。要内联服务器操作,请在函数体顶部添加 "use server"

// app/page.tsx

// 服务器组件
export default function Page() {
  // 服务器操作
  async function create() {
    'use server'
    // ...
  }
  
  return (
    // ...
  )
}
客户端组件

客户端组件只能导入使用模块级 "use server" 指令的操作。

要在客户端组件中调用服务器操作,请创建一个新文件,并在其顶部添加 "use Server" 指令。文件中的所有函数都将标记为可在客户端组件和服务器组件中重复使用的服务器操作:

// app/actions.ts

'use server'
 
export async function create() {
  // ...
}
// app/ui/button.tsx

import { create } from '@/app/actions'
 
export function Button() {
  return (
    // ...
  )
}

你还可以将服务器操作作为 prop 传递给客户端组件:

<ClientComponent updateItem={updateItem} />
// app/client-component.jsx

'use client'
 
export default function ClientComponent({ updateItem }) {
  return <form action={updateItem}>{/* ... */}</form>
}

行为

  • 可以使用 <form> 元素中的 action 属性调用服务器操作:
    • 默认情况下,服务器组件支持渐进式增强,这意味着即使 JavaScript 尚未加载或被禁用,表单也会被提交。
    • 在客户端组件中,如果 JavaScript 尚未加载,调用服务器操作的表单将对提交进行排队,从而优先考虑客户端水合。
    • 水合后,浏览器不会在表单提交时刷新。
  • 服务器操作不限于 <form>,可以从事件处理程序、useEffect、第三方库和其他表单元素(如:<button>)调用。
  • 服务器操作与 Next.js 缓存和重新验证体系结构集成。当调用一个操作时,Next.js 可以在单个服务器往返中返回更新的 UI 和新数据。
  • 在幕后,操作使用 POST 方法,并且只有此 HTTP 方法才能调用它们。
  • 服务器操作的参数和返回值必须可由 React 序列化。有关可序列化参数和值的列表,请参阅 React 文档。
  • 服务器操作是函数。这意味着它们可以在应用程序中的任何位置重复使用。
  • 服务器操作从其使用的页面或布局继承运行时

例子

表单

React 扩展了 HTML <form> 元素,允许使用 action prop 调用服务器操作。

在表单中调用时,该操作会自动接收 FormData 对象。你不需要使用 React useState 来管理字段,而是可以使用本地 FormData 方法提取数据:

// app/invoices/page.tsx

export default function Page() {
  async function createInvoice(formData: FormData) {
    'use server'
 
    const rawFormData = {
      customerId: formData.get('customerId'),
      amount: formData.get('amount'),
      status: formData.get('status'),
    }
 
    // 变异数据
    // 重新验证缓存
  }
 
  return <form action={createInvoice}>...</form>
}

需要知道:

传递其他参数

你可以使用 JavaScript bind 方法将其他参数传递给服务器操作。

// app/client-component.tsx

'use client'
 
import { updateUser } from './actions'
 
export function UserProfile({ userId }: { userId: string }) {
  const updateUserWithId = updateUser.bind(null, userId)
 
  return (
    <form action={updateUserWithId}>
      <input type="text" name="name" />
      <button type="submit">Update User Name</button>
    </form>
  )
}

除了表单数据外,服务器操作还将接收 userId 参数:

// app/actions.js

'use server'
 
export async function updateUser(userId, formData) {
  // ...
}

需要知道:

  • 另一种选择是将参数作为表单中的隐藏输入字段传递(例如:<input type=“hidden” name=“userId” value={userId} />)。但是,该值将是渲染的 HTML 的一部分,不会进行编码。

  • .bind 适用于服务器组件和客户端组件。它还支持渐进增强。

挂起的状态

你可以使用 React useFormStatus hook 来显示提交表单时的挂起状态。

  • useFormStatus 返回特定 <form> 的状态,因此必须将其定义为 <form> 元素的子级
  • useFormStatus 是一个 React hook,因此必须在客户端组件中使用。
// app/submit-button.tsx

'use client'
 
import { useFormStatus } from 'react-dom'
 
export function SubmitButton() {
  const { pending } = useFormStatus()
 
  return (
    <button type="submit" aria-disabled={pending}>
      Add
    </button>
  )
}

<SubmitButton /> 然后可以以任何形式嵌套:

// app/page.tsx

import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'
 
// 服务器组件
export default async function Home() {
  return (
    <form action={createItem}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}
服务器端验证和错误处理

我们建议使用 HTML 验证,如 requiredtype="email" 进行基本的客户端表单验证。

对于更高级的服务器端验证,可以使用类似 zod 的库要在更改数据之前验证表单字段,请执行以下操作:

// app/actions.ts

'use server'
 
import { z } from 'zod'
 
const schema = z.object({
  email: z.string({
    invalid_type_error: 'Invalid Email',
  }),
})
 
export default async function createUser(formData: FormData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })
 
  // 如果表单数据无效,请提前返回
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }
 
  // 突变数据
}

一旦在服务器上验证了字段,就可以在操作中返回一个可序列化的对象,并使用 React useFormState hook 向用户显示消息。

  • 通过将操作传递给 useFormState,操作的函数签名将更改为接收新的 prevStateinitialState 参数作为其第一个参数。

  • useFormState 是一个 React 钩子,因此必须在客户端组件中使用。

// app/actions.ts

'use server'
 
export async function createUser(prevState: any, formData: FormData) {
  // ...
  return {
    message: 'Please enter a valid email',
  }
}

然后,你可以将操作传递到 useFormState hook,并使用返回的 state 显示错误消息。

// app/ui/signup.tsx

'use client'
 
import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'
 
const initialState = {
  message: null,
}
 
export function Signup() {
  const [state, formAction] = useFormState(createUser, initialState)
 
  return (
    <form action={formAction}>
      <label htmlFor="email">Email</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
      <button>Sign up</button>
    </form>
  )
}

需要知道:

乐观地更新

你可以使用 React useOptimistic hook,以便在服务器操作完成之前乐观地更新 UI,而不是等待响应:

// app/page.tsx

'use client'
 
import { useOptimistic } from 'react'
import { send } from './actions'
 
type Message = {
  message: string
}
 
export function Thread({ messages }: { messages: Message[] }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic<Message[]>(
    messages,
    (state: Message[], newMessage: string) => [
      ...state,
      { message: newMessage },
    ]
  )
 
  return (
    <div>
      {optimisticMessages.map((m, k) => (
        <div key={k}>{m.message}</div>
      ))}
      <form
        action={async (formData: FormData) => {
          const message = formData.get('message')
          addOptimisticMessage(message)
          await send(message)
        }}
      >
        <input type="text" name="message" />
        <button type="submit">Send</button>
      </form>
    </div>
  )
}
嵌套元素

你可以在 <form> 中嵌套的元素中调用服务器操作,如 <button><input type=“submit”><input type=“image”>。这些元素接受 formAction prop 或事件处理程序

这在你想在一个表单中调用多个服务器操作的情况下很有用。例如,你可以创建一个特定的 <button> 元素,用于保存后草稿并发布它。请参阅 React <form> 文档了解更多信息。

无表单元素

虽然在 <form> 元素中使用服务器操作很常见,但它们也可以从代码的其他部分调用,如:事件处理程序和 useEffect

事件处理程序

你可以从事件处理程序(如:onClick)调用服务器操作。例如,要增加类似计数:

// app/like-button.tsx

'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>
    </>
  )
}

为了改善用户体验,我们建议使用其他 React API,如:useOptimistic 并使用 Transition 以在服务器上完成服务器操作执行之前更新 UI,或显示挂起状态。

你还可以将事件处理程序添加到表单元素中,例如,在 onChange上保存表单字段:

// app/ui/edit-post.tsx

'use client'
 
import { publishPost, saveDraft } from './actions'
 
export default function EditPost() {
  return (
    <form action={publishPost}>
      <textarea
        name="content"
        onChange={async (e) => {
          await saveDraft(e.target.value)
        }}
      />
      <button type="submit">Publish</button>
    </form>
  )
}

对于这种情况,其中可能会快速连续触发多个事件,我们建议使用防抖来阻止不必要的服务器操作调用。

useEffect

你可以使用 React useEffect hook 在组件装载或依赖项更改时调用服务器操作。这对于依赖于全局事件或需要自动触发的突变很有用。例如,onKeyDown 用于应用程序快捷方式,交叉点观察者挂钩用于无限滚动,或者当组件安装以更新视图计数时:

// app/view-count.tsx

'use client'
 
import { incrementViews } from './actions'
import { useState, useEffect } from 'react'
 
export default function ViewCount({ initialViews }: { initialViews: number }) {
  const [views, setViews] = useState(initialViews)
 
  useEffect(() => {
    const updateViews = async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    }
 
    updateViews()
  }, [])
 
  return <p>Total Views: {views}</p>
}

记住要考虑 useEffect行为和注意事项

错误处理

当抛出错误时,它将被客户端上最近的 error.js<Suspense> 边界捕获。我们建议使用 try/catch 返回要由 UI 处理的错误。

例如,你的服务器操作可能会通过返回消息来处理创建新项目时的错误:

// app/actions.ts

'use server'
 
export async function createTodo(prevState: any, formData: FormData) {
  try {
    // 突变数据
  } catch (e) {
    throw new Error('Failed to create task')
  }
}

需要知道:

重新验证数据

你可以使用 revalidatePath API 重新验证服务器操作中的 Next.js 缓存

// app/actions.ts

'use server'
 
import { revalidatePath } from 'next/cache'
 
export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidatePath('/posts')
}

或者使用 revalidateTag 使具有缓存标记的特定数据提取无效:

// app/actions.ts

'use server'
 
import { revalidateTag } from 'next/cache'
 
export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidateTag('posts')
}
重定向

如果你希望在完成服务器操作后将用户重定向到不同的路由,则可以使用 redirect API。redirect 需要在 try/catch 块之外调用:

// app/actions.ts

'use server'
 
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
 
export async function createPost(id: string) {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidateTag('posts') // 更新缓存的帖子
  redirect(`/post/${id}`) // 导航到新的文章页面
}
Cookies

你可以使用 cookies API 中的 getsetdelete 服务器操作中的 cookies:

// app/actions.ts

'use server'
 
import { cookies } from 'next/headers'
 
export async function exampleAction() {
  // Get cookie
  const value = cookies().get('name')?.value
 
  // Set cookie
  cookies().set('name', 'Delba')
 
  // Delete cookie
  cookies().delete('name')
}

请参阅有关从服务器操作中删除 cookie 的其他示例

安全

认证与授权

你应该像对待公开的 API 端点一样对待服务器操作,并确保用户有权执行该操作。例如:

// app/actions.ts

'use server'
 
import { auth } from './lib'
 
export function addItem() {
  const { user } = auth()
  if (!user) {
    throw new Error('You must be signed in to perform this action')
  }
 
  // ...
}
闭包和加密

在组件内定义服务器操作会创建一个闭包,其中操作可以访问外部函数的范围。例如,publish 操作可以访问 publishVersion 变量:

// app/page.tsx

export default function Page() {
  const publishVersion = await getLatestVersion();
 
  async function publish(formData: FormData) {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('The version has changed since pressing publish');
    }
    ...
  }
 
  return <button action={publish}>Publish</button>;
}

当你需要在渲染时捕获数据快照(例如:publishVersion),以便稍后调用操作时使用时,闭包非常有用。

然而,为了实现这一点,在调用操作时,捕获的变量会被发送到客户端并返回到服务器。为了防止敏感数据暴露给客户端,Next.js 自动对封闭变量进行加密。每次构建 Next.js 应用程序时,都会为每个操作生成一个新的私钥。这意味着只能对特定的生成调用操作。

需要知道:

  • 我们不建议仅依靠加密来防止敏感值在客户端上暴露。相反,你应该使用 React taint API 来主动防止特定数据发送到客户端。
重写加密密钥(高级)

当跨多个服务器自托管 Next.js 应用程序时,每个服务器实例最终可能会使用不同的加密密钥,从而导致潜在的不一致性。

为了缓解这种情况,可以使用 process.env.NEXT_SERVER_ACTIONS_encryption_key 环境变量覆盖加密密钥。指定此变量可确保加密密钥在构建中是持久的,并且所有服务器实例都使用相同的密钥。

这是一个高级用例,其中跨多个部署的一致加密行为对您的应用程序至关重要。您应该考虑标准的安全实践,如密钥轮换和签名。

需要知道:

  • 部署到 Vercel 的 Next.js 应用程序会自动处理此问题。
允许的来源(高级)

由于服务器操作可以在 <form> 元素中调用,这会使它们受到 CSRF 攻击

在后台,服务器操作使用 POST 方法,并且只允许此 HTTP 方法调用它们。这可以防止现代浏览器中的大多数 CSRF 漏洞,尤其是SameSite cookie 是默认的。

作为一种额外的保护,Next.js 中的 Server Actions 还比较了 Origin 头Host 头(或 X-Forwarded-Host)。如果这些不匹配,请求将被中止。换句话说,服务器操作只能在承载它的页面所在的主机上调用。

对于使用反向代理或多层后端架构的大型应用程序(其中服务器 API 与生产域不同),建议使用配置选项serverActions.allowedOrigins 选项来指定安全来源列表。该选项接受一个字符串数组。

// next.config.js

/** @type {import('next').NextConfig} */
module.exports = {
  experimental: {
    serverActions: {
      allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
    },
  },
}

了解有关安全和服务器操作的详细信息。

额外资源

有关服务器操作的更多信息,请查看以下 React 文档:

数据获取模式和最佳实践

React 和 Next.js 中有一些获取数据的推荐模式和最佳实践。本页将介绍一些最常见的模式以及如何使用它们。

在服务器上获取数据

只要可能,我们建议在服务器上获取数据。这允许你:

  • 可以直接访问后端数据资源(如:数据库)。

  • 通过防止敏感信息(如:访问令牌和 API 密钥)暴露给客户端,使你的应用程序更加安全。

  • 在同一环境中获取数据并进行渲染。这既减少了客户端和服务器之间的来回通信,也减少了客户端上主线程的工作

  • 使用单个往返而不是在客户端上执行多个单独的请求来执行多个数据提取。

  • 减少客户端-服务器瀑布

  • 根据你所在的地区,数据获取也可以在离数据源更近的地方进行,从而减少延迟并提高性能。

你可以使用服务器组件、路由处理程序服务器操作在服务器上获取数据。

在需要的地方获取数据

如果你需要在树中的多个组件中使用相同的数据(例如:当前用户),则不必全局获取数据,也不必在组件之间转发 props。相反,你可以在需要数据的组件中使用 fetch 或 React cache,而不用担心对同一数据发出多个请求的性能影响。

这是可能的,因为 fetch 请求是自动存储的。了解有关请求备忘录的更多信息。

需要知道:

  • 这也适用于布局,因为不可能在父布局及其子布局之间传递数据。

Streaming

Streaming 和 Suspense 是 React 的功能,允许你逐步渲染和递增地将 UI 的渲染单元流式传输到客户端。

使用服务器组件和嵌套布局,你可以立即渲染页面中不特别需要数据的部分,并显示页面中正在获取数据的部分的加载状态。这意味着用户不必等待整个页面加载后才能开始与之交互。

在这里插入图片描述

要了解有关 Streaming 和 Suspense 的更多信息,请参阅加载 UIStreaming 与 Suspense 页面。

并行和顺序数据获取

在 React 组件内部获取数据时,需要注意两种数据获取模式:并行(Parallel)和顺序(Sequential)。

在这里插入图片描述

  • 通过顺序数据获取,路由中的请求是相互依赖的,因此会创建瀑布。在某些情况下,你可能需要此模式,因为一次提取取决于另一次提取的结果,或者您希望在下一次提取之前满足一个条件以节省资源。然而,这种行为也可能是无意的,并导致更长的加载时间。

  • 通过并行数据获取,路由中的请求被急切地启动,并将同时加载数据。这减少了客户端-服务器瀑布和加载数据所需的总时间。

顺序数据获取

如果你有嵌套的组件,并且每个组件都获取自己的数据,那么如果这些数据请求不同,则数据提取将按顺序进行(这不适用于对相同数据的请求,因为它们会自动存储)。

例如,Playlists 组件只有在 Artist 组件完成获取数据后才会开始获取数据,因为 Playlists 取决于 artistID prop:

// app/artist/[username]/page.tsx
 
async function Playlists({ artistID }: { artistID: string }) {
  // 等待播放列表
  const playlists = await getArtistPlaylists(artistID)
 
  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}
 
export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // 等待艺术家
  const artist = await getArtist(username)
 
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

在这种情况下,你可以使用 loading.js(用于路由段)或 React <Suspense>(用于嵌套组件)来显示即时加载状态,同时 React 在结果中进行流式传输。

这将防止整个路由被数据获取阻塞,并且用户将能够与页面中未被阻塞的部分进行交互。

阻止数据请求:

防止瀑布的另一种方法是在应用程序的根全局获取数据,但这将阻止其下所有路由段的渲染,直到数据完成加载。这可以被描述为 “要么全取,要么全无” 的数据获取。要么你拥有页面或应用程序的全部数据,要么没有。

任何带有 await 的请求获取都将阻止其下整个树的渲染和数据提取,除非它们被封装在 <Suspense> 边界中或使用 loading.js。另一种选择是使用并行数据获取预加载模式

并行数据获取

要并行获取数据,你可以通过在使用数据的组件外部定义请求,然后从组件内部调用请求来更早地启动请求。这通过并行启动请求来节省时间,但是,在所有的 promises 都 resolved 之前,用户不会看到渲染的结果。

在下面的示例中,getArtistgetArtistAlbums 函数在 Page 组件外部定义,然后在组件内部调用,我们等待这两个承诺得到解决:

// app/artist/[username]/page.tsx

import Albums from './albums'
 
async function getArtist(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}
 
async function getArtistAlbums(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}
 
export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // 并行启动多个请求
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)
  
  // 等待所有的 promises 都 resolve
  const [artist, albums] = await Promise.all([artistData, albumsData])
 
  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums}></Albums>
    </>
  )
}

为了改善用户体验,可以添加 Suspense Boundary 以分解渲染工作并尽快渲染部分结果。

预加载数据

防止瀑布的另一种方法是使用预加载模式。你可以选择创建一个 preload 函数来进一步优化并行数据获取。有了这种方法,你就不必把承诺当作 props。preload 函数也可以有任何名称,因为它是一个模式,而不是 API。

// components/Item.tsx

import { getItem } from '@/utils/get-item'
 
export const preload = (id: string) => {
  // void 计算给定的表达式并返回 undefined
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export default async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}
// app/item/[id]/page.tsx

import Item, { preload, checkIsAvailable } from '@/components/Item'
 
export default async function Page({
  params: { id },
}: {
  params: { id: string }
}) {
  // 开始加载项数据
  preload(id)
  // 执行另一个异步任务
  const isAvailable = await checkIsAvailable()

  return isAvailable ? <Item id={id} /> : null
}
使用 React cacheserver-only 和预加载模式

你可以将 cache 功能、preload 模式和 server-only 的包结合起来,创建一个可在整个应用程序中使用的数据获取实用程序。

// utils/get-item.ts

import { cache } from 'react'
import 'server-only'
 
export const preload = (id: string) => {
  void getItem(id)
}
 
export const getItem = cache(async (id: string) => {
  // ...
})

使用这种方法,你可以更早地获取数据、缓存响应,并确保这种数据获取只发生在服务器上

Layouts、Pages 或其他组件可以使用 utils/get-item 导出来控制何时获取项的数据。

需要知道:

  • 我们建议使用 server-only 的包,以确保客户端永远不会使用服务器数据获取功能。

防止敏感数据暴露给客户端

我们建议使用 React 的 taint API,即 taintObjectReferencetaintUniqueValue,以防止整个对象实例或敏感值被传递到客户端。

要在应用程序中启用 tainting,请将 Next.js 配置 experial.taint 选项设置为 true

// next.config.js

module.exports = {
  experimental: {
    taint: true,
  },
}

然后将要 taint 的对象或值传递给 experimental_taintObjectReferenceexperimental_taintUniqueValue 函数:

// app/utils.ts

import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'
 
export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    'Do not pass the whole user object to the client',
    data
  )
  experimental_taintUniqueValue(
    "Do not pass the user's phone number to the client",
    data,
    data.phoneNumber
  )
  return data
}
// app/page.tsx

import { getUserData } from './data'
 
export async function Page() {
  const userData = getUserData()
  return (
    <ClientComponent
      user={userData} // 这将导致一个错误,因为 tainObjectReference
      phoneNumber={userData.phoneNumber} // 这将导致一个错误,因为 tainUniqueValue
    />
  )
}

了解有关安全和服务器操作的详细信息。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jackson Mseven

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值