数据获取

Nuxt 提供 composables 来处理应用中的数据获取。

Nuxt 内置了两个 composable 和一个库,用于在浏览器或服务端环境中执行数据获取:useFetch、useAsyncData 和 $fetch。

简而言之:

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

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

为什么需要 useFetch 和 useAsyncData

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

useFetch 和 useAsyncData composable 解决了这个问题:如果 API 请求在服务端发起,数据会通过 payload 转发到客户端。

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

使用 Nuxt DevTools 在 Payload 标签页中查看这些数据。

<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 == undefined">
    暂无数据
  </div>
  <div v-else>
    <form @submit="handleFormSubmit">
      <!-- 表单输入标签 -->
    </form>
  </div>
</template>

上面的例子中,useFetch 确保请求在服务端发起并正确转发到浏览器。$fetch 没有这种机制,更适合仅在浏览器中发起的请求。

Suspense

Nuxt 底层使用 Vue 的 <Suspense> 组件,在所有异步数据就绪前阻止导航。数据获取 composable 可以帮你利用这个特性,按调用选择最合适的方式。

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

关于 await 的说明

本文档中的示例通常 await useFetch 和 useAsyncData 的调用,但这不是必须的。

await 不会改变服务端渲染的 HTML。在服务端渲染期间,Nuxt 无论如何都会等待请求 resolve 后再序列化页面(底层是 <Suspense> 和 onServerPrefetch),所以完整填充的结果总是会发送到浏览器。

await 真正改变的是你自己的 <script setup> 中接下来的执行,以及客户端导航的行为:

  • 有 await:执行暂停直到数据就绪,所以调用后的代码可以依赖 data 已经填充。客户端导航时,这会阻塞导航直到数据 resolve:用户停留在当前页面(可选显示 <NuxtLoadingIndicator>),然后到达完整填充的页面。这是默认行为。
  • 没有 await:请求在后台运行时执行立即继续,所以 data 从默认值开始,请求 resolve 后填充。客户端导航时,导航立即发生,你需要自己处理加载和错误状态,通常通过返回的 status 和 error refs。

两种方式没有绝对优劣;正确的选择取决于你希望该路由的体验。

不 await 与 lazy 选项有类似的用户可见效果(不阻塞导航,自己处理加载状态),但两者不完全相同:lazy 是显式标志,将请求推迟到组件挂载时;而简单地不 await 是在 setup 期间就发起请求。想要非阻塞行为时,优先用 lazy(或 useLazyFetch / useLazyAsyncData),因为它让意图更明确。

await 和 lazy 是独立的,在客户端 await 一个 lazy 函数不会有你期望的效果。如果你 await 一个 lazy 调用(比如 await useLazyFetch(...) 或 await useFetch(..., { lazy: true })),它仍然像往常一样阻塞服务端渲染,但在客户端导航时,await 立即 resolve 而不等待请求。data 在 await 之后仍然是默认值,你必须通过 status 处理加载状态。如果你真的想让导航等待数据,去掉 lazy 选项,而不是依赖 await。

$fetch

Nuxt 包含 ofetch 库,并在应用中全局自动导入为 $fetch 别名。

<script setup lang="ts">
async function addTodo () {
  const todo = await $fetch('/api/todos', {
    method: 'POST',
    body: {
      // 我的 todo 数据
    },
  })
}
</script>

注意,仅使用 $fetch 不会提供网络调用去重和导航阻止。

建议客户端交互(基于事件)使用 $fetch,或者在获取初始组件数据时与 useAsyncData 结合使用。

阅读更多关于 $fetch。

为持有 $fetch 的值指定类型

要为持有 $fetch 本身的值指定类型(比如传入插件的 $fetch 实例,或通过 provide/inject 提供的),使用 $Fetch 类型(TypedFetch 的别名):

import type { $Fetch } from '#app'

