数据获取

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

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

简而言之:

  • $fetch 是发起网络请求的最简单方式。
  • useFetch 是对 $fetch 的封装,仅在 通用渲染 中获取一次数据。
  • useAsyncDatauseFetch 类似,但提供了更精细的控制。

useFetchuseAsyncData 共享一组通用的选项和模式,我们将在最后几节详细介绍。

为什么需要 useFetchuseAsyncData

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

useFetchuseAsyncData 组合式函数通过确保服务器端的 API 调用数据会被包含在负载中转发到客户端,从而解决了这一问题。

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

使用 Nuxt DevToolsPayload 选项卡 中检查这些数据。
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 没有这种机制,因此更适合仅在浏览器中发起的请求。

Suspense

Nuxt 在底层使用了 Vue 的 <Suspense> 组件,以防止在所有异步数据准备好之前进行导航。数据获取的组合式函数(composables)可以帮助你更好地利用这一特性,并根据每次调用的具体情况选择最合适的方式。

你可以添加 <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 之前要非常小心,仅包含你需要的头部。并非所有头部都适合直接绕过,可能会引入意外行为。以下是一些不应代理的常见头部列表:
  • host, accept
  • content-length, content-md5, content-type
  • x-forwarded-host, x-forwarded-port, x-forwarded-proto
  • cf-connecting-ip, cf-ray ::

useFetch

useFetch 组合式函数在底层使用 $fetchsetup 函数中进行 SSR 安全的网络调用。
app.vue
<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>

<template>
  <p>页面访问量:{{ count }}</p>
</template>
此组合式函数是对 useAsyncData 组合式函数和 $fetch 工具的封装。

阅读并编辑实时示例中的内容 Docs > Examples > Features > Data Fetching.

useAsyncData

useAsyncData 组合式函数负责包装异步逻辑并在解析完成后返回结果。
useFetch(url) 几乎等同于 useAsyncData(url, () => event.$fetch(url))。:br 这是为最常见用例提供的开发者体验优化。(你可以在 useRequestFetch 了解更多关于 event.fetch 的信息。)

在某些情况下,使用 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 的引用,在 <script setup> 中通过 .value 访问。

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

如果你没有在服务器端获取数据(例如,使用 server: false),那么在水合完成之前数据 不会 被获取。这意味着即使你在客户端 await useFetch,在 <script setup>data 仍将保持为 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 组合式函数应在 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 作为 key。或者,也可以在作为最后一个参数传入的 options 对象中提供一个 key 值。
  • useAsyncData 如果第一个参数是字符串,则使用它作为 key;如果第一个参数是执行查询的处理函数(handler function),则会自动为你生成一个基于文件名和代码行号的唯一 key。
要通过键获取缓存数据,可以使用 useNuxtData

共享状态和选项一致性

当多个组件使用相同的 key 调用 useAsyncDatauseFetch 时,它们将共享相同的 dataerrorstatus 引用。这保证了组件之间的一致性,但要求一些选项必须保持一致。

以下选项 必须在使用相同 key 的所有调用中保持一致

  • handler 函数
  • deep 选项
  • transform 函数
  • pick 数组
  • getCachedData 函数
  • default
// ❌ 这将触发开发警告
const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { deep: false })
const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { deep: true })

以下选项 可以安全地不同,而不会触发警告

  • server
  • lazy
  • immediate
  • dedupe
  • watch
// ✅ 这是允许的
const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { immediate: true })
const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { immediate: false })

如果你需要独立的实例,可以使用不同的 key:

// 这些是完全独立的实例
const { data: users1 } = useAsyncData('users-1', () => $fetch('/api/users'))
const { data: users2 } = useAsyncData('users-2', () => $fetch('/api/users'))

响应式键

你可以使用计算属性、普通引用或 getter 函数作为 key,这允许动态数据获取,并在依赖项变化时自动更新:

// 使用计算属性作为 key
const userId = ref('123')
const { data: user } = useAsyncData(
  computed(() => `user-${userId.value}`),
  () => fetchUser(userId.value)
)

// 当 userId 变化时,数据将自动重新获取
// 并且如果没有其他组件使用旧数据,它将被清除
userId.value = '456'

刷新和执行

如果你想手动获取或刷新数据,可以使用组合式函数提供的 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,并在这些值变化时刷新数据。你可以将每个参数作为响应式值附加,Nuxt 会自动使用响应式值并在每次变化时重新获取。

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

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

对于更复杂的 URL 构建,你可以使用一个返回 URL 字符串的 计算 getter 作为回调。

每次依赖项发生变化时,将使用新构建的 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:当获取成功完成时

在浏览器中调用 $fetch 时,用户的头部(如 cookie)会直接发送到 API。

通常,在服务器端渲染期间,出于安全考虑,$fetch 不会包含用户浏览器的 cookie,也不会传递获取响应的 cookie。

然而,当在服务器端使用相对 URL 调用 useFetch 时,Nuxt 会使用 useRequestFetch 来代理头部和 cookie(不包括不应转发的头部,如 host)。

如果你想反向传递/代理 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>

Options API 支持

Nuxt 提供了在 Options 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 组件。

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

在使用 useAsyncDatauseLazyAsyncData 将服务器端获取的数据传输到客户端时(以及其他利用 Nuxt 负载 的内容),负载会使用 devalue 进行序列化。这允许我们不仅传输基本 JSON,还可以序列化和恢复/反序列化更高级的数据类型,如正则表达式、Date、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` 的类型被推断为字符串
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)
}

并行请求

当请求之间没有依赖关系时,你可以使用 Promise.all() 并行发起请求以提升性能。
const { data } = await useAsyncData(() => {
  return Promise.all([
    $fetch("/api/comments/"), 
    $fetch("/api/author/12")
  ]);
});

const comments = computed(() => data.value?.[0]);
const author = computed(() => data.value?.[1]);