Nuxt 3 已于 31 July 2026 结束生命周期
此版本将不再接收错误修复或安全补丁。 升级到 Nuxt 4 或获取 HeroDevs 提供的延长支持

数据获取

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

Maschinelle Übersetzung ·0 % geprüft

Nuxt 内置了两个组合式函数和一个库,可在浏览器或服务器环境中执行数据获取:useFetch、useAsyncData 和 $fetch。

简而言之:

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

useFetch 和 useAsyncData 都共享一组通用的选项和模式,我们将在最后的章节中详细介绍。

对 useFetch 和 useAsyncData 的需求

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

useFetch 和 useAsyncData 组合式函数通过确保在服务器上发起的 API 调用会将数据转发到客户端负载中,解决了这个问题。

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

使用 Nuxt DevTools 在 Payload 标签页 检查这些数据。
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> 组件阻止导航,直到所有异步数据可用于视图。数据获取组合函数可以帮助你利用此功能,并根据单次调用情况选择最合适的方式。

你可以添加 <NuxtLoadingIndicator>,在页面导航之间显示进度条。

$fetch

Nuxt 包含了 ofetch 库,并在全局自动导入成 $fetch 别名。

<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 组合式函数在底层使用 $fetch,在 setup 函数中发起 SSR 安全的网络调用。

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

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

这个组合式函数封装了 useAsyncData 组合式函数和 $fetch 工具。

Weitere Informationen findest du unter 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 的信息。)

在某些情况下,不适合使用 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 (_nuxtApp, { signal }) => {
  const [coupons, offers] = await Promise.all([
    $fetch('/cart/coupons', { signal }),
    $fetch('/cart/offers', { signal }),
  ])

  return { coupons, offers }
})
// discounts.value.coupons
// discounts.value.offers
</script>

useAsyncData 用于获取和缓存数据,而不是触发诸如调用 Pinia actions 之类的副作用,因为这可能导致重复执行等意外行为,例如使用空值重复执行。如果需要触发副作用,请使用 callOnce 工具。

<script setup lang="ts">
const offersStore = useOffersStore()

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

返回值

useFetch 和 useAsyncData 返回相同的对象,包含如下内容:

  • data:传入的异步函数结果。
  • refresh/execute:用于刷新由 handler 函数返回的数据。
  • clear:用于将 data 设为 undefined(或提供的 options.default() 的值),error 设为 null,status 设为 idle,并将当前所有待定请求标记为取消。
  • error:数据获取失败时的错误对象。
  • status:表示数据请求状态的字符串("idle"、"pending"、"success"、"error")。
data、error 和 status 是 Vue 的 ref,在 <script setup> 中用 .value 访问。

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

如果你没有在服务器上获取数据(例如,使用 server: false),那么直到水合完成后才会获取数据。这意味着即使你在客户端等待 useFetch,<script setup> 中的 data 仍会是 undefined。

选项

useAsyncData 和 useFetch 返回相同类型的对象,并在最后一个参数中接受一组通用选项。这些选项可以帮助你控制组合式函数的行为,例如是否阻止导航、缓存或执行。

延迟加载

默认情况下,数据获取组合函数会等待其异步函数解析完成后,利用 Vue 的 Suspense 阻止页面导航。客户端导航时,可以通过 lazy 选项忽略此功能。此时,你需要手动使用 status 管理加载状态。

<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>

你也可以使用 useLazyFetch 和 useLazyAsyncData,以便捷的方式实现相同功能。

<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 }))
  },
})
pick 和 transform 不阻止初始数据获取,只阻止不必要数据被加入服务器到客户端的负载中。

缓存与重新获取

键

useFetch 和 useAsyncData 使用键来避免重复获取相同的数据。

  • useFetch 使用提供的 URL 作为键。或者,也可以在作为最后一个参数传入的 options 对象中提供 key 值。
  • useAsyncData 在第一个参数为字符串时将其用作键。如果第一个参数是执行查询的 handler 函数,则会根据 useAsyncData 实例所在的文件名和行号为你生成一个唯一键。
要按键获取缓存数据,可以使用 useNuxtData

共享状态与选项一致性

多个组件若使用相同的 useAsyncData 或 useFetch 键,将共享 data、error 和 status refs,保证状态一致。但这要求某些选项保持一致。

以下选项必须在所有同键调用中保持一致:

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

以下选项可安全不同且不会触发警告:

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

若需要独立实例,请使用不同键:

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

响应式键

你可以使用计算属性 ref、普通 ref 或 getter 函数作为键,实现依赖变化时自动重新获取数据:

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

// 当 userId 变化时数据会自动刷新,且若无其他组件使用旧数据则被清理
userId.value = '456'

refresh 和 execute

如果想手动获取或刷新数据,可使用组合函数提供的 execute 或 refresh 函数。

<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 的别名,工作方式完全相同,但在获取非即时时语义更明确。

要全局重新获取或使缓存数据失效,请参阅 clearNuxtData 和 refreshNuxtData。

清理

若想清理已有数据而不必使用特定键调用 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。例如,下面代码会一直请求初始的 user id:

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

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

如果需要根据响应式值更改 URL,可以改用计算 URL。

提供响应式获取选项时,Nuxt 会自动监听它们并触发重新获取。在某些情况下,可以通过指定 watch: false 来选择不采用此行为。

const id = ref(1)

// 当 id 变化时不会自动重新获取
const { data, execute } = await useFetch('/api/users', {
  query: { id }, // 默认会监听 id
  watch: false, // 禁用对 id 的自动监听
})

// 不会触发重新获取
id.value = 2

计算 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:请求成功完成

在浏览器执行 $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)
  /* 从响应中获取 Cookies */
  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 提供在 Options API 中执行 asyncData 获取数据的方式。使用时需将组件定义包裹在 defineNuxtComponent 中。

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

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

使用 useAsyncData 和 useLazyAsyncData 将服务器上获取的数据传输到客户端时(以及使用Nuxt 负载的其他功能),负载会使用 devalue 进行序列化。这不仅允许我们传输基本的 JSON,还可以序列化并恢复/反序列化更高级的数据类型,例如正则表达式、Dates、Map 和 Set、ref、reactive、shallowRef、shallowReactive 和 NuxtError 等。

你也可以为 Nuxt 不支持的类型定义自己的序列化器/反序列化器。更多信息请参阅 useNuxtApp 文档。

注意,此功能不适用于使用 $fetch 或 useFetch 从服务器路由获取数据后传入的内容——详见下一节。

API 路由中的数据序列化

从 server 目录获取数据时,响应会使用 JSON.stringify 进行序列化。然而,由于序列化仅限于 JavaScript 原始类型,Nuxt 会尽力将 $fetch 和 useFetch 的返回类型转换为与实际值相匹配的类型。

了解更多关于 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 函数。定义后,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'

// `data` 推断为 { createdAt: 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',
})

// 创建一个新的 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((_nuxtApp, { signal }) => {
  return Promise.all([
    $fetch('/api/comments/', { signal }),
    $fetch('/api/author/12', { signal }),
  ])
})

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