数据获取

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

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

简而言之:

  • $fetch 是进行网络请求的最简单方法。
  • useFetch 是对 $fetch 的封装,仅在 通用渲染 中进行一次数据获取。
  • useAsyncData 类似于 useFetch,但提供了更细粒度的控制。

useFetchuseAsyncData 都共享一组通用选项和模式,我们将在最后的部分详细说明。

为何需要 useFetchuseAsyncData

Nuxt 是一个框架,可以在服务器和客户端环境中运行同构(或通用)代码。如果在 Vue 组件的 setup 函数中使用 $fetch 函数 来执行数据获取,这可能导致数据被获取两次,一次在服务器上(渲染 HTML),另一次在客户端(当 HTML 被水合时)。这可能导致水合问题,增加交互时间并导致不可预测的行为。

useFetchuseAsyncData 组合函数通过确保如果在服务器上进行 API 调用,则数据将转发给客户端的有效载荷,来解决此问题。

有效载荷是一个通过 useNuxtApp().payload 可访问的 JavaScript 对象。它用于在客户端避免在浏览器中执行代码时重新获取相同的数据 在水合期间

使用 Nuxt DevTools有效载荷选项卡 中检查这些数据。
app.vue
<script setup lang="ts">
const { data } = await useFetch('/api/data')

async function handleFormSubmit() {
  const res = await $fetch('/api/submit', {
    method: 'POST',
    body: {
      // 我的表单数据
    }
  })
}
</script>

<template>
  <div v-if="data == null">
    没有数据
  </div>
  <div v-else>
    <form @submit="handleFormSubmit">
      <!-- 表单输入标签 -->
    </form>
  </div>
</template>

在上面的例子中,useFetch 将确保请求在服务器上发生并正确转发到浏览器。$fetch 没有这样的机制,因此在请求仅从浏览器发起时使用它是更好的选择。

暂停

Nuxt 在内部使用 Vue 的 <Suspense> 组件,防止在每次异步数据可用于视图之前导航。数据获取组合函数可以帮助您利用此功能,并根据每次调用使用最适合的函数。

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

$fetch

Nuxt 包含 ofetch 库,并在整个应用程序中作为 $fetch 别名自动导入。

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

将客户端请求头传递给 API

当在服务器上调用 useFetch 时,Nuxt 将使用 useRequestFetch 来代理客户端请求头和 cookie(不包括不打算转发的请求头,如 host)。

<script setup lang="ts">
const { data } = await useFetch('/api/echo');
</script>
// /api/echo.ts
export default defineEventHandler(event => parseCookies(event))

或者,下面的示例展示了如何使用 useRequestHeaders 从服务器端请求中访问并发送 cookie 到 API(源于客户端)。使用同构的 $fetch 调用,我们确保 API 端点能够访问用户浏览器最初发送的相同 cookie 头。如果不使用 useFetch,则这是必需的。

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

async function getCurrentUser() {
  return await $fetch('/api/me', { headers })
}
</script>
您还可以使用 useRequestFetch 自动将请求头代理到调用中。
在将请求头代理到外部 API 之前请格外小心,仅包含您所需的请求头。并非所有请求头都是安全的,可能会引入不必要的行为。以下是一些常见不应代理的请求头:
  • hostaccept
  • content-lengthcontent-md5content-type
  • x-forwarded-hostx-forwarded-portx-forwarded-proto
  • cf-connecting-ipcf-ray

useFetch

useFetch 组合函数在底层使用 $fetch 来进行 SSR 安全的网络调用。

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.

useAsyncData

useAsyncData 组合函数负责封装异步逻辑并在解析时返回结果。

useFetch(url) 几乎等同于 useAsyncData(url, () => event.$fetch(url))。:br 它为最常见的用例提供了开发者体验的糖。 (您可以在 useRequestFetch 中找到更多关于 event.fetch 的信息。)
观看 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 用于获取和缓存数据,而不是触发副作用,例如调用 Pinia 动作,因为这可能会导致意外行为,例如重复执行和空值。如果您需要触发副作用,请使用 callOnce 实用程序来执行。
<script setup lang="ts">
const offersStore = useOffersStore()

