Nuxt 数据获取指南:useLazyFetch 非阻塞导航请求完全解析
【免费下载链接】nuxtthe full-stack Vue framework项目地址: https://gitcode.com/GitHub_Trending/nu/nuxt
useLazyFetch是 Nuxt 官方基于useFetch封装的"懒加载"数据获取 composable,它通过内置开启lazy: true选项,让客户端导航在异步请求完成前就立即执行,数据在后台继续拉取。本文将从源码实现、API 签名、参数与返回值、加载/错误状态处理、以及真实测试用例五个维度,完整讲解在 Nuxt 应用中如何用useLazyFetch构建不阻塞路由切换的 SSR 友好数据流,同时剖析它与await、useFetch、useAsyncData之间的边界关系,帮助你在正确的场景下做出正确的选择。
它解决了什么问题:阻塞式导航与懒加载
在 Nuxt 中,默认的useFetch会通过 Vue 的<Suspense>机制阻塞客户端导航,直到其异步 handler 全部 resolve 之后,用户才会真正进入新页面——用户停留在旧页面(期间可配合进度条),然后直接落在"数据已就绪"的新页面上。这在用户体验上表现为"等待后再进入"。
而useLazyFetch允许导航立即继续,数据在后台获取,页面先渲染出加载态(loading state),数据到达后再更新 UI。
useLazyFetch本质就是useFetch设置了lazy: true。官方文档对此的表述是:useLazyFetch通过在内部将lazy选项置为true,提供了对useFetch的封装,使其在 handler 被 resolve 之前就触发导航。
在源码层面,这一点可以得到精确验证。Nuxt 使用createUseFetch工厂函数生成了useFetch与useLazyFetch两个实例,两者共享同一套实现逻辑,唯一的差别是工厂默认选项不同。见 fetch.ts 工厂与实例定义:
export const useFetch: UseFetch = (createUseFetch as unknown as { __nuxt_factory: typeof createUseFetch }).__nuxt_factory() export const useLazyFetch: UseFetch = (createUseFetch as unknown as { __nuxt_factory: typeof createUseFetch }).__nuxt_factory({ lazy: true, // @ts-expect-error private property _functionName: 'useLazyFetch', }) as ReturnType<typeof createUseFetch>可以看到,useLazyFetch就是向createUseFetch工厂传入了{ lazy: true },同时注入内部属性_functionName: 'useLazyFetch'(该属性仅用于开发模式下报错信息与诊断提示,便于 Nuxt 识别调用来源)。
需要特别指出的是,懒加载在服务器端渲染(SSR)期间依然会阻塞渲染。lazy只影响客户端导航行为:在首屏 SSR 阶段,无论是否lazy,Nuxt 都会在序列化页面 HTML 之前等待请求完成(底层通过<Suspense>与onServerPrefetch实现),因此返回给浏览器的首屏 HTML 始终包含完整数据。
基本用法:一个最小可运行示例
与useFetch一样,useLazyFetch应在<script setup>、插件(plugin)或路由中间件(middleware)中调用。
<script setup lang="ts"> const { status, data: posts } = await useLazyFetch('/api/posts') </script> <template> <div v-if="status === 'pending'"> Loading ... </div> <div v-else-if="status === 'error'"> Error loading posts </div> <div v-else> <div v-for="post in posts"> <!-- do something --> </div> </div> </template>关于代码中的await需要给出精确的行为预期(这一点即使在官方文档中也常被误解):
- 在服务器端:
await会照常等待请求,SSR 渲染会被阻塞直至数据就绪,HTML 中始终包含完整数据; - 在客户端导航时:对
lazy调用执行await会"立即 resolve"——注意它并不会真的等待请求完成。data在await之后仍处于默认值(即undefined或default()工厂的返回值),因此你必须通过status在模板中自行处理加载态与错误态。
也就是说,
await与lazy是相互独立的两件事。若你希望在客户端导航时真正等待数据到达再进入页面,应当去掉lazy选项(即改用useFetch),而不是依赖await。这一要点在入门指南数据获取中有专门的警告强调。
与useFetch({ lazy: true })、不await三者之间的差别
从用户可感知的行为来看,以下三种写法"看起来"相似,但底层语义并不相同:
| 写法 | 请求发起时机 | 客户端导航 | SSR 行为 |
|---|---|---|---|
useFetch(url)(默认,已await) | setup 期间立即发起 | 阻塞导航直到数据就绪 | 等待数据后输出完整 HTML |
useFetch(url, { lazy: true }) | 组件挂载(mounted)后发起 | 立即导航,后台拉取 | 等待数据后输出完整 HTML |
useFetch(url)(不await) | setup 期间立即发起 | 立即导航,后台拉取 | 等待数据后输出完整 HTML |
useLazyFetch(url) | 组件挂载后发起 | 立即导航,后台拉取 | 等待数据后输出完整 HTML |
关键在于:不await只是"不等待",请求仍在 setup 阶段就开始;而lazy(以及useLazyFetch)是把请求真正推迟到组件挂载时再发起,意图更明确、语义更自洽。因此官方建议:想要非阻塞行为时,优先使用useLazyFetch/useLazyAsyncData或在选项中显式写lazy: true。
useLazyFetch拥有与useFetch完全一致的签名(signature),差异仅在于内部已把lazy预设为true。
类型签名(Signature)
export function useLazyFetch<ResT, ErrorT = NuxtError<unknown>, DataT = ResT> ( url: string | Request | Ref<string | Request> | (() => string | Request), options?: UseFetchOptions<ResT, DataT>, ): AsyncData<DataT, ErrorT> & Promise<AsyncData<DataT, ErrorT>>需要理解的关键点是返回值AsyncData<DataT, ErrorT> & Promise<AsyncData<DataT, ErrorT>>:返回对象同时具备 Promise 能力,因此你可以:
await它:Promise 会在请求(在 SSR 下)完成后 resolve;- 直接解构而不
await:data可能在一段时间内为undefined; - 安全地解构 Promise 的方法:
then、catch、finally都是可枚举且可解构的(该能力由 asyncData.ts 中的Object.defineProperties保证)。
由于AsyncData是 Promise 与响应式对象融合的特殊类型,即使你不写await,解构出的status、error、data也始终是 Vue refs,可在模板中直接响应式使用,而在<script setup>内访问时需通过.value。
参数详解
useLazyFetch接受的参数与useFetch完全一致,lazy已自动置为true:
url(string | Request | Ref<string | Request> | () => string | Request):要请求的 URL 或 Request 对象。支持传入 ref 或返回字符串/Request 的函数,从而支持动态端点。options:请求配置对象,完整定义可参考useFetch的参数说明。所有选项都可以是静态值、ref或 computed 值。
useLazyFetch常用且高频的选项包括(完整列表见useFetch文档的 Options 表格):
| 选项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
key | MaybeRefOrGetter<string> | 自动生成 | 用于去重的唯一 key;缺省时由 URL、options 与源码调用位置组合哈希生成 |
query/params | MaybeRefOrGetter<SearchParams> | - | 追加到 URL 的查询参数,对象会被自动序列化 |
server | boolean | true | 是否在服务器端发起请求;设为false可做纯客户端获取 |
immediate | boolean | true | 设为false可阻止请求立即发起,配合execute()手动触发 |
default | () => DataT | - | 异步数据到达前data的默认值工厂函数,配合useLazyFetch尤其有用 |
transform | (input) => DataT | - | 对 resolve 后的结果进行转换(不可与pick同时使用) |
pick | string[] | - | 只从结果中挑选指定字段,可减小 Nuxt payload 体积(不可与transform同时使用) |
watch | MultiWatchSources \| false | - | 额外 watch 的响应式源数组,变化时自动刷新 |
dedupe | 'cancel' \| 'defer' | 'cancel' | 避免同一 key 并发重复请求:cancel取消旧请求,defer复用进行中的请求 |
enabled | MaybeRefOrGetter<boolean> | true | 是否允许请求执行的"闸门",为false时所有执行都被拦截 |
getCachedData | 函数 | 见下 | 自定义缓存读取策略,返回undefined时触发真实请求 |
为data提供默认值:懒加载的黄金搭档
由于懒加载下data在初始阶段就是默认值,若你想避免模板中的空值判断,可以配合default选项。仓库测试用例 use-fetch.test.ts 精确验证了这一行为:
it('should use default value with lazy', () => { const { data, pending } = useLazyFetch<TestData>('/api/test', { default: () => ({ method: 'default', headers: {} }) }) expect(pending.value).toBe(true) expect(data.value).toEqual({ method: 'default', headers: {} }) expect(data.value).not.toBeNull() expect(data.value.method).toEqual('default') })测试断言了三点:请求发出后pending为true、data立即等于default()返回的默认对象而非null/undefined、默认值可被安全访问。
getCachedData默认实现
const getDefaultCachedData = (key, nuxtApp, ctx) => nuxtApp.isHydrating ? nuxtApp.payload.data[key] : nuxtApp.static.data[key]该默认缓存仅当在nuxt.config中开启experimental.payloadExtraction时才生效。
返回值(AsyncData 对象)
useLazyFetch返回与useFetch相同的AsyncData对象,解构出的成员如下:
| 名称 | 类型 | 说明 |
|---|---|---|
data | Ref<DataT \| undefined> | 异步请求的结果。懒加载模式下请求完成前为默认值 |
refresh | (opts?: AsyncDataExecuteOptions) => Promise<void> | 手动刷新数据。默认情况下 Nuxt 会等待上一次refresh完成才允许下一次执行 |
execute | (opts?: AsyncDataExecuteOptions) => Promise<void> | refresh的别名 |
error | Ref<ErrorT \| undefined> | 请求失败时的错误对象 |
status | Ref<'idle' \| 'pending' \| 'success' \| 'error'> | 请求状态,用于区分四种生命周期状态 |
pending | Ref<boolean> | 请求进行中为true。若开启experimental.pendingWhenIdle,则status为idle且无缓存数据时也为true |
clear | () => void | 重置data为undefined(或options.default()的返回值)、error为undefined、status置为idle,并取消所有进行中的请求 |
四种status状态
idle:请求尚未开始(例如设置了{ immediate: false },或在 SSR 阶段设置了{ server: false })pending:请求进行中success:请求成功完成error:请求失败
在实际 SSR + 懒加载的首屏场景中,idle一般不会被观测到,因为 SSR 会先等待请求结束再输出 HTML;客户端导航场景下状态流转则为pending → success | error。
用watch观测渐进更新的数据
因为懒加载下posts一开始可能是undefined,你无法在await之后立即访问其内容,但可以通过watch观测它的变化。这是文档推荐的标准模式:
<script setup lang="ts"> /* useLazyFetch lets navigation continue before the fetch completes. * Handle loading and error states in the template. */ const { status, data: posts } = await useLazyFetch('/api/posts') watch(posts, (newPosts) => { // Because posts might start out null, you won't have access // to its contents immediately, but you can watch it. }) </script> <template> <div v-if="status === 'pending'"> Loading ... </div> <div v-else-if="status === 'error'"> Error loading posts </div> <div v-else> <div v-for="post in posts"> <!-- do something --> </div> </div> </template>实战组合:动态 URL、延迟请求与手动刷新
useLazyFetch支持响应式 URL 与响应式选项,常见于列表筛选、按 ID 查询等动态请求场景。
响应式查询参数:自动重新请求
const id = ref(null) const { data, status } = useLazyFetch('/api/user', { query: { user_id: id, }, })id变化时 Nuxt 会自动用新查询参数重新请求。该响应式能力的底层实现见 fetch.ts 的_fetchOptions:所有 fetch 选项被放入一个reactive对象,并作为 watch 源注入useAsyncData的watch数组,因此选项一旦变化即触发带cause: 'watch'的自动刷新。
computed getter URL:复杂路径构建
<script setup lang="ts"> const id = ref(null) const { data, status } = useLazyFetch(() => `/api/users/${id.value}`, { immediate: false, }) </script> <template> <div> <!-- disable the input while fetching --> <input v-model="id" type="number" :disabled="status === 'pending'" > <div v-if="status === 'idle'"> Type a user ID </div> <div v-else-if="status === 'pending'"> Loading ... </div> <div v-else> {{ data }} </div> </div> </template>immediate: false+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>上述三个组合场景均来自入门指南数据获取中关于 lazy / computed URL / not-immediate 的完整示例,可直接复制运行。
底层原理:从源码看懒加载如何实现
理解useLazyFetch的关键在于lazy: true在useAsyncData中如何改变行为。核心的分支逻辑位于 asyncData.ts 的客户端分支,其判断顺序可以概括为:
- SSR 或服务器端(
import.meta.server且fetchOnServer && opts.immediate):通过onServerPrefetch(() => promise)注册请求,Nuxt 会等待该 promise 完成后再序列化页面——因此懒加载不改变 SSR 的 HTML 完整度; - 客户端首屏 / 导航:
- 若正在 hydration 且已有服务器数据,直接沿用,不再发起请求;
- 若处于组件 setup 且满足
opts.lazy条件,则把initialFetch压入_nuxtOnBeforeMountCbs数组,推迟到组件onBeforeMount(即将挂载)时才真正执行请求——这正是"不阻塞导航、挂载后再取数"的机制来源; - 否则(非 lazy)立即执行
initialFetch(),从而阻塞导航。
由此可见:useFetch与useLazyFetch的分水岭,本质上是 asyncData.ts 中这段"请求何时发起"的分派逻辑——lazy 把请求从"导航前 await"推迟到"组件挂载前注册",导航因此不再等待。
另一个值得注意的实现细节:useAsyncData的execute内建了去重与取消机制。当同一 key 已有进行中的请求时,dedupe: 'cancel'(默认值)会通过AbortController中止旧请求;调用clear()同样会触发 abort,仓库测试 use-fetch.test.ts 通过 mockAbortController验证了useLazyFetch的clear会取消进行中的/api/sleep请求。
与 SSR payload 的关系
useLazyFetch(作为useFetch家族的成员)会自动把响应数据加入 Nuxt payload,使数据能够从服务器端传递到客户端,避免 hydration 时重复请求。在服务端渲染 + hydration 场景下,客户端若发现 payload 中已存在该 key 的数据,将直接使用而不再发起请求。
在获取数据前若要跨组件共享同一份数据,可以通过useNuxtData取回用useFetch/useLazyFetch(指定相同key)创建的 keyed state。若需要自定义默认选项(如baseURL或认证头)的useFetch变体,官方推荐使用createUseFetch——useLazyFetch本身就是该工厂能力的最好例证。
重要警告与注意事项
围绕useLazyFetch有以下三条必须遵守的边界:
await不等待数据(客户端):在客户端导航期间await useLazyFetch(...)会立即 resolve,data仍处于默认值。必须检查模板中的status === 'pending'与status === 'error'后再使用结果;若希望导航真正等待数据,请改用useFetch。保留函数名,不可自定义:
useLazyFetch与useFetch一样属于编译器会转换的保留函数名,因此你不应把自己的函数命名为useLazyFetch。要创建带预置选项的自定义变体,应使用createUseFetch或createUseAsyncData。这在源码中亦有体现:_functionName等内部元数据正是由编译期注入,用于开发诊断。不要引入来自其他库的同名导入:若
data解构出来是字符串而非 JSON 解析对象,请检查组件中是否意外引入了import { useFetch } from '@vueuse/core'之类的冲突导入。
相关资源
- useFetch API 参考:完整的 options 表格、类型定义与返回值说明
- useAsyncData API 参考:更底层的异步数据原语
- 数据获取入门指南:lazy、SSR、payload、headers 传递等系统讲解
- createUseFetch API 参考:自定义带默认选项的 useFetch 变体
- useLazyFetch 源码实现:工厂实例定义
- useAsyncData 核心实现:懒加载分派、去重、abort、payload 序列化逻辑
- 仓库单元测试:
useLazyFetch的默认值、abort 行为等可执行验证
【免费下载链接】nuxtthe full-stack Vue framework项目地址: https://gitcode.com/GitHub_Trending/nu/nuxt
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考