function createClient (fetcher: $Fetch) {
  return {
    todos: () => fetcher('/api/todos'),
  }
}

传递客户端请求头到 API

在服务端调用 useFetch 时,Nuxt 会使用 useRequestFetch 代理客户端请求头和 cookies(不包括不应转发的请求头,如 host)。

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

或者,下面的例子展示如何使用 useRequestHeaders 从服务端请求(源于客户端)访问并发送 cookies 到 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 composable 底层使用 $fetch 在 setup 函数中发起 SSR 安全的网络调用。

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

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

这个 composable 是 useAsyncData composable 和 $fetch 工具的封装。

useAsyncData

useAsyncData composable 负责封装异步逻辑,并在结果 resolve 后返回。

useFetch(url) 几乎等同于 useAsyncData(url, () => event.$fetch(url))。

它是最常见用例的开发体验语法糖。(可以在 useRequestFetch 中了解更多 event.fetch。)

有些场景不适合使用 useFetch composable,比如 CMS 或第三方提供自己的查询层时。这种情况下,可以使用 useAsyncData 封装调用,仍然保留 composable 提供的好处。

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

// 也可以这样:
const { data, error } = await useAsyncData(() => myGetFunction('users'))
</script>

useAsyncData 的第一个参数是唯一键,用于缓存第二个参数(查询函数)的响应。可以直接传入查询函数来忽略这个键,键会自动生成。


由于自动生成的键只考虑 useAsyncData 被调用的位置,建议始终创建自己的键来避免不期望的行为,比如在你自己封装 useAsyncData 的自定义 composable 中。


设置键可以用于通过 useNuxtData 在组件间共享相同数据,或刷新特定数据。

<script setup lang="ts">
const { id } = useRoute().params

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

useAsyncData composable 是封装并等待多个 $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 action),因为这可能导致意外行为,比如以空值重复执行。如果需要触发副作用,使用 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 设为 undefined、将 status 设为 idle,并将当前所有待处理请求标记为已取消的函数。
  • error:数据获取失败时的错误对象。
  • status:表示数据请求状态的字符串("idle"、"pending"、"success"、"error")。

data、error 和 status 是 Vue refs,在 <script setup> 中通过 .value 访问

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

如果你没有在服务端获取数据(比如用 server: false),那么数据不会在水合完成前获取。这意味着即使在客户端 await useFetch,data 在 <script setup> 中仍然是 undefined。

选项

useAsyncData 和 useFetch 返回相同类型的对象,并接受一组通用选项作为最后一个参数。它们可以帮你控制 composable 行为,比如导航阻塞、缓存或执行。

Lazy

默认情况下,数据获取 composable 会使用 Vue 的 Suspense 等待异步函数 resolve 后再导航到新页面。这个功能可以在客户端导航时通过 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。

仅客户端获取

默认情况下,数据获取 composable 会在客户端和服务端环境都执行异步函数。设置 server 选项为 false 则仅在客户端执行调用。初始加载时,数据不会在水合完成前获取,所以你需要处理 pending 状态;不过在后续客户端导航中,数据会在加载页面前被 await。

与 lazy 选项结合,这对于首次渲染不需要的数据(比如非 SEO 敏感数据)很有用。

/* 这个调用在水合前执行 */
const articles = await useFetch('/api/article')

/* 这个调用仅在客户端执行 */
const { status, data: comments } = useFetch('/api/comments', {
  lazy: true,
  server: false,
})

useFetch composable 应该在 setup 方法中调用,或在生命周期 hooks 的顶层直接调用,否则应该使用 $fetch 方法。

最小化 payload 大小

pick 选项通过只选择你想从 composable 返回的字段,来帮助最小化存储在 HTML 文档中的 payload 大小。

<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 都不会阻止初始获取不需要的数据。但它们会阻止不需要的数据被添加到从服务端传输到客户端的 payload 中。

缓存和重新获取

键

