Learn Nuxt with a Collection of 100+ Tips!

数据获取

Nuxt 提供了组合式函数来处理应用程序中的数据获取。

Nuxt 提供了两个组合式函数和一个内置库,用于在浏览器或服务器环境中执行数据获取:useFetchuseAsyncData$fetch

简而言之:

  • useFetch 是处理组件设置函数中的数据获取的最直接的方式。
  • $fetch 用于基于用户交互进行网络请求。
  • useAsyncData 结合 $fetch,提供了更精细的控制。

useFetchuseAsyncData 共享一套常见选项和模式,我们将在最后一节中详细介绍它们。

在此之前,了解为何使用这些组合式函数至关重要。

为什么使用特定的组合式函数进行数据获取?

Nuxt 是一个框架,可以在服务器和客户端环境中运行同构(或通用)代码。如果在 Vue 组件的设置函数中使用 $fetch 函数 执行数据获取,这可能会导致数据在服务器上获取一次(用于渲染 HTML),然后在客户端上再次获取一次(在 HTML 注水期间)。这就是为什么 Nuxt 提供了特定的数据获取组合式函数,以便只获取一次数据。

网络请求重复

useFetchuseAsyncData 组合式函数确保一旦在服务器上进行了 API 请求,数据将正确传递给客户端。

负载是通过 useNuxtApp().payload 可访问的 JavaScript 对象。在浏览器中执行代码(在全球渲染期间)时,可以使用它来避免重新获取相同的数据。

使用 Nuxt DevTools 检查 Payload 标签页 中的这些数据。

Suspense

Nuxt 在底层使用 Vue 的 <Suspense> 组件,以防止在每个异步数据可用于视图之前导航。数据获取组合式函数可以帮助您充分利用此功能,并根据每个调用的实际情况使用适合的函数。

您可以添加 <NuxtLoadingIndicator> 以在页面导航之间添加进度条。

useFetch

useFetch 组合式函数是执行数据获取的最直接的方式。

app.vue
<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>

<template>
  <p>页面访问次数:{{ count }}</p>
</template>

该组合式函数是 useAsyncData 组合式函数和 $fetch 实用工具的包装。

观看 Alexander Lichter 的视频,避免错误使用 useFetch
Read more in Docs > API > Composables > Use Fetch.
Read and edit a live example in Docs > Examples > Features > Data Fetching.

$fetch

Nuxt 包含 ofetch 库,并且作为 $fetch 别名全局自动导入到应用程序中。这是 useFetch 在后台使用的工具。

pages/todos.vue
<script setup lang="ts">
async function addTodo() {
  const todo = await $fetch('/api/todos', {
    method: 'POST',
    body: {
      // 要添加的待办事项数据
    }
  })
}
</script>
请注意,仅使用 $fetch 将不提供网络请求去重和导航阻止。:br 建议在客户端交互(基于事件的情况)或与 useAsyncData 结合使用时使用 $fetch 来获取初始组件数据。
阅读有关 $fetch 的更多信息。

useAsyncData

useAsyncData 组合式函数负责包装异步逻辑,并在解析完成后返回结果。

useFetch(url) 几乎等同于 useAsyncData(url, () => $fetch(url))。:br 对于最常见的用例,这是开发者体验的简化语法。
观看 Alexander Lichter 的视频,深入了解 useFetchuseAsyncData 之间的区别。

有一些情况下,不适合使用 useFetch 组合式函数,例如当 CMS 或第三方提供自己的查询层时。在这种情况下,您可以使用 useAsyncData 来包装调用,并仍然保留组合式函数提供的好处。

pages/users.vue
<script setup lang="ts">
const { data, error } = await useAsyncData('users', () => myGetFunction('users'))

// 也可以如下操作:
const { data, error } = await useAsyncData(() => myGetFunction('users'))
</script>
useAsyncData 的第一个参数是用于缓存第二个参数(查询函数)的响应的唯一键。如果直接传递查询函数,则可以忽略该键,键将自动生成。

