数据获取
Nuxt 附带了两个组合式 API 和一个内置库来在浏览器或服务器环境中执行数据获取:useFetch
、useAsyncData
和 $fetch
。
简而言之:
$fetch
是最简单的网络请求方式。useFetch
是$fetch
的封装,只在 通用渲染 中获取数据一次。useAsyncData
类似于useFetch
,但提供了更细致的控制。
useFetch
和 useAsyncData
共享一组常见的选项和模式,我们将在最后的部分详细介绍。
useFetch
和 useAsyncData
的必要性
Nuxt 是一个可以在服务器和客户端环境中运行同构(或通用)代码的框架。如果在 Vue 组件的 setup 函数中使用 $fetch
函数 来执行数据获取,可能会导致数据被两次获取,一次在服务器(以渲染 HTML),另一次在客户端(当 HTML 被水合时)。这可能会导致水合问题,增加交互时间,并引发不可预测的行为。
useFetch
和 useAsyncData
组合式 API 解决了这个问题,确保如果在服务器上进行 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: {
// 我的表单数据
}
})
}
</script>
<template>
<div v-if="data == null">
没有数据
</div>
<div v-else>
<form @submit="handleFormSubmit">
<!-- 表单输入标签 -->
</form>
</div>
</template>
在上面的示例中,useFetch
将确保请求将在服务器上进行,并正确转发到浏览器。$fetch
没有这样的机制,更适合在请求仅在浏览器中发出时使用。
悬念
Nuxt 使用 Vue 的 <Suspense>
组件来防止在所有异步数据可用于视图之前进行导航。数据获取组合式 API 可以帮助你利用此功能,并根据每次调用使用最合适的方式。
<NuxtLoadingIndicator>
在页面导航之间添加进度条。$fetch
Nuxt 包含了 ofetch 库,并在你的应用程序中全局自动导入作为 $fetch
别名。
<script setup lang="ts">
async function addTodo() {
const todo = await $fetch('/api/todos', {
method: 'POST',
body: {
// 我的待办事项数据
}
})
}
</script>
将客户端头信息传递给API
在服务器端渲染期间,由于 $fetch
请求是在服务器内部进行的,因此它不会包含用户的浏览器 cookies。
我们可以使用 useRequestHeaders
从服务器端访问并代理 cookies 到 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
组合式 API 在底层使用 $fetch
进行 SSR 安全的网络调用。
<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>
<template>
<p>页面访问量: {{ count }}</p>
</template>
这个组合式 API 是 useAsyncData
组合式 API 和 $fetch
工具的封装。
useAsyncData
useAsyncData
组合式 API 负责封装异步逻辑,并在解决后返回结果。
useFetch(url)
几乎等同于 useAsyncData(url, () => event.$fetch(url))
。 这是针对最常见用例的开发者体验优化。(您可以在
useRequestFetch
中了解更多关于 event.fetch
的信息。)在某些情况下,使用 useFetch
组合式 API 不是合适的,例如,当 CMS 或第三方提供其自己的查询层时。在这种情况下,您可以使用 useAsyncData
来封装您的调用,并仍然保留组合式 API 提供的好处。
<script setup lang="ts">
const { data, error } = await useAsyncData('users', () => myGetFunction('users'))
// 这也是可以的:
const { data, error } = await useAsyncData(() => myGetFunction('users'))
</script>
useAsyncData
的第一个参数是一个唯一键,用于缓存第二个参数即查询函数的响应。这个键可以通过直接传递查询函数而忽略,键将自动生成。
由于自动生成的键仅考虑调用
useAsyncData
的文件和行,因此建议始终创建您自己的键,以避免不必要的行为,例如在您创建自己的自定义组合式 API 封装 useAsyncData
时。
设置键可以方便在使用
useNuxtData
时在组件之间共享相同的数据,或者用于 刷新特定数据。<script setup lang="ts">
const { id } = useRoute().params
const { data, error } = await useAsyncData(`user:${id}`, () => {
return myGetFunction('users', { id })
})
</script>
useAsyncData
组合式 API 是一个很好的方式来封装和等待多个 $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
是带有 .value
的 Vue refs,在 <script setup>
中可以访问。默认情况下,Nuxt 等待 refresh
完成后,才能再次执行。
server: false
),则数据 将不会 在水合完成之前被获取。这意味着即使您在客户端等待 useFetch
,data
在 <script setup>
中将保持为 null。选项
useAsyncData
和 useFetch
返回相同的对象类型,并作为最后一个参数接受一组共同的选项。它们可以帮助您控制组合式 API 的行为,例如导航阻止、缓存或执行。
懒加载
默认情况下,数据获取组合式 API 将在其异步函数解析之前,使用 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>
仅客户端获取
默认情况下,数据获取组合式 API 会在客户端和服务器环境中执行其异步函数。将 server
选项设置为 false
仅在客户端执行调用。在初始加载时,在水合完成之前不会获取数据,因此您需要处理待处理状态,但在随后的客户端导航中,将在加载页面之前等待数据。
与 lazy
选项结合使用时,这对在第一次渲染时不需要的数据(例如,非 SEO 敏感数据)很有用。
/* 这个调用在水合之前执行 */
const articles = await useFetch('/api/article')
/* 这个调用仅在客户端执行 */
const { status, data: comments } = useFetch('/api/comments', {
lazy: true,
server: false
})
useFetch
组合式 API 意在被调用在 setup 方法中或直接在生命周期钩子的函数顶部水平调用,否则您应使用 $fetch
方法。
最小化负载大小
pick
选项帮助您通过仅选择希望从组合式 API 返回的字段,来最小化存储在 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
如果第一个参数是字符串,则将其用作键。如果第一个参数是执行查询的处理函数,则将自动为您生成唯一于文件名和行号的键。
useNuxtData
。刷新和执行
如果您要手动获取或刷新数据,请使用组合式 API 提供的 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
的具体键,您可以使用组合式 API 提供的 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。例如,这将继续获取用户的初始 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 构造中,您也可以使用作为 计算 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
组合式 API 将在被调用的那一刻开始获取数据。您可以通过设置 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
时,Nuxt 将使用 useRequestFetch
来代理头部和 Cookies(不包括那些不应转发的头部,如 host
)。
将 cookies 从服务器端 API 调用传递到 SSR 响应
如果您希望将 cookies 传递或代理到客户端,您需要自己处理这一过程。
import { appendResponseHeader } from 'h3'
import type { H3Event } from 'h3'
export const fetchWithCookie = async (event: H3Event, url: string) => {
/* 获取来自服务器端点的响应 */
const res = await $fetch.raw(url)
/* 从响应中获取 cookie */
const cookies = res.headers.getSetCookie()
/* 将每个 cookie 附加到我们的请求中 */
for (const cookie of cookies) {
appendResponseHeader(event, 'set-cookie', cookie)
}
/* 返回响应的数据 */
return res._data
}
<script setup lang="ts">
// 此组合式 API 将自动将 cookies 传递给客户端
const event = useRequestEvent()
const { data: result } = await useAsyncData(() => fetchWithCookie(event!, '/api/with-cookie'))
onMounted(() => console.log(document.cookie))
</script>
选项 API 支持
Nuxt 提供了一种在选项 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">
来声明 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">
// `data` 的类型被推断为字符串,即使我们返回了 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
。但是,您可以将负载作为普通字符串返回,并使用 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(服务器发送事件)
EventSource
或 VueUse 组合式 API useEventSource
。通过 POST 请求消费 SSE 时,您需要手动处理连接。以下是如何操作的:
// 向 SSE 端点发出 POST 请求
const response = await $fetch<ReadableStream>('/chats/ask-ai', {
method: 'POST',
body: {
query: "你好 AI,你好吗?",
},
responseType: 'stream',
})
// 使用 TextDecoderStream 从响应创建新的 ReadableStream,获取数据为文本
const reader = response.pipeThrough(new TextDecoderStream()).getReader()
// 在我们获取数据时读取数据块
while (true) {
const { value, done } = await reader.read()
if (done)
break
console.log('收到:', value)
}