Nuxt 中 `useFetch` 完全指南:SSR 友好的数据请求组合式函数从参数到原理
2026/9/8 20:39:51 网站建设 项目流程

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 中对返回值的描述):

  1. 自动生成请求 key:根据 URL、options 以及调用点在源码中的位置计算出一个稳定且唯一的 key;
  2. 服务端路由类型提示:当你请求server/api目录中定义的路由时,TypeScript 能根据服务端路由自动推导可用的 URL 与 HTTP method;
  3. 自动推断 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 后才被填充;在客户端导航时不阻塞,你需要自己依据返回的statuserrorref 处理加载与错误态。

这与lazy选项产生的效果类似,但lazy才是显式选择非阻塞导航的正式方式。

在 SSR 场景中,无论你是否await,Nuxt 都会等待请求完成后再渲染页面,因此返回的 HTML 始终包含数据。

返回值都是什么形态

注意datastatuserror是 Vue 的ref,在<script setup>中需要以.value访问;而refresh/executeclear普通函数,直接调用即可。

添加 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&param2=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,彼此不会共享状态,会各自发起请求;而同一个组件的多个实例因为使用同一调用点,天然共享状态。

如果你希望跨组件共享同一份dataerrorstatus,需要为每次调用显式传入相同的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,请求选项本身也支持响应式:可以传computedref或计算属性 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(...),又能直接解构出datarefresh等属性。

参数详解

第一参数URL类型为string | Request | Ref<string | Request> | (() => string | Request):可以是字符串、Request对象、Vue ref,或返回字符串/Request的函数,完整支持动态端点的响应式。

第二参数options为对象:扩展了 unjs/ofetch 的 options 与AsyncDataOptions。所有选项既可以是静态值,也可以是ref或 computed 值。

OptionTypeDefaultDescription
keyMaybeRefOrGetter<string>auto-gen用于去重的唯一 key。若未提供,则由 URL、options 与源码中调用点位置生成。
methodMaybeRefOrGetter<string>'GET'HTTP 请求方法。
queryMaybeRefOrGetter<SearchParams>-追加到 URL 的查询/搜索参数。别名:params
paramsMaybeRefOrGetter<SearchParams>-query的别名。
bodyMaybeRefOrGetter<RequestInit['body'] \| Record<string, any>>-请求体。对象会被自动字符串化。
headersMaybeRefOrGetter<Record<string, string> \| [key, value][] \| Headers>-请求头。
baseURLMaybeRefOrGetter<string>-请求的基础 URL。
cachefalse \| string-缓存控制。布尔值false关闭缓存,或使用 Fetch API 值如defaultno-store等。
serverbooleantrue是否在服务端请求。
lazybooleanfalsetrue时在路由加载完成后才 resolve(不阻塞导航)。
immediatebooleantruefalse时阻止请求立即发起。
default() => DataT-在异步 resolve 之前为data提供默认值的工厂函数。
timeoutnumber-请求超时毫秒数(默认undefined,即不超时)。
transform(input: DataT) => DataT \| Promise<DataT>-在结果 resolve 后转换结果的函数。
getCachedData(key, nuxtApp, ctx) => DataT \| undefined-返回缓存数据的函数,默认实现见下文。
pickstring[]-只从结果中挑选指定 key。
watchMultiWatchSources \| false-要监听并自动刷新的响应式源数组;false关闭监听。
deepbooleanfalse是否用深响应 ref 返回数据。默认false(浅响应 ref),性能更好。
dedupe'cancel' \| 'defer''cancel'避免同一 key 同时发起多次请求。
enabledbooleantrue控制请求是否允许执行的「闸门」。为false时,初始请求、execute/refresh、watch 触发的执行全被阻塞;true → false会取消进行中的请求但不清空data;重新启用不会自动重新请求。
serializebooleantrue是否把 resolve 后的数据写入 Nuxt payload(__NUXT_DATA__)。为false时服务端取回的数据不进入 payload,水合后若组件渲染了它,客户端会重新请求。可配合惰性水合(lazy hydration)避免水合不一致与多余客户端请求。
$fetchtypeof globalThis.$fetch-自定义 $fetch 实现,参见 Nuxt 自定义 useFetch。

:::note 所有 fetch 选项都可以传入computedref值。它们会被监听,一旦值更新就自动用新值发起新请求(除非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。 :::

NameTypeDescription
dataRef<DataT \| undefined>异步请求的结果。
refresh(opts?: AsyncDataExecuteOptions) => Promise<void>手动刷新数据的方法。默认情况下 Nuxt 会等上一次refresh完成才允许下一次执行。
execute(opts?: AsyncDataExecuteOptions) => Promise<void>refresh的别名。
errorRef<ErrorT \| undefined>请求失败时的错误对象。
statusRef<'idle' \| 'pending' \| 'success' \| 'error'>数据请求的状态,用于区分四种状态。
pendingRef<boolean>请求进行中为true。配合experimental.pendingWhenIdle,在statusidle且无缓存数据时也为true
clear() => void重置dataundefined(若提供了options.default()则为该值)、errorundefinedstatus设为idle,并取消任何进行中的请求。

:::tip 如果你没有 await 返回值,Promise 的thencatchfinally也可以被安全地解构出来使用。 :::

四种状态取值(Status Values)

  • idle:请求尚未开始(例如{ immediate: false },或在服务端渲染时{ server: false });
  • pending:请求进行中;
  • success:请求成功完成;
  • error:请求失败。

结合源码理解内部实现

createUseFetchuseFetch

在 fetch.ts 中,createUseFetch通过defineKeyedFunctionFactory构造,内部返回的useFetch函数完成三个核心步骤:

  1. 合并默认选项:工厂选项(options)与调用方选项(opts)按「普通对象:工厂 < 用户;函数模式:用户 < 工厂」的策略合并,随后把serverlazydefaulttransformpickwatchimmediategetCachedDatadeepdedupetimeoutenabledserialize等 AsyncData 层选项单独拆出,其余全部归入fetchOptions传给$fetch(见 fetch.ts);
  2. 构造响应式 URL 与 key
  3. 把真正的请求包进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)会把methodbaseURLquery/paramsbody解包成可哈希片段——例如 body 为ArrayBufferFormData或普通对象时分别以不同方式序列化后参与 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):若fetchOnServerimmediate,立即执行初始请求,并把它注册到onServerPrefetch(组件内)或app:createdhook,确保页面渲染前数据已就绪;
  • 水合期(asyncData.ts):若服务端已有数据(key存在于nuxtApp.payload.datagetCachedData命中),则直接复用,不再发起新请求——这正是「不重复请求」的实现基础;
  • 客户端导航 / 组件挂载(asyncData.ts):根据server: falselazy: trueimmediate等标志,决定是在onBeforeMount后取数还是同步阻塞等待。

值得留意的默认值归位逻辑(asyncData.ts):server默认truelazy默认falseimmediate默认truededupe默认'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 状态机;
  • useLazyFetchuseNuxtData:非阻塞取数与跨组件共享/读取;
  • $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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询