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

news2024/9/20 12:41:31

数据获取

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

数据获取是任何应用程序的核心部分。本页介绍如何在 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 令牌)时,这很有用。

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

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

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

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

您还可以使用第三方库(如:SWR 或 React 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>
}

需要知道:

  • 示例:带有加载和错误状态的表单

  • 在处理具有多个字段的表单时,你可能需要考虑将 entries() 方法与 JavaScript 的 Object.fromEntries() 一起使用。例如:const rawFormData = Object.fromEntries(formData.entries())

  • 请参阅 React <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')
  }
}

需要知道:

  • 除了抛出错误之外,你还可以返回一个由 useFormStatus 处理的对象。请参阅服务器端验证和错误处理。
重新验证数据

你可以使用 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 文档:

  • "use server"
  • <form>
  • useFormStatus
  • useFormState
  • useOptimistic

数据获取模式和最佳实践

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

在服务器上获取数据

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

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

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

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

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

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

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

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

在需要的地方获取数据

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

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

需要知道:

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

Streaming

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

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

在这里插入图片描述

要了解有关 Streaming 和 Suspense 的更多信息,请参阅加载 UI 和 Streaming 与 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
    />
  )
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1322201.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java最全面试题专题---4、并发编程(1)

基础知识 并发编程的优缺点 为什么要使用并发编程&#xff08;并发编程的优点&#xff09; 充分利用多核CPU的计算能力&#xff1a;通过并发编程的形式可以将多核CPU的计算能力发挥到极致&#xff0c;性能得到提升方便进行业务拆分&#xff0c;提升系统并发能力和性能&#…

网络游戏需要资质

网络游戏需要资质 介绍一、ICP许可证或者ICP备案二、《网络出版服务许可证》三、网络游戏的出版前置审批四、《网络文化经营许可证》 介绍 前段时间一直在忙着给公司做APP的ICP备案所以一直也没有更新文章&#xff0c;去办这个APP的ICP备案也是踩了些坑&#xff0c;今天来讲一…

安全运营之安全加固和运维

安全运营是一个将技术、流程和人有机结合的复杂系统工程&#xff0c;通过对已有安全产品、工具和服务产出的数据进行有效的分析&#xff0c;持续输出价值&#xff0c;解决安全问题&#xff0c;以确保网络安全为最终目标。 安全加固和运维是网络安全运营中的两个重要方面。 安全…

使用栈的特性实现多位计算器

创建一个栈&#xff1a; //定义一个ArrayStack 表示栈 class ArrayStack2 {private int maxSize; //栈的大小private int[] stack; //定义一个栈private int top -1; //定义一个栈顶指针public ArrayStack2(int size) {maxSize size;stack new int[maxSize];}//栈满public …

黑马头条--day04--文章审核

目录 一.审核 1.流程 2.内容安全第三方接口 3.百度AI审核 3.1首先注册百度云账号&#xff0c;然后完成个人信息认证 3.2开启文本审核和图像审核功能 3.3添加sdk依赖 3.4构建测试类 3.5文本审核 3.6图片审核 3.7对百度云审核的封装 3.7.1在自媒体服务的config目录下面…

【1.8计算机组成与体系结构】磁盘管理

目录 1.磁盘基本结构与存取过程1.1 磁盘基本结构1.2 磁盘的存取过程 2.磁盘优化分布存储3.磁盘单缓冲区与双缓冲区4.磁盘移臂调度算法 1.磁盘基本结构与存取过程 1.1 磁盘基本结构 磁盘&#xff1a;柱面&#xff0c;磁道&#xff0c;扇区。 1.2 磁盘的存取过程 存取时间寻…

C#调用阿里云接口实现动态域名解析,支持IPv6(Windows系统下载可用)

电信宽带一般能申请到公网IP&#xff0c;但是是动态的&#xff0c;基本上每天都要变&#xff0c;所以想到做一个定时任务&#xff0c;随系统启动&#xff0c;网上看了不少博文很多都支持IPv4&#xff0c;自己动手写了一个。 &#xff08;私信可全程指导&#xff09; 部署步骤…

原生JS实现组件切换(不刷新页面)

这是通过原生Es6实现的组件切换&#xff0c;代码很简单&#xff0c;原理和各种框架原理大致相同。 创建文件 ├── component&#xff1a;存放组件 │ ├── home1.js&#xff1a;组件1 │ ├── home2.js&#xff1a;组件2 ├── index.html ├── index.js初始化ht…