由于自动生成的键仅考虑调用 useAsyncData 的文件和行,因此建议始终创建自己的键,以避免不需要的行为,例如在创建包装 useAsyncData 的自定义组合式函数时。

设置键可以在使用 useNuxtData 共享相同数据的组件之间使用,或者用于刷新特定数据。:
pages/users/[id].vue
<script setup lang="ts">
const { id } = useRoute().params

const { data, error } = await useAsyncData(`user:${id}`, () => {
  return myGetFunction('users', { id })
})
</script>

useAsyncData 组合式函数是包装并等待多个 $fetch 请求完成,然后处理结果的很好的方式。

<script setup lang="ts">
const { data: discounts, status } = await useAsyncData('cart-discount', async () => {
  const [coupons, offers] = await Promise.all([
    $fetch('/cart/coupons'),
    $fetch('/cart/offers')
  ])

  return { coupons, offers }
})
// discounts.value.coupons
// discounts.value.offers
</script>
阅读有关 useAsyncData 的更多信息。

返回值

useFetchuseAsyncData 具有相同的返回值,详见下文。

  • data:传递的异步函数的结果。
  • refresh/execute:可用于刷新 handler 函数返回的数据的函数。
  • clear:可用于将 data 设置为 undefined,将 error 设置为 null,将 status 设置为 idle,并标记当前正在进行的请求为已取消的函数。
  • error:如果数据获取失败,则为错误对象。
  • status:一个字符串,表示数据请求的状态("idle""pending""success""error")。
dataerrorstatus 是在 <script setup> 中使用 .value 访问的 Vue 引用。

默认情况下,Nuxt 等待 refresh 完成后,才能再次执行它。

如果没有在服务器上获取数据(例如,使用 server: false),则数据将在注水完成之前不会被获取。这意味着,即使在客户端上等待 useFetchdata<script setup> 中仍将保持为 null。

选项

useAsyncDatauseFetch 返回相同的对象类型,并且最后一个参数接受一组公共选项,用于控制组合式函数的行为,例如导航阻止、缓存或执行。

懒加载

默认情况下,数据获取组合式函数将等待其异步函数的解析,然后通过使用 Vue 的 Suspense 导航到新页面。通过 lazy 选项可以在客户端导航中忽略此功能。在这种情况下,您将需要使用 status 值来手动处理加载状态。

app.vue
<script setup lang="ts">
const { status, data: posts } = useFetch('/api/posts', {
  lazy: true
})
</script>

<template>
  <!-- 您需要处理加载状态 -->
  <div v-if="status === 'pending'">
    加载中...
  </div>
  <div v-else>
    <div v-for="post in posts">
      <!-- 做点什么 -->
    </div>
  </div>
</template>

您还可以使用 useLazyFetchuseLazyAsyncData 作为执行相同操作的便捷方法。

<script setup lang="ts">
const { status, data: posts } = useLazyFetch('/api/posts')
</script>
阅读有关 useLazyFetch 的更多信息。
阅读有关 useLazyAsyncData 的更多信息。

仅客户端获取

默认情况下,数据获取组合式函数将在服务器和客户端环境中执行其异步函数。将 server 选项设置为 false,以仅在客户端上执行调用。在首次加载时,数据不会在注水完成前被获取,因此您必须处理一种待处理状态,但在随后的客户端导航中,数据将在加载页面之前被等待。

结合 lazy 选项使用,可以用于不需要在首次渲染时使用的数据(例如,非 SEO 敏感数据)。

/* 在注水之前执行此调用 */
const 
articles
= await
useFetch
('/api/article')
/* 此调用将仅在客户端上执行 */ const {
status
,
data
:
comments
} =
useFetch
('/api/comments', {
lazy
: true,
server
: false
})