useFetch 和 useAsyncData 使用键来防止重复获取相同数据。

  • useFetch 从 URL、fetch 选项和调用在源代码中的位置生成键。这意味着不同组件中相同 URL 的两个 useFetch 调用有不同的键,各自发起自己的请求。要在多个组件间共享相同数据,在作为最后一个参数传入的 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 }))

响应式键

你可以使用 computed refs、普通 refs 或 getter 函数作为键,实现依赖变化时自动更新的动态数据获取:

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

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

Refresh 和 execute

如果你想手动获取或刷新数据,使用 composable 提供的 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。

Clear

如果你想清除提供的数据(无论什么原因),而不需要知道传给 clearNuxtData 的具体键,可以使用 composable 提供的 clear 函数。

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

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

Watch

要在应用中其他响应式值变化时重新运行获取函数,使用 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,可以考虑使用 computed URL。

当提供响应式 fetch 选项时,它们会被自动观察并触发重新获取。某些情况下,可以通过指定 watch: false 来选择退出这种行为。

const id = ref(1)

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

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

Computed URL

有时你需要从响应式值计算 URL,并在这些值变化时刷新数据。不用折腾,可以将每个参数作为响应式值附加。Nuxt 会自动使用响应式值,并在每次变化时重新获取。

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

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

对于更复杂的 URL 构造,可以使用回调作为 computed getter,返回 URL 字符串。

每次依赖变化时,会使用新构造的 URL 获取数据。结合非立即,可以等到响应式元素变化后再获取。

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

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

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

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

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

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

如果你需要在其他响应式值变化时强制刷新,也可以观察其他值。

非立即

useFetch composable 在调用时立即开始获取数据。可以设置 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 不会包含用户的浏览器 cookies,也不会传递 fetch 响应的 cookies。

不过,在服务端调用 useFetch 且使用相对 URL 时,Nuxt 会使用 useRequestFetch 代理请求头和 cookies(不包括不应转发的请求头,如 host)。

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

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

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 附加到传入的 Request */
  for (const cookie of cookies) {
    appendResponseHeader(event, 'set-cookie', cookie)
  }
  /* 返回响应的数据 */
  return res._data
}
<script setup lang="ts">
// 这个 composable 会自动将 cookies 传递给客户端
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 组件的推荐方式。

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

使用 useAsyncData 和 useLazyAsyncData 将服务端获取的数据传输到客户端时(以及任何使用 Nuxt payload 的情况),payload 会用 devalue 序列化。这让我们不仅能传输基本 JSON,还能序列化和反序列化更高级的数据类型,比如正则表达式、Date、Map 和 Set、ref、reactive、shallowRef、shallowReactive 和 NuxtError——等等。

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

注意,这不适用于用 $fetch 或 useFetch 获取的服务端路由传递的数据——参见下一节了解更多。

API 路由的数据序列化

从 server 目录获取数据时,响应用 JSON.stringify 序列化。不过,由于序列化仅限于 JavaScript 原始类型,Nuxt 会尽力转换 $fetch 和 useFetch 的返回类型以匹配实际值。

了解更多关于 JSON.stringify 的限制。

示例

export default defineEventHandler(() => {
  return new Date()
})
<script setup lang="ts">
// `data` 的类型被推断为 string,即使我们返回了 Date 对象
const { data } = await useFetch('/api/foo')
</script>

自定义序列化器函数

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

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
})
<script setup lang="ts">
// `data` 的类型被推断为
// {
//   createdAt: {
//     year: number
//     month: number
//     day: number
//   }
// }
const { data } = await useFetch('/api/bar')
</script>

使用替代序列化器

Nuxt 目前不支持替代 JSON.stringify 的序列化器。不过,你可以将 payload 作为普通字符串返回,并利用 toJSON 方法保持类型安全。

下面的例子中,我们使用 superjson 作为序列化器。

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

发起并行请求

当请求不相互依赖时,可以用 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])