数据获取

Nuxt 为您的应用提供了用于处理数据获取的 composables。

Nuxt 附带两个 composables 和一个内置库,用于在浏览器或服务器环境中执行数据获取:useFetchuseAsyncData$fetch

简而言之:

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

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

需要 useFetchuseAsyncData 的原因

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

useFetchuseAsyncData 这两个 composables 通过确保如果在服务器上进行了 API 调用,则会将数据转发到客户端的 payload 来解决此问题。

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

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

async function handleFormSubmit () {
  const res = await $fetch('/api/submit', {
    method: 'POST',
    body: {
      // My form data
    },
  })
}
</script>

<template>
  <div v-if="data == undefined">
    No data
  </div>
  <div v-else>
    <form @submit="handleFormSubmit">
      <!-- form input tags -->
    </form>
  </div>
</template>

在上面的示例中,useFetch 会确保请求在服务器上发生并正确转发到浏览器。$fetch 没有这种机制,当请求仅在浏览器端发起时,使用 $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: {
      // My todo data
    },
  })
}
</script>
请注意,仅使用 $fetch 无法提供网络调用去重和导航阻止
建议在客户端交互(基于事件)中使用 $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 在服务端请求(源自客户端)中访问并向 API 发送 cookie。通过使用同构的 $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 在 setup 函数中使用 $fetch 作为底层实现,以便进行 SSR 安全的网络调用。

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

<template>
  <p>Page visits: {{ count }}</p>
</template>

该 composable 是对 useAsyncData composable 和 $fetch 工具的封装。

Read more in Docs > 4 X > API > Composables > Use Fetch.
Read and edit a live example in Docs > 4 X > Examples > Features > Data Fetching.

useAsyncData

useAsyncData composable 负责包装异步逻辑并在其解析后返回结果。

useFetch(url)useAsyncData(url, () => event.$fetch(url)) 基本等价。
它是针对最常见用例的开发者体验糖衣。(您可以在 useRequestFetch 中了解更多关于 event.fetch 的信息。)

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

app/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 的自定义 composable 时。

设置键也可以用于在使用 useNuxtData 的组件之间共享相同数据,或用于刷新特定数据
app/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 的 actions),因为这可能导致诸如重复执行且带有空值等意外行为。如果您需要触发副作用,请使用 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(或如果提供了 options.default() 则为该值)、将 error 设置为 undefined、将 status 设置为 idle,并将任何当前挂起的请求标记为已取消的函数。
  • error:如果数据获取失败,则为错误对象。
  • status:指示数据请求状态的字符串("idle""pending""success""error")。
dataerrorstatus 是 Vue 的 refs,在 <script setup> 中可通过 .value 访问。

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

如果您没有在服务器上获取数据(例如,设置了 server: false),那么数据将在 hydration 完成之前不会被获取。这意味着即使在客户端等待 useFetch,在 <script setup>data 仍将保持为 null。

选项

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

延迟(Lazy)

默认情况下,数据获取的 composables 会在其异步函数解析完成之前使用 Vue 的 Suspense 等待,从而阻止导航。在客户端导航时,可以通过 lazy 选项忽略此功能。在这种情况下,您需要使用 status 值手动处理加载状态。

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

<template>
  <!-- 你需要自己处理加载状态 -->
  <div v-if="status === 'pending'">
    Loading ...
  </div>
  <div v-else>
    <div v-for="post in posts">
      <!-- do something -->
    </div>
  </div>
</template>

您也可以使用 useLazyFetchuseLazyAsyncData 作为方便的方法来实现相同功能。

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

仅客户端获取

默认情况下,数据获取的 composables 会在客户端和服务器环境中执行它们的异步函数。将 server 选项设置为 false 可仅在客户端执行调用。在初次加载时,数据不会在 hydration 完成之前被获取,因此您必须处理挂起(pending)状态;不过在随后的客户端导航中,数据会在加载页面之前被等待。

lazy 选项结合使用时,这对首次渲染时不需要的数据(例如非 SEO 相关数据)非常有用。

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

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

useFetch composable 应在 setup 方法中被调用或在生命周期钩子的顶层函数中直接调用,否则应使用 $fetch 方法

最小化 payload 大小

pick 选项可以帮助您最小化存储在 HTML 文档中的 payload 大小,只选择您希望从 composables 返回的字段。

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

缓存与重新获取

键(Keys)

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

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

共享状态与选项一致性

当多个组件对相同键使用 useAsyncDatauseFetch 时,它们将共享相同的 dataerrorstatus refs。这确保了组件间的一致性,但要求某些选项保持一致。

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

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

如果您需要独立的实例,请使用不同的键:

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

响应式键

