ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Nuxt 数据获取实战指南:useFetch、useAsyncData 与 $fetch 的完整解析

Nuxt 数据获取实战指南:useFetch、useAsyncData 与 $fetch 的完整解析 Nuxt 数据获取实战指南useFetch、useAsyncData 与 $fetch 的完整解析【免费下载链接】nuxtthe full-stack Vue framework项目地址: https://gitcode.com/GitHub_Trending/nu/nuxtNuxt 通过useFetch、useAsyncData两个 composable 与内置的$fetch工具在浏览器和服务端两种环境中安全地完成数据获取。本文基于 Nuxt 官方文档 Data Fetching 的完整脉络展开并结合开源仓库中 fetch.ts、asyncData.ts 等核心源码讲透三者各自的适用场景、payload 传输机制、全部关键选项以及序列化的底层细节帮助你在 SSR 应用中做出正确且高效的数据获取决策。为什么需要useFetch和useAsyncDataNuxt 是一个支持同构isomorphic / universal代码的框架同一段代码既在服务端执行渲染 HTML也在客户端执行水合 HTML。如果直接在组件的setup函数里用$fetch发起请求数据会被拉取两次——服务端渲染一次客户端水合时再一次。这不仅带来重复请求还会引发水合不一致、交互可用时间变长等难以预料的问题。useFetch与useAsyncData解决的核心问题就是如果 API 调用发生在服务端数据会通过 payload 转发到客户端浏览器水合时直接复用不再重复请求。这个 payload 是一个 JavaScript 对象可通过useNuxtApp().payload访问参见 useNuxtApp 文档。在源码层面客户端从内联在 HTML 中的__NUXT_DATA__元素中解析它payload.ts 的getNuxtClientPayload会读取document.getElementById(__NUXT_DATA__)并用parsePayload反序列化。开发时可以用 Nuxt DevTools 的Payload 标签页检查这份数据。script setup langts const { data } await useFetch(/api/data) async function handleFormSubmit () { const res await $fetch(/api/submit, { method: POST, body: { // My form data }, }) } /script template div v-ifdata undefined No data /div div v-else form submithandleFormSubmit !-- form input tags -- /form /div /template在上面的示例中useFetch确保请求在服务端发起并正确转发给浏览器而$fetch没有这套机制更适合仅从浏览器发起的、事件驱动的请求如表单提交。Suspense 与导航阻塞Nuxt 在底层使用 Vue 的Suspense组件确保在视图所需的所有异步数据就绪之前不会完成页面导航。文档建议可以为页面间导航添加NuxtLoadingIndicator进度条改善等待体验。关于await的重要说明文档示例中通常对useFetch/useAsyncData进行await但这并非总是必须的理解其确切语义非常关键await不会改变服务端渲染的 HTML。服务端渲染时Nuxt 无论如何都会等待请求完成再序列化页面底层是Suspense与onServerPrefetch所以发给浏览器的永远是填充完整的结果。await真正影响的是你script setup中后续代码的执行时机和客户端导航行为带await执行会暂停直到数据就绪后续代码可依赖data已填充。客户端导航时导航会被阻塞用户停留在当前页可配合NuxtLoadingIndicator然后落地到数据完整的页面。这是默认行为。不带await执行立即继续请求在后台运行data先取默认值请求完成后才填充。客户端导航会立即发生你需要自己用返回的status和error处理加载与错误状态。两种写法没有普适的优劣选择取决于该路由期望的交互体验。不await的可见效果与lazy选项类似导航不被阻塞、自行处理加载态但两者并不相同lazy是显式标志会把请求推迟到组件挂载时而不await只是请求在 setup 期间就开始、只是不阻塞执行。想要非阻塞行为时推荐显式使用lazy或useLazyFetch/useLazyAsyncData让意图更明确。注意await与lazy是相互独立的且在客户端await一个lazy调用不会得到你期望的效果。例如await useLazyFetch(...)或await useFetch(..., { lazy: true })在服务端渲染时照常阻塞但在客户端导航上await会立即 resolvedata在await之后仍为默认值你必须通过status处理加载态。若真的希望导航等待数据应当去掉lazy选项而不是依赖await。$fetch最底层的网络请求工具Nuxt 内置了 ofetch 库并将其全局自动导入为$fetch别名script setup langts async function addTodo () { const todo await $fetch(/api/todos, { method: POST, body: { // My todo data }, }) } /script注意仅使用$fetch无法获得请求去重和导航阻塞保护。推荐将其用于客户端交互事件驱动或结合useAsyncData获取组件初始数据。将客户端 Headers 传递给 API在服务端调用useFetch时Nuxt 会使用useRequestFetch自动代理客户端的 headers 和 cookieshost等不宜转发的头除外。script setup langts const { data } await useFetch(/api/echo) /script// /api/echo.ts export default defineEventHandler(event parseCookies(event))这一机制的源码在 ssr.ts 中createRequestFetch会包装底层fetch当请求 URL 以/开头时把event.req.headers中不在UNFORWARDED_HEADERS黑名单里的头逐一注入而 fetch.ts 第 371 行的关键逻辑是fetchOptions.$fetch || (import.meta.server ? useRequestFetch() : $fetch)——即服务端默认走代理版客户端走原生$fetch。UNFORWARDED_HEADERS黑名单覆盖了accept、content-length、content-type、host等 20 余个逐跳hop-by-hop或描述请求体的头。如果你不走useFetch而用同构的$fetch可以用useRequestHeaders手动取出并转发 cookiescript setup langts const headers useRequestHeaders([cookie]) async function getCurrentUser () { return await $fetch(/api/me, { headers }) } /script你也可以直接使用useRequestFetch自动代理 headers。警告向外部 API 代理 headers 要极其谨慎只包含你真正需要的头。以下常见头不应被代理host、accept、content-length、content-md5、content-type、x-forwarded-host、x-forwarded-port、x-forwarded-proto、cf-connecting-ip、cf-ray。useFetchSSR 安全的请求封装useFetch内部基于$fetch执行 SSR 安全的网络调用script setup langts const { data: count } await useFetch(/api/count) /script template pPage visits: {{ count }}/p /template从源码结构看fetch.ts 中useFetch的实现最终落到useAsyncData(key, handler, opts)它把 URL、method、query、body 等通过generateOptionSegments参与 key 的哈希生成第 323 行$f hashKey([autoKey, url, ...generateOptionSegments(fetchOptions)])这解释了后文「相同 URL 在不同组件中 key 不同」的缓存语义。useLazyFetch则只是lazy: true的工厂实例fetch.ts。更完整的参数说明见 useFetch API 文档。useAsyncData更细粒度的控制useAsyncData负责包装任意异步逻辑并在其 resolve 后返回结果。useFetch(url)几乎等价于useAsyncData(url, () event.$fetch(url))它就是最常见用法上的开发者体验糖。当useFetch不适用时——例如 CMS 或第三方提供了自己的查询层——就可以用useAsyncData包装自定义调用同时保留 composable 的全部收益script setup langts const { data, error } await useAsyncData(users, () myGetFunction(users)) // This is also possible: const { data, error } await useAsyncData(() myGetFunction(users)) /script第一个参数是唯一的 key用于缓存第二个参数查询函数的响应。直接传查询函数时 key 会自动生成。由于自动生成的 key 只依据useAsyncData的调用位置建议始终显式创建自己的 key避免意外行为比如你自己封装的useAsyncDatacomposable 场景。设置 key 还有助于通过useNuxtData在组件间共享同一份数据或刷新特定数据。script setup langts const { id } useRoute().params const { data, error } await useAsyncData(user:${id}, () { return myGetFunction(users, { id }) }) /scriptuseAsyncData也很适合包装并等待多个$fetch请求完成后再统一处理结果script setup langts const { data: discounts, status } await useAsyncData(cart-discount, async (_nuxtApp, { signal }) { const [coupons, offers] await Promise.all([ $fetch(/cart/coupons, { signal }), $fetch(/cart/offers, { signal }), ]) return { coupons, offers } }) // discounts.value.coupons // discounts.value.offers /script注意 handler 的第二参数提供了{ signal }把它传给$fetch可以让请求随组件卸载或刷新而正确取消。从源码看asyncData.ts 中execute会通过mergeAbortSignals把内部AbortController、enabled监听器与timeout的信号合并传给 handler。注意useAsyncData用于获取和缓存数据不是用来触发副作用如调用 Pinia action的——这可能导致空值下的重复执行等意外行为。确需触发副作用时请使用callOnce工具。script setup langts const offersStore useOffersStore() // you cant do this await useAsyncData(() offersStore.getOffer(route.params.slug)) /script详细说明见 useAsyncData API 文档。返回值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访问。源码中这五个字段在 asyncData.ts 的asyncReturn对象中构造clear通过AbortController.abort取消在途请求后调用内部的clearNuxtDataByKey把data重置为default()、status重置为idle与文档描述逐条对应。默认情况下Nuxt 会等待上一次refresh完成后才允许再次执行。注意如果服务端没有获取数据例如server: false数据不会在水合完成前被获取。这意味着即使在客户端await useFetchdata在script setup中仍会是undefined。选项详解useAsyncData和useFetch返回相同的对象类型并接受一组共同的选项作为最后一个参数可控制导航阻塞、缓存与执行等行为。以下逐项展开默认值均对照 asyncData.ts 中opts.xxx ?? 默认值的源码确认。lazy不阻塞导航默认情况下数据获取 composable 会通过 Vue 的 Suspense 等待异步函数 resolve 后才导航到新页面。lazy选项默认false可在客户端导航中忽略这一行为此时你需要用status手动处理加载态script setup langts const { status, data: posts } useFetch(/api/posts, { lazy: true, }) /script template !-- you will need to handle a loading state -- div v-ifstatus pending Loading ... /div div v-else div v-forpost in posts !-- do something -- /div /div /template等价写法是直接使用useLazyFetch/useLazyAsyncDatascript setup langts const { status, data: posts } useLazyFetch(/api/posts) /scriptserver: false仅客户端获取默认情况下数据获取 composable 会在客户端和服务端两个环境执行异步函数。把server选项默认true设为false可以让调用只发生在客户端。首次加载时数据要等到水合完成后才获取因此你必须处理 pending 状态但后续的客户端侧导航会照常等待数据后再加载页面。配合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意在setup方法中调用或直接在生命周期钩子内调用其他场景应使用$fetch。pick/transform最小化 payload 体积pick选项只从 composable 结果中选取指定字段减少存储在 HTML 文档中的 payload 体积script setup langts /* 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 })) }, })transform与pick不要在同一个调用中混用。源码中 asyncData.ts 的execute完成链是先执行options.transform(_result)再对结果做pick两者都在数据写入nuxtApp.payload.data[key]之前生效。注意pick和transform都不能阻止无用的数据最初被网络请求获取到它们只是阻止这些数据进入从服务端传到客户端的 payload。缓存与重新获取KeysuseFetch和useAsyncData都通过 key 防止重复获取相同数据useFetch从 URL、fetch 选项和调用位置生成 key。这意味着不同组件中两个相同 URL 的useFetch调用拥有不同的 key各自发起请求。要在多个组件间共享同一份数据请在最后一个参数options 对象中显式传入相同的keyuseAsyncData若第一个参数是字符串则直接作为 key若第一个参数是查询函数则按调用位置自动生成 key。要按 key 读取已缓存的数据可以用useNuxtData。共享状态与选项一致性当多个组件用同一个 key 调用useAsyncData或useFetch时它们共享同一份data、error、statusref。这保证了组件间的一致性但要求某些选项保持一致。以下选项在相同 key 的所有调用间必须一致handler函数、deep、transform、pick、getCachedData、default。// ❌ This will trigger a development warning const { data: users1 } useAsyncData(users, (_nuxtApp, { signal }) $fetch(/api/users, { signal }), { deep: false }) const { data: users2 } useAsyncData(users, (_nuxtApp, { signal }) $fetch(/api/users, { signal }), { deep: true })以下选项可以安全地不同不会触发警告server、lazy、immediate、dedupe、watch。// ✅ This is allowed const { data: users1 } useAsyncData(users, (_nuxtApp, { signal }) $fetch(/api/users, { signal }), { immediate: true }) const { data: users2 } useAsyncData(users, (_nuxtApp, { signal }) $fetch(/api/users, { signal }), { immediate: false })开发环境下的一致性校验在 asyncData.ts 中实现它会对handler、transform、pick、getCachedData、serialize、default做哈希比对并对deep做 ref 深度检查不一致时通过NUXT_E3004诊断报告警告。需要相互独立的实例时使用不同的 key// These are completely independent instances const { data: users1 } useAsyncData(users-1, (_nuxtApp, { signal }) $fetch(/api/users, { signal })) const { data: users2 } useAsyncData(users-2, (_nuxtApp, { signal }) $fetch(/api/users, { signal }))响应式 keykey 可以是 computed ref、普通 ref 或 getter 函数实现依赖变化时自动更新的动态数据获取// Using a computed property as a key const userId ref(123) const { data: user } useAsyncData( computed(() user-${userId.value}), () fetchUser(userId.value), ) // When userId changes, the data will be automatically refetched // and the old data will be cleaned up if no other components use it userId.value 456源码中asyncData.ts 通过isKeyReactive判断 key 是否响应式并对响应式 key 挂载一个flush: sync的 watcherkey 变化时先为旧 key 执行unregister引用计数归零时释放 watcher、中止在途请求再为新 key 建立容器并触发获取旧数据在无其他组件使用时被清理。refresh与execute要手动获取或刷新数据使用 composable 提供的execute或refreshscript setup langts const { data, error, execute, refresh } await useFetch(/api/users) /script template div p{{ data }}/p button click() refresh() Refresh data /button /div /templateexecute是refresh的语义化别名在获取并非立即执行 的场景下更贴切。要全局重新获取或使缓存失效见clearNuxtData与refreshNuxtData——两者的实现同样位于 asyncData.tsrefreshNuxtData通过app:data:refresh钩子广播触发各 key 的重新执行。clear不需要知道具体 key 时可以用 composable 返回的clear函数清空数据script setup langts const { data, clear } await useFetch(/api/users) const route useRoute() watch(() route.path, (path) { if (path /) { clear() } }) /scriptwatch监听响应式值触发重新获取watch选项默认不启用可以在其他响应式值变化时重跑获取函数可传入一个或多个可监听源script setup langts const id ref(1) const { data, error, refresh } await useFetch(/api/users, { /* Changing the id will trigger a refetch */ watch: [id], }) /script注意监听响应式值不会改变被请求的 URL。例如下面代码始终请求最初构造 URL 时的用户 ID因为 URL 在函数调用时就已经定型script setup langts const id ref(1) const { data, error, refresh } await useFetch(/api/users/${id.value}, { watch: [id], }) /script需要根据响应式值改变 URL 时应使用后文的 computed URL。另一个细节提供响应式 fetch 选项method、baseURL、query、params、body、headers见 fetch.ts 中的MAYBE_REF_OR_GETTER_OPTION_KEYS时它们会被自动监听并触发重新获取。某些情况下可用watch: false退出该行为const id ref(1) // Wont automatically refetch when id changes const { data, execute } await useFetch(/api/users, { query: { id }, // id is watched by default watch: false, // disables automatic watching of id }) // doesnt trigger refetch id.value 2Computed URL有时需要根据响应式值计算 URL并在每次变化时刷新数据。把参数作为响应式值传入query即可Nuxt 会自动跟踪这些值并在变化时重新获取script setup langts const id ref(null) const { data, status } useLazyFetch(/api/user, { query: { user_id: id, }, }) /script更复杂的 URL 构造可以用返回 URL 字符串的回调作为 computed getter。每次依赖变化数据都会用新构造的 URL 重新获取配合immediate: false可以等到响应式值真正变化后再发起请求script setup langts const id ref(null) const { data, status } useLazyFetch(() /api/users/${id.value}, { immediate: false, }) /script template div !-- disable the input while fetching -- input v-modelid typenumber :disabledstatus pending div v-ifstatus idle Type a user ID /div div v-else-ifstatus pending Loading ... /div div v-else {{ data }} /div /div /template若需要在其他响应式值变化时强制刷新也可以 watch 其他值。immediate延迟立即执行useFetch默认在调用时立即开始获取。设置immediate: false可以阻止这一行为例如等待用户交互。此时你需要同时使用status处理获取生命周期、execute启动数据获取script setup langts const { data, error, execute, status } await useLazyFetch(/api/comments, { immediate: false, }) /script template div v-ifstatus idle button clickexecute Get data /button /div div v-else-ifstatus pending Loading comments... /div div v-else {{ data }} /div /templatestatus的四个取值含义idle获取尚未开始pending获取已开始但尚未完成error获取失败success获取成功完成。该状态机在源码中的定义是 asyncData.ts 的AsyncDataRequestStatus idle | pending | success | error初始值为idle请求发起时置pending成功后置success失败或被用户 abort时分别置error或回落到idle。此外还有几个值得注意的辅助选项dedupe默认cancel可改为defer同一 key 已有在途请求时直接复用其 promisetimeout以毫秒为单位中止超时请求enabled支持 ref 或 getter置false时阻止获取并取消在途请求deep默认由构建配置决定设为false可让data以浅 ref 返回以提升性能serialize控制结果是否写入 payload。传递 Headers 与 Cookies在浏览器中调用$fetch时用户的cookie等 header 会直接发给 API。通常出于安全考虑服务端渲染期间$fetch不包含用户浏览器的 cookies也不会传递 fetch 响应中的 cookies。但当在服务端用相对 URL调用useFetch时Nuxt 会通过useRequestFetch代理 headers 和 cookieshost等除外实现细节见上文 ssr.ts 的UNFORWARDED_HEADERS与createRequestFetch。从服务端 API 调用向 SSR 响应传递 Cookies反方向——把内部请求拿到的 cookies 传回客户端——需要自行处理import type { H3Event } from h3 export const fetchWithCookie async (event: H3Event, url: string) { /* Get the response from the server endpoint */ const res await $fetch.raw(url) /* Get the cookies from the response */ const cookies res.headers.getSetCookie() /* Attach each cookie to our incoming Request */ for (const cookie of cookies) { event.res.headers.append(set-cookie, cookie) } /* Return the data of the response */ return res._data }script setup langts // 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)) /scriptOptions API 支持Nuxt 提供了一种在 Options API 中执行asyncData获取的方式组件定义必须用defineNuxtComponent包裹script export default defineNuxtComponent({ /* Use the fetchKey option to provide a unique key */ fetchKey: hello, async asyncData () { return { hello: await $fetch(/api/hello), } }, }) /script更完整的说明见 defineNuxtComponent 文档。不过script setup或script setup langts才是 Nuxt 中声明 Vue 组件的推荐方式。服务端到客户端的数据序列化使用useAsyncData/useLazyAsyncData以及一切利用 Nuxt payload 的机制把服务端获取的数据传到客户端时payload 使用devalue进行序列化。这意味着传输的不仅是基础 JSON还能序列化并 revive 正则、Date、Map、Set、ref、reactive、shallowRef、shallowReactive、NuxtError等更复杂的数据类型。源码印证payload.ts 顶部import { parse } from devalueparsePayload用useNuxtApp()._payloadRevivers作为 revive 回调解析__NUXT_DATA__内容。你也可以为 Nuxt 不支持的类型定义自己的序列化/反序列化器definePayloadReducer/definePayloadReviver同样位于 payload.ts详见 useNuxtApp 文档。注意上述 devalue 机制不适用于用$fetch或useFetch从你的服务端路由获取的数据——那部分走JSON.stringify见下节。从 API 路由序列化数据从server目录获取数据时响应用JSON.stringify序列化。由于序列化只支持 JavaScript 原始类型Nuxt 会尽力把$fetch和useFetch的返回类型转换成与实际值匹配。示例export default defineEventHandler(() { return new Date() })script setup langts // Type of data is inferred as string even though we returned a Date object const { data } await useFetch(/api/foo) /script自定义序列化函数在返回对象上定义toJSON即可定制序列化行为Nuxt 会尊重toJSON的返回类型不再做类型转换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 langts // Type of data is inferred as // { // 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(), // 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 langts 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 请求消费 SSEServer-Sent Events通过 GET 消费 SSE 时可用浏览器原生EventSource或 VueUse 的useEventSource。但通过POST消费 SSE 需要手动处理连接// Make a POST request to the SSE endpoint const response await $fetchReadableStream(/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) }并行请求当多个请求互不依赖时用Promise.all()并行执行可提升性能const { data } await useAsyncData((_nuxtApp, { signal }) { return Promise.all([ $fetch(/api/comments/, { signal }), $fetch(/api/author/12, { signal }), ]) }) const comments computed(() data.value?.[0]) const author computed(() data.value?.[1])小结$fetch最简单直接的请求方式适合事件驱动的客户端交互无 payload 转发与去重机制。useFetch$fetch的 SSR 安全封装服务端请求经useRequestFetch自动代理 cookie/headers数据随 payload 下发客户端默认阻塞导航直到数据就绪。useAsyncDatauseFetch的底层与超集可包装任意异步逻辑第三方 SDK、并行请求组合通过 key 实现跨组件共享、去重与按 key 刷新/清理。用lazy、server: false、immediate: false、watch、computed URL 精确控制获取时机用pick/transform收缩 payload 体积用status四态idle/pending/success/error驱动 UI。数据经 payloaddevalue与 API 路由JSON.stringifytoJSON逃生口两条不同的序列化通道传输理解差异可避免类型推断与实际值不符的坑。以上机制均可在仓库源码中逐行验证useFetch 实现、useAsyncData 实现、headers 代理、payload 解析。【免费下载链接】nuxtthe full-stack Vue framework项目地址: https://gitcode.com/GitHub_Trending/nu/nuxt创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进