首发于公众号 前端从进阶到入院,欢迎关注。
使用 Nuxt 3 的渲染模式,你可以在客户端和服务器端执行 API 调用和渲染页面,这带来了一些挑战。例如,我们要避免重复的网络调用,实现高效的缓存,并确保这些 API call 可以跨环境工作。为了解决这些挑战,Nuxt 提供了一个内置的数据获取库($fetch
)和两个 composable API(useFetch
和useAsyncData
)。
本文将详细解释 Nuxt 3 中可用的不同数据获取方法,以及使用时机。
数据获取库
Nuxt 内置了一个数据获取库:ofetch
ofetch
建立在 fetch API 的基础上,提供了一些方便的特性:
- 可在 node、浏览器和 workers 中使用
- 智能解析响应中的 JSON 和原生值
- 当
response.ok
为false
时自动抛出错误,并带有友好的错误信息和简洁的堆栈(隐藏内部详情) - 如果发生错误,会自动重试请求
- 你可以提供异步拦截器,在
ofetch
生命周期中搞事情。
你可以在整个应用中使用 $fetch
别名来使用 ofetch
:
const todos = await $fetch("/api/todos").catch((error) => error.data);
useFetch
useFetch
是在组件 setup 函数中处理数据获取最直接的方式。
<script setup>
const { data, error, pending, refresh } = await useFetch('/api/todos')
</script>
<template>
<div v-if="pending">加载中...</div>
<div v-else-if="data">todos: {{ data }}</div>
<div v-else-if="error">错误: {{ error }}</div>
<button @click="refresh">刷新</button>
</template>
useFetch
返回三个响应式变量和一个函数:
data
: 包含异步函数结果的响应式变量error
: 包含请求错误信息的响应式对象pending
: 表示请求是否在进行中的 Boolean 响应式变量refresh/execute
: 刷新由handler
函数返回的数据。默认情况下,Nuxt 会等待一个refresh
完成后才能再次执行。
如果 API 调用是在服务器端执行的,useFetch
composable 函数会将数据转发给客户端。这样,页面水合(hydrate)时客户端不需要重新获取在服务器端已经获取过的数据。你可以通过 useNuxtApp.payload()
检查这个数据; Nuxt 开发者工具 可以在 payload 标签页中可视化这些数据。
另外,useFetch
会使用键(key)来缓存 API 响应,以减少 API 调用。这个键会根据 URL 和 fetch 选项自动生成。useFetch
composable 函数可以在 setup
函数、生命周期钩子和插件或路由中间件中自动导入和使用。
你可以在 URL 字符串中使用 ref
的值来确保组件在响应式变量改变时更新:
const todoId = ref("uuid");
const {
data: tracks,
pending,
error,
} = useFetch(() => `/api/todos/${todoId.value}`);
如果 todoId
的值改变,URL 会相应更新并重新获取数据。
可以查看官方文档获取更多信息。
选项
useFetch
接受一组选项作为最后一个参数,可以用来控制 composable 函数的行为。
懒加载
当使用 Vue 的 Suspense 时,数据获取的 composable 函数会在导航到新页面前自动等待异步函数解析。Nuxt 在底层使用了 Vue 的 <Suspense>
组件来确保前往新页面前,所有数据都已经获取到。
然而,如果你想在客户端导航时绕过这个行为,可以使用 lazy
选项:
<script setup>
const { pending, data: todos } = useFetch("/api/todos", {
lazy: true,
});
</script>
<template>
<div v-if="pending">加载中...</div>
<div v-else>
<div v-for="todo in todos">{{ todo.name }}</div>
</div>
</template>
在这种情况下,你需要通过使用 pending
值手动处理加载状态。
另外,你可以选择使用 useLazyFetch
,这是一个实现相同结果的方便方法:
const { pending, data: todos } = useLazyFetch("/api/todos");
仅客户端
默认情况下,数据获取 composable 函数会在客户端和服务器环境中执行它们的异步函数。如果你想要仅在客户端执行,可以把 server
选项设置为 false
:
const { pending, data: posts } = useFetch("/api/comments", {
lazy: true,
server: false,
});
这在组合使用 lazy
选项时特别有用,用于初次渲染不需要的数据,比如对 SEO 不敏感的数据。
警告:如果你没有在服务器端获取数据,例如使用 server: false
,在水合过程完成前数据不会在客户端获取。
这意味着即使你在客户端 await useFetch
,在 <script setup>
中 data
变量仍会继续为 null。
最小化有效载荷大小
pick
选项可以通过选择想要从 composable 函数中返回的字段来最小化 HTML 文档中的有效载荷大小:
<script setup>
const { data: todos } = await useFetch('/api/todos', {
pick: ['id', 'name'],
})
</script>
<template>
<div v-for="todo in todos">
<span>{{ todo.name }}</span>
<span>{{ todo.id }}</span>
</div>
</template>
为了获得更多控制或遍历多个对象,你可以使用 transform
函数修改查询结果:
const { data: todos } = await useFetch("/api/todos", {
transform: (todos) => {
return todos.map((todo) => ({ name: todo.title, id: todo.description }));
},
});
重新获取
要手动获取或更新数据,可以使用 composable 函数提供的 execute
或 refresh
函数:
<script setup>
const { data, error, execute, refresh } = await useFetch('/api/todos')
</script>
<template>
<div>
<p>{{ data }}</p>
<button @click="refresh">刷新数据</button>
</div>
</template>
这两个函数的目的相同,但 execute
是 refresh
的别名,在使用 immediate: false
时更合适。当 immediate
选项设置为 false
(默认为 true
)时,它会阻止请求立即触发。
使用 watch
选项,可以在应用中的其他响应式值发生改变时重新运行获取函数:
const count = ref(1);
const { data, error, refresh } = await useFetch("/api/todos", {
watch: [count],
});
何时使用 refresh 与 watch 选项?
当你知道服务器端的数据已经修改,需要相应更新客户端的数据时,使用 refresh()
。
当用户修改了需要发送到服务器的参数时,将这些参数设置为 watch 源。比如,如果你想要使用 search 参数过滤 API 结果,那就 watch 这个参数。这可以确保无论用户怎样改变 search 参数,都会从 API 重新加载最新和准确的数据。
查询搜索参数
使用 query
选项,可以在查询中包含搜索参数:
const queryValue = ref("anyValue");
const { data, pending, error, refresh } = await useFetch("/api/todos", {
query: { queryKey: queryValue, anotherQueryKey: "anotherQueryValue" },
});
这个选项是 ofetch 的扩展,利用 ufo 生成 URL。提供的对象会自动转换为字符串格式。
拦截器
你可以定义异步拦截器,钩入 API 调用的生命周期事件:
const { data, pending, error, refresh } = await useFetch("/api/todo", {
onRequest({ request, options }) {},
onRequestError({ request, options, error }) {},
onResponse({ request, response, options }) {},
onResponseError({ request, response, options }) {},
});
这些选项由内置的 ofetch 库提供。
useAsyncData
useFetch
用于从给定 URL 获取数据,而 useAsyncData
允许更复杂的逻辑。基本上,useFetch(url)
相当于 useAsyncData(url, () => $fetch(url))
,为最常见的用例提供了更流畅的开发体验。
然而,在某些情况下使用 useFetch
composable 函数可能不太合适,比如 CMS 或第三方服务提供自己的查询层。在这种情况下,你可以利用 useAsyncData
封装你的调用,同时享受 composable 函数提供的好处:
const { data, error } = await useAsyncData("getTodos", () => fetchTodos());
在 useAsyncData
中,第一个参数充当第二个参数(查询函数)获得的响应的唯一缓存 key。不过也可以省略这个参数,直接传入查询函数本身。在这种情况下,唯一 key 会被自动生成。
提示: useAsyncData
和 useFetch
提供相同的返回值对象类型,并接受共同的一组选项作为最后一个参数。这些选项允许你自定义 composable 函数的行为,包括导航阻塞、缓存和执行控制等特性。
你可以查看官方文档获取更多信息。
总结
让我们总结一下应该在哪些情况下使用哪种数据获取方法:
-
根据用户交互发起网络请求时,
$fetch
函数是一个合适的选择。当发送数据到事件处理程序、仅执行客户端逻辑或与useAsyncData
组合使用时,建议使用$fetch
。 -
在组件的 setup 函数中处理数据获取,
useFetch
composable 函数是最简单的方法。 -
如果需要对数据获取过程进行更精确的控制,可以选择结合使用
useAsyncData
和$fetch
。
首发于公众号 前端从进阶到入院,分享 Vue 源码 / React / TS / 浏览器 / 工程化等各个前端领域,我的文章帮助了很多小伙伴进入大厂,也阔以加我的好友,交个朋友。