数据获取
Nuxt提供了组合式API去处理应用中的数据获取。
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对象。它在客户端欧根于避免在激活期间在浏览器中重新获取相同的数据。
ts
// app.vue
<script setup lang="ts">
const {} = 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>组件,以防止在所有异步数据可用于视图之前进行导航。本质是为了防止页面过早跳转,要等数据加载完再允许用户进入页面。目的是避免用户过早进入页面,要等数据全准备好才行。
可添加
<NuxtLoadingIndicator>在页面导航之间添加进度条。
$fetch
Nuxt包含ofetch库,并在整个应用中自动导入为全局的$fetch别名。
html
// pages/todos.vue
<script setup lang="ts">
async function addTodo() {
const todo = await $fetch('/api/todos', {
method: 'POST',
body: {
// 我的待办数据
}
})
}
</script>
仅使用
$fetch不会提供网络请求去重和导航阻止。建议在客户端交互(基于事件)时使用$fetch,或者在获取初始组件数据与useAsyncData结合使用。
将客户端标头传递到API
当在服务器上调用useFetch时,Nuxt将使用useRequestFetch来代理客户端标头和Cookie(除了不打算转发的标头,如host)。
ts
<script setup lang="ts">
const { data } = await useFetch(`/api/echo`);
</script>
ts
// /api/echo.ts
export default defineEventHandler(event => parseCookies(event))
或者,下面的示例展示了如何使用useRequestHeaders从服务器端请求(源自客户端)访问Cookie并将其发送到API。使用同构的$fetch调用,确保API端点可以访问用户浏览器最初发送的相同的cookie标头。这仅在不使用useFetch时才需要。
ts
<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组合式API在底层用$fetch,用于在setup函数中发起SSR安全地网络请求。
html
// app.vue
<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))。
在某些情况下,使用useFetch组合式API并不合适,例如当CMS或第三方提供自己的查询层时。在这种情况下,可以使用useAsyncData去包装调用。同时仍然保留该组合式API提供的优势。
html
// pages/users.vue
<script setup lang="ts">
const { data, error } = await useAsyncData('users', () => myGetFunction('user'))
// 也是可能的:
const { data, error } = await userAsyncData(() => myGetFunction('users'))
</script>
html
useAsyncData的第一个参数是一个唯一键,用于缓存第二个参数(查询参数)的响应。如果直接传递查询函数,这个键可以忽略,它将自动生成。
由于自动生成的键仅考虑调用`useAsyncData`的文件和行,因此建议始终创建自己的键以避免不必要的行为,例如当你创建自己的自定义组合式API来包装`useAsyncData`时。
设置键有助于通过`useNuxtData`在组件之间共享相同的数据,或者刷新特定数据。
html
// 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组合式API是包装并等待多个$fetch请求完成,然后处理结果的好方法。
ts
<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),因为这可能导致意外行为,例如使用空值重复执行。
html
<script setup lang="ts">
const offersStore = useOffersStore()
// 不能这样写
await useAsyncData(() => offtersStore.getOffer(route.params.slug))
</script>
返回值
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的ref,在<script setup>中通过.value访问。
默认情况下,Nuxt会等待refresh完成后才允许再次执行。
选项
useAsyncData和useFetch返回相同的对象类型,并接受一组共同的选项作为最后一个参数。它们可以帮助你控制组合式API的行为,如导航阻塞、缓存或者执行。
延迟加载(Lazy)
默认情况下,数据获取组合式API会通过使用Vue的Suspense等待其异步函数解析后再导航到新页面。可以使用lazy选项在客户端导航时忽略此功能。在这种情况下,将不得不使用status值手动处理加载状态。
html
// app.vue
<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作为便捷方法来执行相同的操作。
ts
<script setup lang="ts">
const { status, data: posts } = useLazyFetch('/api/posts')
</script>
仅客户端获取
默认情况下,数据获取组合式API将在客户端和服务器环境中执行其异步函数。将server选项设置为false以仅在客户端执行调用。在初始加载时,数据将不会在激活前获取。这意味着即使你在客户端等待useFetch,在<script setup>中data仍将保持为null。结合lazy选项,这对于不需要在首次渲染时获取的数据非常有用。
ts
/* 此调用在激活前执行 */
const articles = await useFetch('/api/article')
/* 此调用仅在客户端执行 */
const { status, data: comments } = useFetch('/api/comments', {
lazy: true,
server: false
})
useFetch组合式API旨在在setup方法中调用或直接在生命周期钩子的函数顶层调用,否则应该用$fetch方法。
最小化有效载荷大小
pick选项帮助通过只选择需要从组合式API返回的字段来最小化存储在HTML文档中的有效载荷大小。
ts
<script setup lang="ts">
/* 只选择模版中使用的字段 */
const {} = await useFetch('/api/mountains/everest', {
pick: ['title', 'description']
})
</script>
<template>
<h1>{{ mountain.title }}</h1>
<p>{{ mountain.description }}</p>
</template>
如果需要更多控制或映射多个对象,可以用
transform函数去更改查询结果。
ts
const { data: mountains } = await useFetch('/api/mountains', {
transform: () => {
return mountains.map(mountain => ({ title: mountain.title, description: mountain.description }))
}
})
pick和transform都不能阻止最初获取不需要的数据。但它们会阻止不需要的数据被添加到从服务器传输到客户端的有效载荷中。
缓存和重新获取
键(Keys)
useFetch和useAsyncData使用键来防止重新获取相同的数据。
useFetch使用提供的URL作为键。或者,可以在座位最后一个参数传递的options对象中提供key值。useAsyncData如果第一个参数是字符串,则使用它作为键。如果第一个参数是执行查询的处理函数,则会为你生成一个对于useAsyncData实例的文件名和行号唯一的键。
要通过键获取缓存的数据,可以用
useNuxtData
共享状态和选项一致性
当多个组件用相同的键调用useAsyncData或useFetch时,它们将共享相同的data、error和statusref。这确保了组件之间的一致性,但需要某些选项保持一致。
handler函数deep选项transform函数pick数组getCachedData函数default值
ts
// ❌ 这将触发开发警告
const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { deep: false })
const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { deep: true })
以下选项可以安全地不同而不会触发警告:
serverlazyimmediatededupewatch
ts
// 这是可以的
const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { immediate: true })
const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { immediate: false })
如果你需要独立的实例,请使用不同的键:
ts
// 完全独立的实例
const { data: users1 } = useAsyncData('users-1', () => $fetch('/api/users'))
const { data: users2 } = useAsyncData('users-2', () => $fetch('/api/users'))
响应式键(Reactive Keys)
可以用计算属性ref、普通ref或getter函数作为键,允许动态数据获取,当依赖项更改时自动更新:
ts
// 使用计算属性作为键
const userId = ref('123')
const { data: user } = useAsyncData(
computed(() => `user-${userId.value}`),
() => fetchUser(userId.value)
)
// 当 userId 更改时,数据将自动重新获取
// 并且如果没有其他组件使用旧数据,旧数据将被清理
userId.value = '456'
刷新和执行
要手动获取或刷新数据,用组合式api提供的execute或refresh函数。
ts
<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的特定键,可以用组合式API提供的clear函数。
ts
<script setup lang="ts">
const { data, clear } = await useFetch('/api/users')
const route = useRoute()
watch(() => route.path, (path) => {
if (path === '/') clear()
})
</script>
监听
要在应用程序中的其他响应式更改时重新运行你的获取函数,用watch,监听一个或者多个。
ts
<script setup lang="ts">
const id = ref(1)
const { data, error, refresh } = await useFetch('/api/users', {
/* 更改id将触发重新获取 */
watch: [id]
})
</script>
监听响应式不会改获取的URL。将继续用相同的初始用户id,因为url是在函数调用时构造的。
ts
<script setup lang="ts">
const id = ref(1)
const { data, error, refresh } = await useFetch(`/api/users/${id.value}`, {
watch: [id]
})
</script>
可以用计算URL
计算URL
需要URL响应式值计算,并在这些值更改时刷新数据。只需要将每个参数作为响应式值附加。Nuxt将自动用响应式值,并在每次更改时重新获取。
ts
<script setup lang="ts">
const id = ref(null)
const { data, status } = useLazyFetch('/api/user', {
query: {
user_id: id
}
})
</script>
在更复杂的URL构造情况下,可以用作为计算getter的回到函数,返回URL字符串。
每次依赖项更改时,将用新构造的URL获取数据。
结合非立即执行,可以等待响应式元素更改后再获取。
ts
<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>
<!-- fetching 时禁用输入 -->
<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>
在其他响应值更改时强制刷新,可以监听其他值。
非立即执行(Not immediate)
useFetch组合式API在调用时立即开始获取数据。可以通过设置immediate:false去阻止这种情况,例如,等待用户交互。这样,将需要status去处理获取生命周期,以及execute去启动数据获取。
ts
<script setup lang="ts">
const { data, error, execute, status } = await useLazyFetch('', {
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:当获取成功完成时
传递标头和Cookie
当我们在浏览器中调用$fetch时,用户标头(如 cookie)将直接发送到API。通常,在服务器端渲染期间,出于安全考虑,$fetch不会包含用户的浏览器Cookie,也不会传递来自fetch响应的Cookie。然而,当在服务器上调用useFetch并使用相对URL时,Nuxt将使用useRequestFetch去代理标头和Cookie。
在SSR响应中从服务端API调用传递Cookie
要在另一个方向传递/代理Cookie,从内部请求回到客户端,要自己处理。
ts
// composables/fetch.ts
import { appendResponseHeader } from 'h3'
import type { H3Event } from 'h3'
export const fetchWithCookie = async (event: H3Event, url: string) => {
const res = await $fetch.raw(url)
const cookies = res.headers.getSetCookie()
for (const cookie of cookies) {
appendResponseHeader(event, 'set-cookie', cookie)
}
/* 返回响应的数据 */
return res._data
}
ts
<script setup lang="ts">
// 这个组合式API将自动将Cookie传递给客户端
const event = useRequestEvent()
const {} = await useAsyncData(() => fetchWithCookie(event!, '/api/with-cookie'))
onMounted(() => console.log(document.cookie))
</script>
选项API支持
Nuxt提供了在选项API中执行asyncData获取的方法。为此,你必须将组件定义包装在defineNuxtComponent中。
ts
<script>
export default defineNuxtComponent({
fetchKey: 'hello',
async asyncData () {
return {
hello: await $fetch('/api/hello')
}
}
})
</script>
用
<script setup>或<script setup lang="ts">是在Nuxt中声明Vue组件的推荐方式。
从服务器到客户端的数据序列化
当使用useAsyncData和useLazyAsyncData将在服务器上获取的数据传输到客户端时(以及任何其他利用Nuxt有效载荷的内容),有效载荷用devalue进行序列化。
这允许我们不仅传输基本JSON,还可以序列化和恢复/反序列更高级的数据类型,如正则、日期、Map和Set、ref、reactive、shallowRef、shallowReactive。
从API路由序列化数据
ts
// server/api/foo.ts
export default defineEventHandler(() => {
return new Date()
})
ts
// app.vue
<script setup lang="ts">
// `data`的类型被推断为 string,尽管我们返回了一个Date对象
const { data } = await useFetch('/api/foo')
</script>
自定义序列化函数
ts
// server/api/bar.ts
export default defineEventHandler(() => {
const data = {
createAt: new Date(),
toJSON() {
return {
createdAt: {
year: this.createdAt.getFullYear(),
month: this.createAt.getMonth(),
day: this.createdAt.getDate(),
}
}
}
}
return data
})
ts
// app.vue
<script setup lang="ts">
/**
`data`的类型被推断为
{
createdAt: {
year: number
month: number
day: number
}
}
*/
const { data } = await useFetch('/api/bar')
</script>
用替代序列化器
用toJSON方法去保持类型安全。
ts
// server/api/superjson.ts
export default defineEventHandler(() => {
const data = {
createAt: new Date(),
// 解决类型转换问题
toJSON() {
return this
}
}
//
return superjson.stringify(data) as unknown as typeof data
})
ts
// app.vue
<script setup lang="ts">
// `date`被推断为 { createAt: 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组合式API
useEventSource
ts
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)
}
并行请求
不互相依赖的接口可用这个。提高性能。
ts
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]);