useFetch 组合式函数应在设置方法中调用或直接在生命周期钩子函数中的函数顶层调用,否则应使用 $fetch 方法

减小载荷大小

pick 选项可以帮助您通过仅选择要从组合式函数返回的字段来减小存储在HTML文档中的载荷大小。

<script setup lang="ts">
/* 仅选择您模板中使用的字段 */
const { data: mountain } = await useFetch('/api/mountains/everest', {
  pick: ['title', 'description']
})
</script>

<template>
  <h1>{{ mountain.title }}</h1>
  <p>{{ mountain.description }}</p>
</template>

如果需要更多控制或映射多个对象,您可以使用 transform 函数来改变查询结果。

const { data: mountains } = await useFetch('/api/mountains', {
  transform: (mountains) => {
    return mountains.map(mountain => ({ title: mountain.title, description: mountain.description }))
  }
})
picktransform 都不会阻止最初获取不需要的数据。但是,它们将阻止从服务器传输到客户端的负载中添加不需要的数据。

缓存和重新获取

useFetchuseAsyncData 使用键来防止重新获取相同的数据。

  • useFetch 使用提供的 URL 作为键。或者,可以在最后一个参数的选项对象中提供 key 值。
  • useAsyncData 如果第一个参数是字符串,则将其用作键。如果第一个参数是执行查询的处理函数,则将为您生成一个唯一的键,该键是文件名和行号到 useAsyncData 实例的映射,使其是唯一的。
要通过键获取缓存的数据,可以使用 useNuxtData

刷新和执行

如果要手动获取或刷新数据,请使用组合式函数提供的 executerefresh 函数。

<script setup lang="ts">
const { data, error, execute, refresh } = await useFetch('/api/users')
</script>

<template>
  <div>
    <p>{{ data }}</p>
    <button @click="() => refresh()">刷新数据</button>
  </div>
</template>

execute 函数是 refresh 的别名,用法完全相同,但在 不是立即的 的情况下更具语义化。

要全局刷新或使缓存失效,请参阅 clearNuxtDatarefreshNuxtData

清除

如果您想要清除提供的数据,无论出于什么原因,而无需知道要传递给 clearNuxtData 的特定键值,您可以使用组合式提供的 clear 函数。

<script setup lang="ts">
const { data, clear } = await useFetch('/api/users')

const route = useRoute()
watch(() => route.path, (path) => {
  if (path === '/') clear()
})
</script>

监听

要在应用程序中的其他反应性值发生更改时重新运行获取函数,可以使用 watch 选项。您可以将其用于一个或多个“可监视”的元素。

<script setup lang="ts">
const id = ref(1)

const { data, error, refresh } = await useFetch('/api/users', {
  /* 更改 id 将触发重新获取 */
  watch: [id]
})
</script>

请注意,监听反应性值不会更改获取的 URL。例如,这将继续获取用户的相同初始 ID,因为 URL 是在调用函数的时候构建的。

<script setup lang="ts">
const id = ref(1)

const { data, error, refresh } = await useFetch(`/api/users/${id.value}`, {
  watch: [id]
})
</script>

如果您想要根据反应性值更改 URL,您可能希望使用计算 URL

计算 URL

有时您可能需要从反应性值计算出一个 URL,并在每次更改时刷新数据。您可以将每个参数都作为反应性值附加到 URL。Nuxt 将自动使用反应性值并在其更改时重新获取数据。

<script setup lang="ts">
const id = ref(null)

const { data, status } = useLazyFetch('/api/user', {
  query: {
    user_id: id
  }
})
</script>

在更复杂的 URL 构建情况下,您可以使用回调作为计算获取器,以返回 URL 字符串。

每当依赖关系发生更改时,将使用新构建的 URL 获取数据。将其与 not-immediate 结合使用,可以在获取之前等待反应性元素更改。

<script setup lang="ts">
const id = ref(null)

const { data, status } = useLazyFetch(() => `/api/users/${id.value}`, {
  immediate: false
})

