Nuxt的数据获取是比较重要的一环。因为会影响页面的水合过程。好的设计,会使得页面加载变快。
对比于传统的vue项目多使用axios进行二次封装不同。Nuxt天然提供了几种数据获取的方式useFetch,useAsyncData和$fetch
简单点说:
useFetch: 是获取数据最简单的方法,它可以在客户端和服务端执行,并提供缓存功能$fetch: 地位相当于axios,useFetch是对$fetch的封装useAsyncData: 它相当于一个壳,可以接受一个异步参数,使得这个异步函数也能像useFetch一样使用。但是在Nuxt当中,这个异步函数一般是$fetch
在此之前,有必要了解为什么会有这些函数
为什么需要使用特定的composables来获取数据?
为什么会存在useFetch和useAsyncData这两个composables?
答案是避免重复请求数据
什么叫避免请求重复数据?首先我们需要了解Nuxt框架的本身,它是一个支持SSR渲染的框架。
服务端渲染的好处是,服务器先提前获取数据,渲染出来完整的单个页面的html。这样客户就能立刻查看到数据,速度会快很多。
但是,依赖于Vue框架的其它特性,例如单页面的切换、很多的动态效果仍然需要js动态执行
于是当客户端收到完整的html之后,还会在后台,下载执行js。然后重新在客户端渲染,使得原本的静态html也拥有了动态的能力!
客户端重新渲染,并且把静态HTML恢复成动态HTML的过程,就叫做水合(Hydration)
看见没有,上面这段话,说到了两次渲染,一次是服务端,一次是客户端。这就意味着,页面需要获取两次数据。但是这两次数据,实际上间隔时间这么短,并且请求参数都是一样的,完全是重复的,没有必要的。请求两次也会对后端造成压力。
那么useFetch和useAsyncData的作用就体现出来了。在服务器渲染的时候,useFetch会请求后端,获取数据,并且把数据缓存起来,在页面水合的时候,把缓存的数据传递到客户端,代替掉useFetch的一次请求!
网络请求的复制
上面我们说了,useFetch和useAsyncData一旦在服务端被调用,数据就会以载荷(payload)的形式转发到客户端
payload是一个JavaScript对象,可以通过useNuxtApp().playload进行访问。它用于客户端中,避免在水合期间重复请求相同的数据
Suspense
Nuxt 在幕后使用 Vue 的 <Suspense> 组件来防止在所有异步数据可用之前进行导航。useFetch和useAsyncData 可以帮助您利用这一特性,并根据每次调用的具体需求选择最适合的方式。
您可以在应用中添加
<NuxtLoadingIndicator>来在页面导航之间显示进度条。
useFetch
这是最简单的最直接的数据获取方式
<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>
<template>
<p>Page visits: {{ count }}</p>
</template>
useFetch('/api/count')
相当于
useAsyncData('key',()=>$fetch('/api/count'))
$fetch
$fetch 实际上是nuxt整合了 ofetch这个库,重命名为$fetch并全局自动导入。useFetch的底层就是基于$fetch
ofetch相当于axios,只不过是不同的实现方式
<script setup lang="ts">
async function addTodo() {
const todo = await $fetch('/api/todos', {
method: 'POST',
body: {
// My todo data
}
})
}
</script>
useAsyncData
useAsyncData的作用是封装异步逻辑并且在异步逻辑结束后返回结果。
useFetch(url)等价于useAsyncData(url,()=>$fetch(url))。useFetch只是个语法糖
有些情况,useFetch并不合适使用。例如需要在发送前需要鉴权、或者第三方提供了查询层的时候。
这种情况下我们就需要用到useAsyncData
下面这种写法,是等价的
<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),用于缓存第二个参数(查询函数)的响应结果。第一个参数也可以被忽略,通过直接传递查询函数,键将会自动生成。由于自动生成的键只考虑
useAsyncData被调用的文件和行位置。因此建议自己管理这个键,避免自动生成的键导致的不合预期的行为。同时,设置一个键对于正在使用useNuxtData的组件之间共享相同的数据或刷新特定数据是非常有用的。
自己管理键的例子
<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>
返回值
useFetch和useAsyncData都有相同的返回值
data: 返回结果refresh/execute: 执行函数,不同的名称clear: 清理方法,会将data设置为undefined,error设置为null,status设置为idle,并且把所有当前正在准备的请求,全部取消error: 当执行错误的时候,返回的一个错误的对象status: 状态"idle","pending","success","error"
data,error和status诗vue的ref对象,在<script setup>中使用需要带上.value
设置
延迟Lazy
默认情况下,Nuxt会利用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>
你可以在参数设置,但是Nuxt也提供了另外一种方式,useLazyFetch和useLazyAsyncData可以实现同样的效果
<script setup lang="ts">
const { status, data: posts } = useLazyFetch('/api/posts')
</script>
仅客户端运行
默认情况下,数据获取的组合式函数会在客户端和服务器环境中执行其异步函数。如果需要仅在客户端中执行,则将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组合式函数(实际上useAsyncData也一样)旨在setup方法中调用,或者直接在生命周期钩子函数的顶层调用,否则你应该使用$fetch方法
官网的这句话,在一个不起眼的角落里。但是实际上,它是一个很重要的特性。
什么叫做在setup方法中调用?下面举个例子
<script setup>
//这一行是正确的
const {data} = await useAsyncData(url,()=>$fetch(url)) // 实际等价于const {data} = useFetch(url)
const data2 = ref()
// 错误写法-1
const handleClick = async () => {
// 这里不是setup的顶层,所以会提示一个warning
const {data} = await useAsyncData(url,()=>$fetch(url))
data2.value = data.value
}
// 错误写法-2
// 这里试图先声明,再在函数内调用执行方法
const {data: tempData,execute} = await useAsyncData(url,()=>$fetch(url,{immediate:false}))
const handleClick = async () => {
// 同样的,这里不是setup的顶层,虽然声明在顶层,但是实际上调用是非顶层
execute()
data2.value = tempData.value
}
</script>
<template>
<div>{{data}}</div>
<button @click="handleClick">click me</button>
<div>{{data2}}
</template>
所以,如果是像@click方法内需要获取数据,请把$fetch当作axios一样使用
最小载荷 Minimize payload size
文章上面我们提到过,服务器中执行fetch之后,会把数据放到payload中,传递给客户端进行水合,避免重复获取数据。
这个过程,也是耗用网络资源的,如果我们需要更快的访问,我们可以把需要的部门传递就可以了
使用pick选项
<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都不会改变服务端初次从后端获取的字段,但是可以防止多余的字段传递到客户端
缓存和重新获取
keys
useFetch和useAsyncData都使用了键(keys)来防止重复获取相同的数据
useFetch使用提供的URL作为键。另外,也可以手动在option中提供一个键useAsyncData的第一个参数是字符串的话,那么这个字符串就是键。忽略这个第一个参数,直接传进异步函数,那么Nuxt将为这个请求生成一个由文件名(你调用所在的文件名)和行号(在哪里调用的行号)组成的key
刷新和执行
如果你想手动刷新数据,你可以执行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>
清理
如果你想清理已经获取到的数据,你不需要往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 监控
要在应用程序中的其他响应式值发生变化时重新运行获取函数,请使用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>
特别注意
watch的对象更改,是不会影响到获取数据的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(Computed URL)
Computed URL
如果url传递一个id参数,你可以使用query选项
<script setup lang="ts">
const id = ref(null)
const { data, status } = useLazyFetch('/api/user', {
query: {
user_id: id
}
})
//这里最终会请求'/api/user?user_id=id'
</script>
但是我们还是没有解决上面那种restful风格传递参数的例子
这个时候我们可以传入一个计算URL的回调函数,结合immediate: false,我们可以等到响应值变化完之后再执行函数
<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管理获取数据的生命周期(没有获取完成之前,data是null的,这会导致页面渲染报错。所以你要通过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的,也不会传递fetch响应中的cookies的。
这个时候,需要我们用点特殊技巧实现了
传递客户端header到api
我们可以使用useRequestHeaders从服务器去访问和代理cookies到api
<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])
const { data } = await useFetch('/api/me', { headers })
</script>
将headers代理给外部api是一个危险操作,并不是所有的header都可以安全绕过的。下面是不可以被代理的header
host,acceptcontent-length,content-md5,content-typex-forwarded-host,x-forwarded-port,x-forwarded-protocf-connecting-ip,cf-ray
SSR渲染时,从服务端的api调用传递cookies到客户端
如果你想将 cookies 从内部请求反向传递/代理回客户端,你需要自行处理这一点。
import { appendResponseHeader } from 'h3'
import type { H3Event } from 'h3'
export const fetchWithCookie = async (event: H3Event, url: string) => {
/* 从后端获取response */
const res = await $fetch.raw(url)
/* 从response获取cookies */
const cookies = res.headers.getSetCookie()
/* 将每个cookie附加到传入的请求 */
for (const cookie of cookies) {
appendResponseHeader(event, 'set-cookie', cookie)
}
/* 最终返回数据 */
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>
对选项式api(Options API)的支持
回顾一下vue3两种风格的写法
- 组合式 Composition API
- 选项式 Options API
组合式就是
<script setup>
// code. ...
</script>
或者是
<script>
export default { setup() {
const count = ref(0)
return {count}
}}
</script>
而选项式就像是vue2比较传统的方式,也就是返回一个默认的、特定格式的对象
<script>
export default{
data:{},
methods:{},
mounted(){
}
}
</script>
Nuxt3中,推荐使用组合式api,并且是配合<script setup>使用
但是我们同样可以使用选项式API来定义异步函数
<script lang="ts">
export default defineNuxtComponent({
async asyncData() {
// 模拟异步数据获取
const data = await fetch('https://api.example.com/data')
const json = await data.json()
return {
data: json
}
}
})
</script>
<template>
<div>
<h1>{{ data.title }}</h1>
<p>{{ data.description }}</p>
</div>
</template>
从服务器序列化数据到客户端
载荷(playload)是使用了devalue序列化的。也可以使用自定义的序列化器/反序列化器。详情请看useNextApp的文档
从API路由序列化数据
从服务器目录获取数据时,response使用 JSON.stringify 进行序列化。然而,由于序列化仅限于 JavaScript 的原始类型,Nuxt 尽力将 $fetch 和 useFetch 的返回类型转换以匹配实际值。
server/api/foo.ts
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会遵守这个函数的返回类型,不会尝试转换这些类型
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
})
<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方法来维护类型安全。
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(服务器推送事件)
SSE(Server-Sent Events)是一种让服务器向浏览器发送实时更新的技术。它允许服务器在有新数据可用时主动推送给客户端,而不是客户端定期轮询服务器来检查是否有新数据。SSE 主要用于实现实时数据流,如股票价格更新、聊天消息、实时日志等场景。
当通过 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)
}
这段代码的工作流程如下:
- 发送一个 POST 请求到指定的端点,请求体包含了要询问AI的问题。
- 接收来自服务器的数据流,将数据流通过
TextDecoderStream解码为文本格式。 - 循环读取流中的数据,每次接收到新的数据块就打印出来。
这个代码非常适合像是chatGPT聊天这种形式
重复请求的情况探讨
重复请求的情况探讨(可以最后再看)
据我实测,单纯的使用
$fetch,不使用这两个composables,有的时候并不会导致重复请求。首先说明一下直接调用
$fetch会重复请求的情况
- 第一次打开页面,
$fetch直接在<script setup>中调用对,除此之外我就没有发现了。你可能会很好奇。不是说很容易导致重复请求吗? 这个跟Nuxt的通用渲染模式有关系。在不做多余的配置情况下,只有第一次打开网页的时候,Nuxt才会进行SSR。
进行SSR渲染时候获取一次数据,然后在水合(Hydration)的时候也会获取一次数据。
然后水合之后的网站,就跟普通的传统Vue的项目差不多了!(并不是,特殊情况下吧)
我渲染a页面的时候,a中的函数确实请求了两次。但是我点击一个按钮(利用click事件跳转到其它页面),跳到b页面的时候,b页面的函数并没有获取两次,而是一次。这是因为a页面水合之后,我点击跳到b页面的过程,是水合后,在浏览器执行的js进行渲染的。也就是说这只是单纯的一次客户端渲染。
并且,当我把a页面中的获取数据的函数,放在了onMounted中执行,页面也只获取了一次数据。我查阅了Nuxt的文档,并没有发现其有特殊的说明。但是最终在vue3的官方文档中,我找到了说明。SSR的过程中,onMounted和onUpdated都不会被执行
还有个特殊情况,上面的页面跳转,我特意标红了是点击按钮进行跳转的。在后面的渲染模式文章中,我们还知道,在SSR中,Nuxt会将当前的页面中的所有a标签,都会爬取处理,并对他们同样的进行SSR。
所以,上面页面跳转的时候,如果我是通过a标签进行的话,b页面的数据同样也会被请求两次
综上所述,我再整理归纳不用
useFetch和useAsyncData的时候,什么情况下数据会被请求两次
- 第一次打开页面,
$fetch直接在<script setup>中调用- 该页面被其他页面所链接,导致出现第一种情况
什么情况下不会请求两次
- 通过按钮事件跳转到的页面
- 在onMounted或者onUpdated钩子函数中
- 点击事件中