// 你不能这样做
await useAsyncData(() => offersStore.getOffer(route.params.slug))
</script>
了解更多有关 useAsyncData 的信息。

返回值

useFetchuseAsyncData 的返回值如下所示相同。

  • data:作为参数传入的异步函数的结果。
  • refresh/execute:一个可以用来刷新 handler 函数返回数据的函数。
  • clear:一个可以用来将 data 设置为 undefined,将 error 设置为 null,将 status 设置为 idle,并标记任何当前待处理请求为取消的函数。
  • error:如果数据获取失败,则为错误对象。
  • status:一个字符串,表示数据请求的状态("idle""pending""success""error")。
dataerrorstatus 是 Vue refs,在 <script setup> 中可以通过.value 访问。

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

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

选项

useAsyncDatauseFetch 返回相同的对象类型,并接受最后一个参数作为共同的一组选项。它们可以帮助您控制组合函数的行为,例如导航阻止、缓存或执行。

懒加载

默认情况下,数据获取组合函数将在其异步函数解析后,使用 Vue 的可暂停组件等待新页面的导航。使用 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 组合函数旨在在 setup 方法中调用,或直接在生命周期钩子的函数顶部调用,否则应使用 $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 作为键。或者,可以在作为最后一个参数传递的 options 对象中提供 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

清除

如果您想要清除提供的数据,可以使用组合函数提供的 clear 函数,而不需要知道要传递给 clearNuxtData 的特定密钥。

<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,并在这些值更改时刷新数据。您可以将每个参数附加为响应值。Nuxt 将自动使用响应值,并在每次变化时重新获取。

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

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

在更复杂的 URL 构建场景中,您可以使用回调作为 计算 getter,返回 URL 字符串。

每次依赖项变化时,数据将使用新构建的 URL 进行获取。将此与 不立即 结合使用,您可以等到响应元素更改后再获取。

<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 当获取成功完成时

传递请求头和 Cookies

当我们在浏览器中调用 $fetch 时,用户请求头如 cookie 将直接发送到 API。

通常,在服务器端渲染过程中,由于安全原因,$fetch 不会包括用户浏览器的 cookie,也不会将 fetch 响应中的 cookies 传递。

然而,当在服务器上使用相对 URL 调用 useFetch 时,Nuxt 将使用 useRequestFetch 来代理请求头和 cookies(不包括不应转发的请求头,如 host)。

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

如果您想要在另一个方向上传递/代理 cookies,从内部请求返回到客户端,您将需要自己处理。

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)
  /* 获取响应中的 cookies */
  const cookies = res.headers.getSetCookie()
  /* 将每个 cookie 附加到我们的请求中 */
  for (const cookie of cookies) {
    appendResponseHeader(event, 'set-cookie', cookie)
  }
  /* 返回响应数据 */
  return res._data
}
<script setup lang="ts">
// 此组合函数将在客户中自动传递 cookies
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>
使用 <script setup><script setup lang="ts"> 是在 Nuxt 中定义 Vue 组件的推荐方式。
Read more in Docs > API > Utils > Define Nuxt Component.

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

当使用 useAsyncDatauseLazyAsyncData 将在服务器上获取的数据传输到客户端时(以及利用 Nuxt 有效载荷 的其他任何内容),有效载荷通过 devalue 被序列化。这使我们能够传输不仅仅是基本的 JSON,还可以序列化和复活/反序列化更高级的数据类型,例如正则表达式、日期、映射和集合、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">
// `data` 的类型被推断为字符串,尽管我们返回了一个 Date 对象
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: "你好 AI,你好吗?",
  },
  responseType: 'stream',
})

// 从响应创建一个新的 ReadableStream,使用 TextDecoderStream 将数据作为文本获取
const reader = response.pipeThrough(new TextDecoderStream()).getReader()

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

  if (done)
    break

  console.log('接收:', value)
}