const pending = computed(() => status.value === 'pending');
</script>

<template>
  <div>
    <!-- 在获取时禁用输入 -->
    <input v-model="id" type="number" :disabled="pending"/>

    <div v-if="status === 'idle'">
      输入一个用户 ID
    </div>

    <div v-else-if="pending">
      加载中 ...
    </div>

    <div v-else>
      {{ data }}
    </div>
  </div>
</template>

如果需要在其他反应性值更改时强制刷新,还可以监听其他值

不立即获取

useFetch 组合式将在调用时立即开始获取数据。您可以通过设置 immediate: false 来阻止这种行为,例如等待用户交互。

通过这样做,您将需要 status 来处理获取生命周期,并使用 execute 来开始数据获取。

<script setup lang="ts">
const { data, error, execute, status } = await useLazyFetch('/api/comments', {
  immediate: false
})
</script>

<template>
  <div v-if="status === 'idle'">
    <button @click="execute">获取数据</button>
  </div>

  <div v-else-if="status === 'pending'">
    加载评论中...
  </div>

  <div v-else>
    {{ data }}
  </div>
</template>

要更精细地控制,status 变量可以是:

  • idle - 当获取尚未开始时
  • pending - 当获取已经开始但尚未完成时
  • error - 当获取失败时
  • success - 当获取成功完成时

当我们在浏览器中调用 $fetch 时,用户头部(如 cookie)将直接发送到 API。但是在服务器端渲染期间,由于 $fetch 请求发生在服务器“内部”,它不包含用户的浏览器 cookie,也不会传递来自获取响应的 cookie。

将客户端头部传递给 API

我们可以使用 useRequestHeaders 来访问和代理来自服务器的 cookie。

下面的示例将请求头部添加到同构 $fetch 调用中,以确保 API 端点可以访问用户最初发送的相同 cookie 头部。

<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])

const { data } = await useFetch('/api/me', { headers })
</script>
在将头部代理到外部 API 之前,请仔细考虑并仅包含您所需的头部。并非所有头部都可以安全地绕过,可能会导致意外行为。以下是不应进行代理的常见头部列表:
  • hostaccept
  • content-lengthcontent-md5content-type
  • x-forwarded-hostx-forwarded-portx-forwarded-proto
  • cf-connecting-ipcf-ray

在服务器端 API 调用中从 SSR 响应传递 Cookies

若您希望在内部请求返回客户端时传递/代理 cookie,您将需要自己处理此问题。

composables/fetch.ts
import { appendResponseHeader } from 'h3'
import type { H3Event } from 'h3'

export const fetchWithCookie = async (event: H3Event, url: string) => {
  /* 从服务器端点获取响应 */
  const res = await $fetch.raw(url)
  /* 从响应中获取 cookie */
  const cookies = res.headers.getSetCookie()
  /* 将每个 cookie 附加到我们的请求上 */
  for (const cookie of cookies) {
    appendResponseHeader(event, 'set-cookie', cookie)
  }
  /* 返回响应的数据 */
  return res._data
}
<script setup lang="ts">
// 此组合式将自动将 cookie 传递给客户端
const event = useRequestEvent()

const { data: result } = await useAsyncData(() => fetchWithCookie(event!, '/api/with-cookie'))

onMounted(() => console.log(document.cookie))
</script>

选项 API 支持

Nuxt 提供了一种在选项 API 中执行 asyncData 获取的方式。您必须将组件定义包装在 defineNuxtComponent 中才能完成该操作。

<script>
export default defineNuxtComponent({
  /* 使用 fetchKey 选项提供唯一键 */
  fetchKey: 'hello',
  async asyncData () {
    return {
      hello: await $fetch('/api/hello')
    }
  }
})
</script>
在 Nuxt 3 中,使用 <script setup><script setup lang="ts"> 来声明 Vue 组件是推荐的方式。
Read more in Docs > API > Utils > Define Nuxt Component.