您可以使用计算 ref、普通 ref 或 getter 函数作为键,从而实现基于依赖变化自动更新的数据获取:

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

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

refresh 与 execute

如果您想手动获取或刷新数据,请使用 composables 提供的 executerefresh 函数。

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

<template>
  <div>
    <p>{{ data }}</p>
    <button @click="() => refresh()">
      Refresh data
    </button>
  </div>
</template>

execute 函数是 refresh 的别名,功能完全相同,但在 fetch 不是立即执行 的情况下,execute 更具语义性。

要全局重新获取或使缓存数据失效,请参见 clearNuxtDatarefreshNuxtData

清除(Clear)

如果您想清除所提供的数据(出于任何原因),而不需要知道传递给 clearNuxtData 的特定键,可以使用 composables 提供的 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,因为 URL 在函数被调用时就已构建。

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

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

如果您需要基于响应式值更改 URL,您可能想使用计算 URL

当提供响应式获取选项时,它们将被自动监视并触发重新获取。在某些情况下,您可能希望通过指定 watch: false 来选择退出此行为。

const id = ref(1)

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

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

计算 URL(Computed 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'">
      Type an user ID
    </div>

    <div v-else-if="pending">
      Loading ...
    </div>

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

如果您需要在其他响应式值更改时强制刷新,也可以监听其他值

非立即执行(Not immediate)

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">
      Get data
    </button>
  </div>

  <div v-else-if="status === 'pending'">
    Loading comments...
  </div>

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

更细粒度的控制中,status 变量可以是:

  • idle:尚未开始获取
  • pending:获取已开始但尚未完成
  • error:获取失败
  • success:获取成功完成

当我们在浏览器中调用 $fetch 时,用户的头信息如 cookie 会直接发送到 API。

通常,在服务端渲染期间,出于安全考虑,$fetch 不会包含用户浏览器的 cookies,也不会将 fetch 响应中的 cookie 转发回客户端。

但是,当在服务器上使用相对 URL 调用 useFetch 时,Nuxt 会使用 useRequestFetch 来代理头和 cookies(不包括不应转发的头,例如 host)。

如果您想将 cookie 从内部请求传回/代理到客户端,您需要自行处理。

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

export const fetchWithCookie = async (event: H3Event, url: string) => {
  /* Get the response from the server endpoint */
  const res = await $fetch.raw(url)
  /* Get the cookies from the response */
  const cookies = res.headers.getSetCookie()
  /* Attach each cookie to our incoming Request */
  for (const cookie of cookies) {
    appendResponseHeader(event, 'set-cookie', cookie)
  }
  /* Return the data of the response */
  return res._data
}
<script setup lang="ts">
// This composable will automatically pass cookies to the client
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>
在 Nuxt 中推荐使用 <script setup><script setup lang="ts"> 来声明 Vue 组件。
Read more in Docs > 4 X > API > Utils > Define Nuxt Component.

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

当使用 useAsyncDatauseLazyAsyncData 将在服务器上获取的数据传输到客户端(以及任何其他使用Nuxt payload 的功能)时,payload 会使用 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/app.vue
<script setup lang="ts">
// Type of `data` is inferred as string even though we returned a Date object
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/app.vue
<script setup lang="ts">
// Type of `data` is inferred as
// {
//   createdAt: {
//     year: number
//     month: number
//     day: number
//   }
// }
const { data } = await useFetch('/api/bar')
</script>

使用替代序列化器

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

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

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

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

    // Workaround the type conversion
    toJSON () {
      return this
    },
  }

  // Serialize the output to string, using superjson
  return superjson.stringify(data) as unknown as typeof data
})
app/app.vue
<script setup lang="ts">
import superjson from 'superjson'

// `date` is inferred as { createdAt: Date } and you can safely use the Date object methods
const { data } = await useFetch('/api/superjson', {
  transform: (value) => {
    return superjson.parse(value as unknown as string)
  },
})
</script>

参考用例(Recipes)

通过 POST 请求使用 SSE(Server-Sent Events)

如果您通过 GET 请求消费 SSE,可以使用 EventSource 或 VueUse 的 composable useEventSource

当通过 POST 请求消费 SSE 时,您需要手动处理连接。如下所示:

// Make a POST request to the SSE endpoint
const response = await $fetch<ReadableStream>('/chats/ask-ai', {
  method: 'POST',
  body: {
    query: 'Hello AI, how are you?',
  },
  responseType: 'stream',
})

// Create a new ReadableStream from the response with TextDecoderStream to get the data as text
const reader = response.pipeThrough(new TextDecoderStream()).getReader()

// Read the chunk of data as we get it
while (true) {
  const { value, done } = await reader.read()

  if (done) { break }

  console.log('Received:', 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])