通过 100+ 条技巧学习 Nuxt!

数据获取

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

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

简而言之

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

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

useFetchuseAsyncData 的需求

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

useFetchuseAsyncData 组合式函数通过确保 API 调用在服务器端进行时,数据会转发到客户端的 payload 中来解决此问题。

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

使用 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: {
      // My form data
    }
  })
}
</script>

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

在上面的示例中,useFetch 将确保请求在服务器端发生并正确转发到浏览器。$fetch 没有这种机制,更适合在仅从浏览器发出请求时使用。

Suspense

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
: {
// My todo data } }) } </script>
请注意,仅使用 $fetch 不会提供网络调用去重和导航阻止
建议将 $fetch 用于客户端交互(基于事件)或与 useAsyncData 结合使用,以获取初始组件数据。
阅读更多关于 $fetch 的信息。

将客户端 Headers 传递给 API

在服务器端调用 useFetch 时,Nuxt 将使用 useRequestFetch 代理客户端 headers 和 cookies(除了不打算转发的 headers,如 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 header。仅当你未使用 useFetch 时,这才是必要的。

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

async function getCurrentUser() {
  return await $fetch('/api/me', { headers })
}
</script>
你还可以使用 useRequestFetch 自动将 headers 代理到调用中。
在将 headers 代理到外部 API 之前,请务必小心,并且仅包含你需要的 headers。并非所有 headers 都可以安全绕过,并且可能会引入不必要的行为。以下是一些不应代理的常用 headers 列表
  • 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 安全的网络调用。

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

此组合式函数是 useAsyncData 组合式函数和 $fetch 工具的包装器。

观看 Alexander Lichter 的视频,避免错误地使用 useFetch
阅读更多信息,请参阅 文档 > API > 组合式函数 > Use Fetch
文档 > 示例 > 功能 > 数据获取 中阅读和编辑实时示例。

useAsyncData

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

useFetch(url) 几乎等同于 useAsyncData(url, () => event.$fetch(url))
它是最常见用例的开发者体验糖。(你可以在 useRequestFetch 中找到关于 event.fetch 的更多信息。)
观看 Alexander Lichter 的视频,深入了解 useFetchuseAsyncData 之间的区别。

在某些情况下,使用 useFetch 组合式函数是不合适的,例如当 CMS 或第三方提供他们自己的查询层时。在这种情况下,你可以使用 useAsyncData 包装你的调用,并仍然保留组合式函数提供的优势。

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

// This is also possible:
const { data, error } = await useAsyncData(() => myGetFunction('users'))
</script>
useAsyncData 的第一个参数是用于缓存第二个参数(查询函数)响应的唯一 key。可以通过直接传递查询函数来忽略此 key,key 将自动生成。

由于自动生成的 key 仅考虑调用 useAsyncData 的文件和行,因此建议始终创建自己的 key 以避免不必要的行为,例如当你创建自己的自定义组合式函数包装 useAsyncData 时。

设置 key 对于在使用 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 actions,因为这可能会导致意外行为,例如使用 nullish 值重复执行。如果你需要触发副作用,请使用 callOnce 工具来执行此操作。
<script setup lang="ts">
const offersStore = useOffersStore()

// you can't do this
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 返回相同的对象类型,并接受一组通用选项作为其最后一个参数。它们可以帮助你控制组合式函数的行为,例如导航阻止、缓存或执行。

Lazy

默认情况下,数据获取组合式函数将等待其异步函数解析后才导航到新页面,方法是使用 Vue 的 Suspense。可以使用 lazy 选项在客户端导航中忽略此功能。在这种情况下,你将必须使用 status 值手动处理加载状态。

app.vue
<script setup lang="ts">
const { 
status
,
data
:
posts
} =
useFetch
('/api/posts', {
lazy
: true
}) </script> <template> <!-- you will need to handle a loading state --> <
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 的信息。

仅客户端获取

默认情况下,数据获取组合式函数将在客户端和服务器环境中执行其异步函数。将 server 选项设置为 false 以仅在客户端执行调用。在初始加载时,数据将不会在水合完成之前获取,因此你必须处理待处理状态,尽管在随后的客户端导航中,数据将在加载页面之前等待。

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

/* This call is performed before hydration */
const 
articles
= await
useFetch
('/api/article')
/* This call will only be performed on the client */ const {
status
,
data
:
comments
} =
useFetch
('/api/comments', {
lazy
: true,
server
: false
})

useFetch 组合式函数旨在在 setup 方法中调用,或直接在生命周期函数中顶层调用,否则你应该使用 $fetch 方法

最小化 payload 大小

pick 选项通过仅选择你希望从组合式函数返回的字段,帮助你最小化存储在 HTML 文档中的 payload 大小。

<script setup lang="ts">
/* only pick the fields used in your template */
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 使用 keys 来防止重新获取相同的数据。

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

Refresh 和 execute

如果你想手动获取或刷新数据,请使用组合式函数提供的 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 不是立即执行的情况下更语义化。

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

Clear

如果你想清除提供的数据,无论出于何种原因,而无需知道要传递给 clearNuxtData 的特定 key,则可以使用组合式函数提供的 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', {
/* Changing the id will trigger a refetch */
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 获取数据。将其与not-immediate 结合使用,你可以在响应式元素更改之前等待获取。

<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>
    <!-- disable the input while fetching -->
    <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 组合式函数将在调用时立即开始获取数据。你可以通过设置 immediate: false 来阻止这种情况,例如,等待用户交互。

这样,你将需要 status 来处理 fetch 生命周期,以及 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 当 fetch 尚未开始时
  • pending 当 fetch 已开始但尚未完成时
  • error 当 fetch 失败时
  • success 当 fetch 成功完成时

传递 Headers 和 Cookies

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

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

但是,当在服务器上使用相对 URL 调用 useFetch 时,Nuxt 将使用 useRequestFetch 代理 headers 和 cookies(除了不打算转发的 headers,如 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) => {
  /* 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({
  /* Use the fetchKey option to provide a unique key */
  fetchKey: 'hello',
  async asyncData () {
    return {
      hello: await $fetch('/api/hello')
    }
  }
})
</script>
使用 <script setup><script setup lang="ts"> 是在 Nuxt 3 中声明 Vue 组件的推荐方式。
阅读更多信息,请参阅 文档 > API > 工具 > Define Nuxt Component

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

当使用 useAsyncDatauseLazyAsyncData 将在服务器上获取的数据传输到客户端时(以及任何其他使用 Nuxt payload 的内容),payload 使用 devalue 序列化。这使我们不仅可以传输基本的 JSON,还可以序列化和恢复/反序列化更高级的数据类型,例如正则表达式、Dates、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">
// 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.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.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 组合式函数 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)
}