台湾虾皮本土店铺:如何在台湾虾皮本土店铺开展电商业务

在台湾地区&#xff0c;虾皮&#xff08;Shopee&#xff09;是一款备受欢迎的电商平台。虾皮拥有强大的技术团队、丰富的电商经验和对市场的深刻理解。虾皮本土店铺凭借其在出售、物流、回款、售后、仓储等方面的一条龙服务&#xff0c;为广大卖家提供了全方位的保障和支持。如…

VSCode报错插件Error lens

1.点击左侧扩展图标→搜索“error lens”→点击“安装” 2.安装成功页面如下&#xff1a; 3.代码测试一下&#xff1a;书写代码的过程中会出现红色提醒或红色报错 4.另外推荐小伙伴们安装中文插件&#xff0c;学习过程中会比较实用方便&#xff0c;需要安装中文插件的小伙伴请点…

【性能测试】资深老鸟带你,一篇打通负载与压力测试的区别...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 负载测试 是通过…

【lesson17】MySQL表的基本操作--表去重、聚合函数和group by

文章目录 MySQL表的基本操作介绍插入结果查询&#xff08;表去重&#xff09;建表插入数据操作 聚合函数建表插入数据操作 group by&#xff08;分组&#xff09;建表插入数据操作 MySQL表的基本操作介绍 CRUD : Create(创建), Retrieve(读取)&#xff0c;Update(更新)&#x…

2.vue学习(8-7)

文章目录 8.数据绑定9.el与data的2种写法 8.数据绑定 单向数据绑定就是我们学的v-bind的方式&#xff0c;vue对象变了&#xff0c;页面才变。但是页面变了&#xff0c;vue对象不会变。 双向数据绑定需要用v-model&#xff0c;就能实现双向的改变。 注意&#xff1a;不是所有的…

我的4096创作纪念日

机缘 岁月如梭&#xff0c;时光一晃已经在CSDN扎根4096天了。第一次注册CSDN好像还是在2012年&#xff0c;那会还没大学毕业。初入CSDN&#xff0c;只是把他当作自己编程时遇到问题的在线笔记记录而已&#xff0c;没想到无意间还帮助了其他遇到同样问题困扰的同学。而在这4096…

简析555电压检测电路

555定时器的简介 555定时器是一种多用途的数字——模拟混合集成电路&#xff0c;利用它能极方便地构成施密特触发器、单稳态触发器和多谐振荡器。由于使用灵活、方便&#xff0c;所以555定时器在波形的产生与交换、测量与控制、家用电器、电子玩具等许多领域中都得到了广泛应用…

idea添加外部jar包

在日常开发中在lib包的里面添加了外部的jar&#xff0c;如何将外部的包添加到java类库中&#xff0c;这样项目就可以引用相应的jar包&#xff0c;操作如下&#xff1a; 1.先将需要的jar复制到lib包如下&#xff0c;如下截图&#xff0c;图标前面没有箭头&#xff0c;表示还未添…

echart饼状图文字大小位置颜色调整属性

echart饼状图文字大小位置颜色调整属性 文字位置对应属性代码效果图 文字位置对应属性 1.图中‘1’的文字大小调整在‘legend’对象下的‘textStyle’属性里 2.图中‘2’的文字大小调整在‘tooltip’对象下的‘textStyle’属性里 3.图中‘3’的文字大小调整在‘series’对象下的…

flink 读取 apache paimon表,查看source的延迟时间 消费堆积情况

paimon source查看消费的数据延迟了多久 如果没有延迟 则显示0 官方文档 Metrics | Apache Paimon

什么是证券RPA?证券RPA解决什么问题?证券RPA实施难点在哪里?

RPA智能机器人&#xff0c;也称为“机器人流程自动化”、“软件机器人”&#xff0c;使用智能自动化技术来执行人类工人的重复性办公室任务。它结合API和用户界面(UI)交互来集成和执行企业和生产力应用程序之间的重复性任务。只要预先设计好使用规则&#xff0c;RPA就可以模拟人…

.NET 自定义中间件 判断是否存在 AllowAnonymousAttribute 特性 来判断是否需要身份验证

public Task InvokeAsync(HttpContext context){// 获取终点路由特性var endpointFeature context.Features.Get<IEndpointFeature>();// 获取是否定义了特性var attribute endpointFeature?.Endpoint?.Metadata?.GetMetadata<AllowAnonymousAttribute>();if …