从服务器传递到客户端的数据序列化

使用 useAsyncDatauseLazyAsyncData 将从服务器获取的数据传递到客户端时(以及其他使用 Nuxt 负载 的数据),负载将使用 devalue 进行序列化。这使我们可以传输不仅是基本 JSON,还可以序列化和恢复/反序列化更高级的数据,例如正则表达式、日期、Map 和 Set、refreactiveshallowRefshallowReactiveNuxtError 等等。

您还可以为 Nuxt 不支持的类型定义自己的序列化程序/反序列化程序。您可以在 useNuxtApp 文档中了解更多。

请注意,这不适用于使用 $fetchuseFetch 从服务器路由获取的数据,有关更多信息,请参见下一节。

从 API 路由序列化数据

server 目录获取数据时,响应将使用 JSON.stringify 进行序列化。但由于序列化仅限于 JavaScript 基本类型,Nuxt 会尽最大努力将 $fetchuseFetch 的返回类型转换为与实际值匹配。

了解有关 JSON.stringify 限制的更多信息。

示例

server/api/foo.ts
export default defineEventHandler(() => {
  return new Date()
})
app.vue
<script setup lang="ts">
// 尽管我们返回了一个 Date 对象,但 `data` 的类型被推断为 string
const { data } = await useFetch('/api/foo')
</script>

自定义序列化函数

要自定义序列化行为,您可以在返回的对象上定义一个 toJSON 函数。如果您定义了 toJSON 方法,Nuxt 将尊重函数的返回类型,而不会尝试转换类型。

server/api/bar.ts
export default defineEventHandler(() => {
  const data = {
    createdAt: new Date(),

    toJSON() {
      return {
        createdAt: {
          year: this.createdAt.getFullYear(),
          month: this.createdAt.getMonth(),
          day: this.createdAt.getDate(),
        },
      }
    },
  }
  return data
})
app.vue
<script setup lang="ts">
// `data` 的类型被推断为
// {
//   createdAt: {
//     year: number
//     month: number
//     day: number
//   }
// }
const { data } = await useFetch('/api/bar')
</script>

使用替代序列化程序

Nuxt 当前不支持将替代序列化程序用于 JSON.stringify。但是,您可以将负载作为普通字符串返回,并利用 toJSON 方法来保持类型安全。

在下面的示例中,我们使用 superjson 作为序列化程序。

server/api/superjson.ts
import superjson from 'superjson'

export default defineEventHandler(() => {
  const data = {
    createdAt: new Date(),

    // 绕过类型转换的方法
    toJSON() {
      return this
    }
  }

  // 将输出序列化为字符串,使用 superjson
  return superjson.stringify(data) as unknown as typeof data
})
app.vue
<script setup lang="ts">
import superjson from 'superjson'

// `date` 以 { createdAt: Date } 的形式推断,您可以安全地使用 Date 对象的方法
const { data } = await useFetch('/api/superjson', {
  transform: (value) => {
    return superjson.parse(value as unknown as string)
  },
})
</script>

配方

通过 POST 请求使用 SSE(服务器推送事件)

如果您通过 GET 请求使用 SSE,可以使用 EventSource 或 VueUse 组合式 useEventSource.

当通过 POST 请求使用 SSE 时,您需要手动处理连接。以下是您可以执行的操作:

// 向 SSE 端点发出 POST 请求
const response = await $fetch<ReadableStream>('/chats/ask-ai', {
  method: 'POST',
  body: {
    query: "Hello AI, how are you?",
  },
  responseType: 'stream',
})

// 使用 TextDecoderStream 创建新的 ReadableStream 以获取文本数据
const reader = response.pipeThrough(new TextDecoderStream()).getReader()

// 在获取到的数据块
while (true) {
  const { value, done } = await reader.read()

  if (done)
    break

  console.log('Received:', value)
}