Nuxt 中useFetch完全指南:SSR 友好的数据请求组合式函数从参数到原理
【免费下载链接】nuxtthe full-stack Vue framework项目地址: https://gitcode.com/GitHub_Trending/nu/nuxt
本篇文章以 Nuxt 官方 API 文档 docs/4.api/2.composables/use-fetch.md 为骨架,结合仓库内真实实现 packages/nuxt/src/app/composables/fetch.ts 与 packages/nuxt/src/app/composables/asyncData.ts,系统讲解useFetch的用法、全部参数语义、返回值、响应式行为与内部实现原理。读完你不仅能正确写出可用的useFetch代码,还能理解 key 生成、SSR 请求转发、hydration 数据复用、状态去重等底层机制,避免在服务端渲染与客户端导航场景中踩坑。
概述:useFetch是什么
useFetch是 Nuxt 提供的一个 SSR(服务端渲染)友好的数据请求组合式函数(composable)。它本质上是对两个底层 API 的封装:
useAsyncData:负责「SSR 友好」这部分——管理异步数据的状态机、自动生成请求 key、把响应写入 Nuxt payload,从而在服务端渲染完成后把数据传给客户端,页面水合(hydrate)时不会在客户端重复请求同一份数据;$fetch:负责「发请求」这部分——基于 unjs/ofetch 的增强版 fetch,自动处理 JSON 序列化、错误统一与拦截器等能力。
相比单独使用二者,useFetch额外带来了三个开箱即用的能力(见 fetch.ts 中对返回值的描述):
- 自动生成请求 key:根据 URL、options 以及调用点在源码中的位置计算出一个稳定且唯一的 key;
- 服务端路由类型提示:当你请求
server/api目录中定义的路由时,TypeScript 能根据服务端路由自动推导可用的 URL 与 HTTP method; - 自动推断 API 响应类型:
ResT泛型可以自动从请求中推导,减少手写类型。
:::noteuseFetch是一个 composable,必须直接在被 Vue 组件 setup、Nuxt plugin 或路由中间件(route middleware)等拥有组件实例/作用域的地方调用。它返回响应式对象,并将结果写入 Nuxt payload,从而让数据在服务端渲染完成后被传递到客户端,且客户端水合时无需重新请求。 :::
文档开头将其定位为:Fetch data from an API endpoint with an SSR-friendly composable——即用 SSR 友好的方式从 API 端点获取数据。
在项目中的位置
在源码层面,packages/nuxt/src/app/composables/fetch.ts 中真正被导出的是createUseFetch工厂与默认实例:
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, _functionName: 'useLazyFetch', })从源码可以看到useLazyFetch其实只是createUseFetch({ lazy: true })的产物,其完整用法见useLazyFetch文档。
基本用法
在页面/组件<script setup>中直接调用即可:
<script setup lang="ts"> const { data, status, error, refresh, clear } = await useFetch('/api/modules', { pick: ['title'], }) </script>是否需要await?
useFetch返回一个 Promise,因此可以await,也可以不await。两种写法的差异在于调用之后的行为:
await时:执行会暂停直到data被填充;在客户端导航时,会阻塞导航直到数据就绪,<script setup>中拿到的data一定是有值的;- 不
await时:执行立即继续,data先保持默认值(通常是undefined),直到请求 resolve 后才被填充;在客户端导航时不阻塞,你需要自己依据返回的status与errorref 处理加载与错误态。
这与lazy选项产生的效果类似,但lazy才是显式选择非阻塞导航的正式方式。
在 SSR 场景中,无论你是否
await,Nuxt 都会等待请求完成后再渲染页面,因此返回的 HTML 始终包含数据。
返回值都是什么形态
注意data、status、error是 Vue 的ref,在<script setup>中需要以.value访问;而refresh/execute与clear是普通函数,直接调用即可。
添加 query 查询参数
通过query选项可以为请求添加搜索参数。该选项扩展自 unjs/ofetch,内部用 unjs/ufo 构造 URL;传入的ref对象会被自动解包(stringify):
const param1 = ref('value1') const { data, status, error, refresh } = await useFetch('/api/modules', { query: { param1, param2: 'value2' }, })以上示例最终请求为:https://api.nuxt.com/modules?param1=value1¶m2=value2。
使用拦截器(interceptors)
useFetch透传了 ofetch 的拦截器能力,可以用来设置请求头、处理请求/响应错误、保存 token 等:
const { data, status, error, refresh, clear } = await useFetch('/api/auth/login', { onRequest ({ request, options }) { // 设置请求头 // 注意:此处依赖 ofetch >= 1.4.0,必要时需要刷新 lockfile options.headers.set('Authorization', '...') }, onRequestError ({ request, options, error }) { // 处理请求错误 }, onResponse ({ request, response, options }) { // 处理响应数据 localStorage.setItem('token', response._data.token) }, onResponseError ({ request, response, options }) { // 处理响应错误 }, })响应式 URL 与 key、共享状态的语义
响应式 URL:路由变化时自动重新请求
useFetch的第一个参数支持传入字符串、Request对象、Vueref,或返回前两者的函数。当 URL 是响应式的并发生变化时,请求会自动重新发起:
<script setup lang="ts"> const route = useRoute() const id = computed(() => route.params.id) // 当路由变化、id 更新时,数据会自动重新拉取 const { data: post } = await useFetch(() => `/api/posts/${id.value}`) </script>key 与共享状态
自动生成的 key对每个调用点(call site)都是唯一的。因此在不同组件中用相同 URL 与 options 调用useFetch,彼此不会共享状态,会各自发起请求;而同一个组件的多个实例因为使用同一调用点,天然共享状态。
如果你希望跨组件共享同一份data、error、status,需要为每次调用显式传入相同的key:
::code-group
<script setup lang="ts"> // 与 ComponentB 共享数据——只发起一次请求 const { data } = await useFetch('/api/random', { key: 'random' }) </script><script setup lang="ts"> // 与 ComponentA 共享数据——只发起一次请求 const { data } = await useFetch('/api/random', { key: 'random' }) </script>::
:::tip 使用useFetch创建的带 key 状态,可以在整个 Nuxt 应用内通过useNuxtData获取,例如在另一个组件或 composable 中读取已有数据或执行手动刷新。 :::
两个重要警告
:::warninguseFetch是被编译器转换的保留函数名(关于这一点见后文「从源码看 key 生成与保留函数名」),因此不要自己定义名为useFetch的函数。需要自定义带默认选项的变体时,请改用createUseFetch(参考 自定义 useFetch 配方)。 :::
:::warning 如果你发现从useFetch解构出的data是字符串而不是 JSON 解析后的对象,请检查组件里是否引入了类似import { useFetch } from '@vueuse/core'的导入语句——它会把编译器变换指向另一个库的实现,导致类型与行为都不正确。 :::
响应式请求选项(Reactive Fetch Options)
除了 URL,请求选项本身也支持响应式:可以传computed、ref或计算属性 getter。当某个响应式选项被更新时,useFetch会以更新后的值自动重新发起请求:
const searchQuery = ref('initial') const { data } = await useFetch('/api/search', { query: { q: searchQuery }, }) // 触发重新请求:/api/search?q=new%20search searchQuery.value = 'new search'若想关闭这种自动重请求,将watch设为false:
const searchQuery = ref('initial') const { data } = await useFetch('/api/search', { query: { q: searchQuery }, watch: false, }) // 不会触发重新请求 searchQuery.value = 'new search'从实现看(asyncData.ts),watch: false会让响应式选项对象_fetchOptions从自动 watch 的 sources 中移除,但仍保留 key 的响应式。
类型签名(Signature)
文档中给出的公开类型签名如下(保持与 fetch.ts 中公开的重载一致):
export function useFetch<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>> type UseFetchOptions<ResT, DataT = ResT> = { key?: MaybeRefOrGetter<string> method?: MaybeRefOrGetter<string> query?: MaybeRefOrGetter<SearchParams> params?: MaybeRefOrGetter<SearchParams> body?: MaybeRefOrGetter<RequestInit['body'] | Record<string, any>> headers?: MaybeRefOrGetter<Record<string, string> | [key: string, value: string][] | Headers> baseURL?: MaybeRefOrGetter<string> cache?: false | 'default' | 'force-cache' | 'no-cache' | 'no-store' | 'only-if-cached' | 'reload' server?: boolean lazy?: boolean immediate?: boolean getCachedData?: (key: string, nuxtApp: NuxtApp, ctx: AsyncDataRequestContext) => DataT | undefined deep?: boolean dedupe?: 'cancel' | 'defer' timeout?: number enabled?: MaybeRefOrGetter<boolean> serialize?: boolean default?: () => DataT | Ref<DataT> transform?: (input: ResT) => DataT | Promise<DataT> pick?: string[] $fetch?: typeof globalThis.$fetch watch?: MultiWatchSources | false } type AsyncDataRequestContext = { /** The reason for this data request */ cause: 'initial' | 'refresh:manual' | 'refresh:hook' | 'watch' } type AsyncData<DataT, ErrorT> = { data: Ref<DataT | undefined> pending: Ref<boolean> refresh: (opts?: AsyncDataExecuteOptions) => Promise<void> execute: (opts?: AsyncDataExecuteOptions) => Promise<void> clear: () => void error: Ref<ErrorT | undefined> status: Ref<AsyncDataRequestStatus> } interface AsyncDataExecuteOptions { dedupe?: 'cancel' | 'defer' timeout?: number signal?: AbortSignal } type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'关于AsyncData<DataT, ErrorT> & Promise<AsyncData<DataT, ErrorT>>这个联合返回类型:它既是对象又是 Promise。源码中对应为(asyncData.ts):
export type AsyncData<Data, Error> = _AsyncData<Data, Error> & Promise<_AsyncData<Data, Error>>这解释了为什么既能await useFetch(...),又能直接解构出data、refresh等属性。
参数详解
第一参数URL类型为string | Request | Ref<string | Request> | (() => string | Request):可以是字符串、Request对象、Vue ref,或返回字符串/Request的函数,完整支持动态端点的响应式。
第二参数options为对象:扩展了 unjs/ofetch 的 options 与AsyncDataOptions。所有选项既可以是静态值,也可以是ref或 computed 值。
| Option | Type | Default | Description |
|---|---|---|---|
key | MaybeRefOrGetter<string> | auto-gen | 用于去重的唯一 key。若未提供,则由 URL、options 与源码中调用点位置生成。 |
method | MaybeRefOrGetter<string> | 'GET' | HTTP 请求方法。 |
query | MaybeRefOrGetter<SearchParams> | - | 追加到 URL 的查询/搜索参数。别名:params。 |
params | MaybeRefOrGetter<SearchParams> | - | query的别名。 |
body | MaybeRefOrGetter<RequestInit['body'] \| Record<string, any>> | - | 请求体。对象会被自动字符串化。 |
headers | MaybeRefOrGetter<Record<string, string> \| [key, value][] \| Headers> | - | 请求头。 |
baseURL | MaybeRefOrGetter<string> | - | 请求的基础 URL。 |
cache | false \| string | - | 缓存控制。布尔值false关闭缓存,或使用 Fetch API 值如default、no-store等。 |
server | boolean | true | 是否在服务端请求。 |
lazy | boolean | false | 为true时在路由加载完成后才 resolve(不阻塞导航)。 |
immediate | boolean | true | 为false时阻止请求立即发起。 |
default | () => DataT | - | 在异步 resolve 之前为data提供默认值的工厂函数。 |
timeout | number | - | 请求超时毫秒数(默认undefined,即不超时)。 |
transform | (input: DataT) => DataT \| Promise<DataT> | - | 在结果 resolve 后转换结果的函数。 |
getCachedData | (key, nuxtApp, ctx) => DataT \| undefined | - | 返回缓存数据的函数,默认实现见下文。 |
pick | string[] | - | 只从结果中挑选指定 key。 |
watch | MultiWatchSources \| false | - | 要监听并自动刷新的响应式源数组;false关闭监听。 |
deep | boolean | false | 是否用深响应 ref 返回数据。默认false(浅响应 ref),性能更好。 |
dedupe | 'cancel' \| 'defer' | 'cancel' | 避免同一 key 同时发起多次请求。 |
enabled | boolean | true | 控制请求是否允许执行的「闸门」。为false时,初始请求、execute/refresh、watch 触发的执行全被阻塞;true → false会取消进行中的请求但不清空data;重新启用不会自动重新请求。 |
serialize | boolean | true | 是否把 resolve 后的数据写入 Nuxt payload(__NUXT_DATA__)。为false时服务端取回的数据不进入 payload,水合后若组件渲染了它,客户端会重新请求。可配合惰性水合(lazy hydration)避免水合不一致与多余客户端请求。 |
$fetch | typeof globalThis.$fetch | - | 自定义 $fetch 实现,参见 Nuxt 自定义 useFetch。 |
:::note 所有 fetch 选项都可以传入computed或ref值。它们会被监听,一旦值更新就自动用新值发起新请求(除非watch设为false)。 :::
getCachedData默认实现
const getDefaultCachedData = (key, nuxtApp, ctx) => nuxtApp.isHydrating ? nuxtApp.payload.data[key] : nuxtApp.static.data[key]该默认实现只有在nuxt.config中开启了experimental.payloadExtraction时才会缓存数据。
返回值详解
useFetch返回一个可被 await 的 Promise——因此await后可以直接在<script setup>中使用data(此时必有值而非undefined)。你也可以不去 await 而直接解构,此时在请求完成前data可能为undefined。
:::tip 即便你不 await 返回值,在 SSR 期间 Nuxt 也会等待请求完成,并把 resolve 后的数据发送给客户端。 :::
:::note 如果服务端没有请求数据(例如设置了server: false),那么在水合完成之前不会发起请求。这意味着即使在客户端await useFetch,<script setup>里的data仍然可能是undefined。 :::
| Name | Type | Description |
|---|---|---|
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,并取消任何进行中的请求。 |
:::tip 如果你没有 await 返回值,Promise 的then、catch、finally也可以被安全地解构出来使用。 :::
四种状态取值(Status Values)
idle:请求尚未开始(例如{ immediate: false },或在服务端渲染时{ server: false });pending:请求进行中;success:请求成功完成;error:请求失败。
结合源码理解内部实现
从createUseFetch到useFetch
在 fetch.ts 中,createUseFetch通过defineKeyedFunctionFactory构造,内部返回的useFetch函数完成三个核心步骤:
- 合并默认选项:工厂选项(
options)与调用方选项(opts)按「普通对象:工厂 < 用户;函数模式:用户 < 工厂」的策略合并,随后把server、lazy、default、transform、pick、watch、immediate、getCachedData、deep、dedupe、timeout、enabled、serialize等 AsyncData 层选项单独拆出,其余全部归入fetchOptions传给$fetch(见 fetch.ts); - 构造响应式 URL 与 key;
- 把真正的请求包进
useAsyncData的 handler。
自动 key 是怎么生成的
关键在 fetch.ts:
const _request = computed(() => toValue(request)) const key = computed(() => toValue(fetchOptions.key) || ('$f' + hashKey([autoKey, typeof _request.value === 'string' ? _request.value : '', ...generateOptionSegments(fetchOptions)])))- 前缀
$f区分 fetch 类型的 key; autoKey是编译器在调用点注入的源码位置信息,所以同一调用点的多次实例能共享 key,不同组件不同调用点则不会共享;generateOptionSegments(fetch.ts)会把method、baseURL、query/params、body解包成可哈希片段——例如 body 为ArrayBuffer、FormData或普通对象时分别以不同方式序列化后参与 key 计算,保证不同请求参数对应不同 key。
这也解释了前文警告:useFetch是编译器要识别的保留函数名,编译器依赖源码调用位置来生成autoKey,因此你不应自造同名函数。
另外值得注意:若传入的 URL 以//开头(协议相对地址)且未提供baseURL,会直接抛出错误NUXT_E3001(见 fetch.ts),Nuxt 要求请求使用完整协议。
SSR 阶段用useRequestFetch转发
真正发起请求的 handler 位于 fetch.ts:
const asyncData = useAsyncData<_ResT, ErrorT, DataT, PickKeys, DefaultT>(key, (_, { signal }) => { const _$fetch: TypedFetch<unknown, TypedFetchRequest> = fetchOptions.$fetch || (import.meta.server ? useRequestFetch() : $fetch) ... return _$fetch(_request.value, resolvedOptions as any) as Promise<_ResT> }, _asyncDataOptions)当运行在服务端时,Nuxt 使用useRequestFetch()(见useRequestFetch)作为请求实例,其实现会继承当前请求上下文中的 headers/cookies,让服务端内部请求携带与 SSR 首屏请求一致的会话信息;useFetch文档中那句「基于 server routes 提供请求 URL 的类型提示」即来自TypedFetchRequest/TypedServerResponse类型(types/fetch 相关类型经#build/fetch注入)。
SSR 到 Client 的生命周期:请求、payload、水合
asyncData.ts 中可以看到完整的数据生命周期逻辑:
- 服务端(asyncData.ts):若
fetchOnServer且immediate,立即执行初始请求,并把它注册到onServerPrefetch(组件内)或app:createdhook,确保页面渲染前数据已就绪; - 水合期(asyncData.ts):若服务端已有数据(
key存在于nuxtApp.payload.data或getCachedData命中),则直接复用,不再发起新请求——这正是「不重复请求」的实现基础; - 客户端导航 / 组件挂载(asyncData.ts):根据
server: false、lazy: true、immediate等标志,决定是在onBeforeMount后取数还是同步阻塞等待。
值得留意的默认值归位逻辑(asyncData.ts):server默认true、lazy默认false、immediate默认true、dedupe默认'cancel'、deep默认取自asyncDataDefaults——它们与上文参数表一一对应,构成了useFetch的默认行为。
去重(dedupe)与共享状态容器
所有带 key 的 async data 实体都存放在nuxtApp._asyncData[key]中,不同组件通过同一个 key 读取同一个实体,从而共享状态、避免重复请求(dedupe: 'cancel'时若同一 key 已有请求在途,新触发会取消/等待合并)。这也是useNuxtData能拿到同一份数据、refreshNuxtData能统一刷新的原因。
进阶:用createUseFetch自定义默认行为
如果你需要一个带默认值(如baseURL、认证 headers、统一的transform)的自定义useFetch,官方推荐使用createUseFetch(自 v4.2 引入,源码见 fetch.ts)。它有两种传参模式:
- 传普通对象:作为默认值,调用方的选项可以覆盖工厂默认值(defaults 模式);
- 传函数:工厂返回值会覆盖调用方的选项(override 模式),适合强制某些行为。
完整的类型安全示例见 自定义 useFetch 配方 与createUseFetch文档。
进一步阅读
- 服务端数据获取入门指南:数据获取的完整场景演练;
useAsyncData参考文档:理解useFetch底层的 AsyncData 状态机;useLazyFetch与useNuxtData:非阻塞取数与跨组件共享/读取;$fetch:了解底层请求工具全部能力;useRequestFetch:服务端请求上下文转发机制;- 实现源码:packages/nuxt/src/app/composables/fetch.ts 与 packages/nuxt/src/app/composables/asyncData.ts。
【免费下载链接】nuxtthe full-stack Vue framework项目地址: https://gitcode.com/GitHub_Trending/nu/nuxt
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考