数据获取
Nuxt 带有两个 composables 和一个内置库,用于在浏览器或服务器环境中执行数据获取:useFetch
、useAsyncData
和 $fetch
。
概括来说
$fetch
是发出网络请求的最简单方法。useFetch
是$fetch
的包装器,它只在 通用渲染 中获取一次数据。useAsyncData
类似于useFetch
,但提供了更细粒度的控制。
useFetch
和 useAsyncData
都共享一组通用的选项和模式,我们将在最后一节中详细介绍。
使用 useFetch
和 useAsyncData
的必要性
Nuxt 是一个可以在服务器和客户端环境中运行同构(或通用)代码的框架。如果使用 $fetch 函数
在 Vue 组件的 setup 函数中执行数据获取,这可能会导致数据被获取两次,一次在服务器端(渲染 HTML),另一次在客户端(HTML 水合时)。这会导致水合问题,增加交互时间并导致不可预测的行为。
useFetch
和 useAsyncData
composables 通过确保如果在服务器上进行了 API 调用,则数据将转发到客户端的有效负载中来解决此问题。
有效负载是一个可以通过 useNuxtApp().payload
访问的 JavaScript 对象。它在客户端使用,以避免在浏览器中执行代码时 在水合期间 重新获取相同的数据。
<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>
组件,以防止在所有异步数据都可用于视图之前进行导航。数据获取 composables 可以帮助您利用此功能,并根据每次调用的情况使用最合适的功能。
<NuxtLoadingIndicator>
以在页面导航之间添加进度条。$fetch
Nuxt 包含 ofetch 库,并在整个应用程序中作为 $fetch
别名全局自动导入。
<script setup lang="ts">
async function addTodo() {
const todo = await $fetch('/api/todos', {
method: 'POST',
body: {
// My todo data
}
})
}
</script>
将客户端标头传递到 API
在服务器端渲染期间,由于 $fetch
请求在服务器“内部”发生,因此它不会包含用户的浏览器 Cookie。
我们可以使用 useRequestHeaders
来访问和代理服务器端的 Cookie 到 API。
下面的示例将请求标头添加到同构 $fetch
调用中,以确保 API 端点可以访问用户最初发送的相同 cookie
标头。
<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])
async function getCurrentUser() {
return await $fetch('/api/me', { headers: headers.value })
}
</script>
host
、accept
content-length
、content-md5
、content-type
x-forwarded-host
、x-forwarded-port
、x-forwarded-proto
cf-connecting-ip
、cf-ray
useRequestFetch
自动将标头代理到调用中。::useFetch
useFetch
composable 在后台使用 $fetch
在 setup 函数中进行 SSR 安全的网络调用。<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>
<template>
<p>Page visits: {{ count }}</p>
</template>
useAsyncData
composable 和 $fetch
实用程序的包装器。useAsyncData
useAsyncData
composable 负责包装异步逻辑并在解析后返回结果。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'))
// This is also possible:
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 () => {
const [coupons, offers] = await Promise.all([
$fetch('/cart/coupons'),
$fetch('/cart/offers')
])
return { coupons, offers }
})
// discounts.value.coupons
// discounts.value.offers
</script>
返回值
useFetch
和 useAsyncData
具有以下相同的返回值。data
:传递的异步函数的结果。refresh
/execute
:一个函数,可用于刷新handler
函数返回的数据。clear
:一个函数,可用于将data
设置为undefined
,将error
设置为null
,将status
设置为idle
,并将任何当前挂起的请求标记为已取消。error
:如果数据获取失败,则为错误对象。status
:一个字符串,指示数据请求的状态("idle"
、"pending"
、"success"
、"error"
)。
data
、error
和 status
是 Vue ref,可在 <script setup>
中使用 .value
访问。
refresh
完成后才能再次执行。server: false
),则数据将不会在 hydration 完成之前获取。这意味着即使您在客户端等待 useFetch
,data
在 <script setup>
中仍将保持为 null。选项
useAsyncData
和 useFetch
返回相同类型的对象,并将其最后一个参数作为一组通用的选项。它们可以帮助您控制组合式行为,例如导航阻止、缓存或执行。延迟加载
默认情况下,数据获取组合式将在导航到新页面之前等待其异步函数的解析,方法是使用 Vue 的 Suspense。此功能可以通过 lazy
选项在客户端导航中忽略。在这种情况下,您必须使用 status
值手动处理加载状态。<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>
useLazyFetch
和 useLazyAsyncData
作为方便的方法来执行相同的操作。<script setup lang="ts">
const { status, data: posts } = useLazyFetch('/api/posts')
</script>
仅客户端获取
默认情况下,数据获取组合式将在客户端和服务器环境中执行其异步函数。将 server
选项设置为 false
以仅在客户端执行调用。在初始加载时,数据在 hydration 完成之前不会被获取,因此您必须处理一个挂起状态,但在随后的客户端导航中,数据将在加载页面之前被等待。结合 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
方法。最小化负载大小
pick
选项可帮助您通过仅选择希望从组合式返回的字段来最小化存储在 HTML 文档中的负载大小。<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 }))
}
})
pick
和 transform
都不会阻止最初获取不需要的数据。但它们会阻止将不需要的数据添加到从服务器传输到客户端的负载中。缓存和重新获取
键
useFetch
和 useAsyncData
使用键来防止重新获取相同的数据。useFetch
使用提供的 URL 作为键。或者,可以在作为最后一个参数传递的options
对象中提供key
值。useAsyncData
如果其第一个参数是字符串,则使用它作为键。如果第一个参数是执行查询的处理程序函数,则会为您生成一个对useAsyncData
实例的文件名和行号唯一的键。
useNuxtData
刷新和执行
如果要手动获取或刷新数据,请使用组合式提供的 execute
或 refresh
函数。<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
的别名,工作方式完全相同,但在获取不立即执行时语义更清晰。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', {
/* Changing the id will trigger a refetch */
watch: [id]
})
</script>
<script setup lang="ts">
const id = ref(1)
const { data, error, refresh } = await useFetch(`/api/users/${id.value}`, {
watch: [id]
})
</script>
计算出的 URL
有时您可能需要根据响应式值计算 URL,并在每次更改时刷新数据。与其四处周旋,您可以将每个参数作为响应式值附加。Nuxt 将自动使用响应式值并在每次更改时重新获取。<script setup lang="ts">
const id = ref(null)
const { data, status } = useLazyFetch('/api/user', {
query: {
user_id: id
}
})
</script>
<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>
不立即
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">Get data</button>
</div>
<div v-else-if="status === 'pending'">
Loading comments...
</div>
<div v-else>
{{ data }}
</div>
</template>
status
变量可以是idle
当获取尚未开始时pending
当获取已开始但尚未完成时error
当获取失败时success
当获取成功完成时
传递 Headers 和 Cookies
当我们在浏览器中调用 $fetch
时,像 cookie
这样的用户 Headers 将直接发送到 API。通常,在服务器端渲染期间,由于 $fetch
请求在服务器内部“内部”发生,因此它不会包含用户的浏览器 Cookies,也不会传递获取响应中的 Cookies。但是,当在服务器上调用 useFetch
时,Nuxt 将使用 useRequestFetch
来代理 Headers 和 Cookies(除了不应转发 Headers,例如 host
)。在 SSR 响应中传递来自服务器端 API 调用的 Cookies
如果您想向另一个方向传递/代理 Cookies,即从内部请求传回客户端,则需要自己处理。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 组件的推荐方法。将数据从服务器序列化到客户端
当使用 useAsyncData
和 useLazyAsyncData
将在服务器上获取的数据传输到客户端(以及任何其他利用 Nuxt 负载 的内容)时,负载将使用 devalue
进行序列化。这使我们能够不仅传输基本 JSON,还可以序列化和恢复/反序列化更高级类型的数据,例如正则表达式、日期、Map 和 Set、ref
、reactive
、shallowRef
、shallowReactive
和 NuxtError
等。还可以为 Nuxt 不支持的类型定义您自己的序列化程序/反序列化程序。您可以在 useNuxtApp
文档中阅读更多信息。$fetch
或 useFetch
获取时从服务器路由传递的数据 - 请参阅下一节以获取更多信息。从 API 路由序列化数据
从 server
目录获取数据时,响应将使用 JSON.stringify
进行序列化。但是,由于序列化仅限于 JavaScript 基本类型,因此 Nuxt 会尽力将 $fetch
和 useFetch
的返回类型转换为匹配实际值。示例
export default defineEventHandler(() => {
return new Date()
})
<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 将尊重函数的返回类型,并且不会尝试转换类型。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">
// Type of `data` is inferred as
// {
// createdAt: {
// year: number
// month: number
// day: number
// }
// }
const { data } = await useFetch('/api/bar')
</script>
使用替代序列化程序
Nuxt 目前不支持 JSON.stringify
的替代序列化程序。但是,您可以将负载作为普通字符串返回,并利用 toJSON
方法来维护类型安全。在下面的示例中,我们使用 superjson 作为我们的序列化程序。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
})
<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>
食谱
通过 POST 请求使用 SSE(服务器发送事件)
EventSource
或 VueUse 组合式 useEventSource
。// 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)
}