Nuxt $fetch 全解析:全局 HTTP 请求助手与 SSR 下的数据安全实践
$fetch 是 Nuxt 基于 ofetch 暴露的全局 HTTP 请求助手,可在 Vue 应用、组合式函数与 API 路由中直接使用。它解决了全栈框架在浏览器与服务器两端发送请求的统一性问题,同时需要开发者理解 SSR(服务端渲染)阶段"数据是否会被请求两次""请求头与 Cookie 是否被转发"等关键差异。读完本文,你将掌握 $fetch 的底层实现、与 useAsyncData/useFetch 的正确搭配方式,以及如何安全地在服务端透传用户的 Headers 与 Cookie。
$fetch 是什么:Nuxt 如何把它变成全局能力
在 Nuxt 应用中,$fetch 并非一个手写封装,而是构建阶段由模板注入的全局对象。其核心代码位于 packages/nuxt/src/core/templates.ts,Nuxt 会生成若干运行时模块把 ofetch 挂到应用上:
- 服务端模板
fetch.server.mjs(见 templates.ts#L609-L627):若globalThis.$fetch尚未存在,则用createFetch({ fetch, defaults: { baseURL: baseURL() } })创建,其中fetch取自当前 server builder 提供的运行时实现(见useServerBuild(nuxt).runtime.fetch),baseURL()来自#internal/nuxt/paths,会把应用配置的baseURL一并带入默认值; - 客户端模板
fetch.client.mjs(见 templates.ts#L629-L639):直接_$fetch.create({ baseURL: baseURL() })导出$fetch,并标注/*#__PURE__*/便于摇树; - 类型模板
fetch.d.ts(见 templates.ts#L657-L663):声明export declare const $fetch: import('nuxt/app').TypedFetch,让$fetch具备类型化请求能力; - 为了确保模块求值顺序正确,服务端通过
fetch-setup.server.mjs(templates.ts#L645-L649)以副作用导入的方式在应用入口最先加载#build/fetch,先于其他模块"播种"好globalThis.$fetch;客户端因$fetch是自动导入项,该模块保持为空,避免把 ofetch 拉进从不使用它的打包产物(参见 templates.ts#L641-L644 的注释说明)。
从类型层面看,TypedFetchRequest 由服务端路由推断而来(见 packages/nuxt/src/app/types/fetch.ts#L13),配合路由类型生成后,$fetch('/api/xxx') 的 URL 与返回类型都能得到 IDE 校验。
SSR 中的关键优化:请求内部 API 路由时直接调用处理函数
::tip
在服务端渲染阶段,用 $fetch 请求应用内部的 API 路由 时,Nuxt 会直接调用对应路由函数(模拟一次请求),从而省掉一次额外的网络 API 调用。
::
这一机制在仓库中表现为:Nitro 服务端把相对路径的内部请求通过 serverFetch 重新派发到对应 handler,而不是真的发起一次 HTTP 往返。例如 packages/nitro-server/src/runtime/middleware/base-url.ts 与 packages/nitro-server/src/runtime/handlers/error.ts 都直接 import { serverFetch } from 'nitro' 做内部请求,且 packages/nitro-server/src/index.ts#L117-L127 注释也说明"内部重新派发的请求(internally re-dispatched request)"需要路由保持与 strip 后路径一致(启用 experimental.runtimeBaseURL 时会注册 base-url 中间件做该处理)。因此在 SSR 中请求自己的 /api/** 接口,不会产生对外网络开销。
服务端渲染会重复请求数据吗:与 useAsyncData/useFetch 的关系
::note
在组件中直接使用 $fetch 而不用 useAsyncData 包裹,会造成数据被请求两次:先是在服务端,然后在客户端 hydration 时再次请求。原因在于 $fetch 不会把状态从服务端传递到客户端,客户端必须自己再取一次数据。
::
官方推荐用 useFetch 或 useAsyncData + $fetch 来避免组件数据的双重获取,例如:
<script setup lang="ts">
// 不推荐:SSR 期间数据会在服务端、客户端各请求一次
const dataTwice = await $fetch('/api/item')
// 推荐:SSR 期间只在服务端获取一次,再通过 payload 传给客户端
const { data } = await useAsyncData('item', () => $fetch('/api/item'))
// 同样推荐:useFetch 是 useAsyncData + $fetch 的快捷方式
const { data } = await useFetch('/api/item')
</script>
useFetch 之所以能避免二次请求,是因为它内部把数据装载进了 useAsyncData 的 payload 序列化机制。可以看 packages/nuxt/src/app/composables/fetch.ts#L370-L381:真正的数据获取发生在 useAsyncData(key, ...) 的回调内,即"SSR 时只跑在服务端、结果随 HTML payload 传给客户端"。此外该文件还能看到几个值得注意的实现细节:
- 服务端实际执行请求的是
useRequestFetch(),客户端才是裸$fetch(fetch.ts#L371),这正是"SSR 阶段自动透传请求头"的实现入口(下文详解); - 支持大写 HTTP 方法(见 fetch.ts#L19-L20 的注释与类型定义),
method也允许是 ref; - 缓存键由
'$f' + hashKey([...])基于 URL、method、baseURL、query/params、body等分段生成(fetch.ts#L62-L104、fetch.ts#L323),其中body会对响应式对象做统一序列化处理; - 若请求的是
//开头的协议相对 URL 且未提供baseURL,会抛出NUXT_E3001诊断错误(fetch.ts#L325-L327),防止误把内部请求打到外部地址。
更多数据获取的最佳实践见 数据获取指南。
纯客户端场景:直接在事件回调中使用 $fetch
$fetch 可以放心地用在任何仅在客户端执行的方法里,例如表单提交:
<script setup lang="ts">
async function contactForm () {
await $fetch('/api/contact', {
method: 'POST',
body: { hello: 'world' },
})
}
</script>
<template>
<button @click="contactForm">
Contact
</button>
</template>
这里没有 SSR 的重复请求与透传问题——点击事件发生在浏览器端,请求由浏览器直接发出,cookie 等用户请求头会由浏览器自动附带。
$fetch 与 Nuxt 2 时代的 HTTP 方案
$fetch 是 Nuxt 发起 HTTP 调用的首选方式,取代了面向 Nuxt 2 的 [@nuxt/http] 与 [@nuxtjs/axios]。它基于 Promise、零配置支持 JSON 序列化/解析、统一错误处理,并天然与 Nuxt 的类型化路由与 SSR 内部请求机制配合,迁移到 Nuxt 3/4 的项目应逐步用 $fetch 替换上述模块。
开发期调用自签名 HTTPS 外部接口
::note
如果在开发环境用 $fetch 请求带自签名证书的(外部)HTTPS URL,需要在环境变量中设置 NODE_TLS_REJECT_UNAUTHORIZED=0。
::
注意这仅用于本地联调(例如调用尚在内网、使用自签证书的测试服务),它关闭了 Node.js 对 TLS 证书合法性的校验,生产环境不应开启。因为该证书校验发生在 Node 服务端进程内,浏览器端的 fetch 行为不受此环境变量影响。
在 SSR 中透传 Headers 与 Cookie:安全模型与显式转发
当 $fetch 在浏览器中执行时,用户请求头(如 cookie)会由浏览器直接随请求发给 API。
但在服务端渲染期间,出于 SSRF(服务端请求伪造) 与 身份认证滥用 等安全风险考虑,$fetch 既不会带上用户浏览器的 Cookie,也不会转发来自 fetch 响应的 Set-Cookie:
<script setup lang="ts">
// 在 SSR 期间,这不会把用户请求头与 Cookie 转发给 /api/cookies
const { data } = await useAsyncData(() => $fetch('/api/cookies'))
</script>
export default defineEventHandler((event) => {
const foo = getCookie(event, 'foo')
// ... Do something with the cookie
})
服务端如果需要把请求头和 Cookie 继续透传给内部 API,必须手动转发,标准做法是使用 useRequestFetch:
<script setup lang="ts">
// 这会把用户的请求头与 Cookie 转发给 /api/cookies
const requestFetch = useRequestFetch()
const { data } = await useAsyncData(() => requestFetch('/api/cookies'))
</script>
此外,当在服务端用相对 URL 调用 useFetch 时,Nuxt 会自动改用 useRequestFetch 代理请求头与 Cookie(排除掉 host 等不应转发的头)。
useRequestFetch 的底层实现:白名单式的安全转发
为什么"手动转发"能保证安全?关键在于它是按"明确白名单"进行代理的。看 packages/nuxt/src/app/composables/ssr.ts 的实现:
- 客户端直接返回全局
$fetch(ssr.ts#L100); - 服务端则通过
createRequestFetch(event)创建一个 per-request 的包装 fetch(结果缓存在WeakMap<RequestEvent, TypedFetch>中,见 ssr.ts#L76-L96)。只有当请求字符串以/开头(相对内部 URL)时,才会把event.req的请求头合并进来,并且跳过UNFORWARDED_HEADERS中列出的头(ssr.ts#L52-L74)。
被排除的 UNFORWARDED_HEADERS 就是那些逐跳(hop-by-hop)或描述请求体的头,包括 accept、accept-encoding、connection、content-length、content-md5、content-type、expect、host、条件请求头(if-match、if-modified-since、if-none-match 等)、keep-alive、proxy-authenticate、proxy-authorization、te、trailer、transfer-encoding、upgrade。将代理逻辑以底层 fetch 的形式注入而非使用 onRequest 钩子或默认 headers(见 ssr.ts#L80-L82 的注释),是为了让用户提供的钩子与绝对 URL 都无法绕过这套检查。
各场景请求头转发行为速查
| 场景 | 请求位置 | 用户 Cookie / Headers 是否转发 |
|---|---|---|
$fetch('/api/...') |
浏览器 | ✅ 由浏览器自动附带 |
$fetch('/api/...') 或 useAsyncData(() => $fetch(...)) |
服务端 | ❌ 出于 SSRF / 认证安全默认不透传 |
useRequestFetch()('/api/...') 包裹在 useAsyncData 中 |
服务端 | ✅ 显式转发(host 等除外) |
useFetch('/api/...') |
服务端 | ✅ 内部自动走 useRequestFetch 代理 |
仅在客户端执行的 $fetch(如点击事件内) |
浏览器 | ✅ 正常附带 |
总结
$fetch 是 Nuxt 数据请求的统一入口:在浏览器端它就是带 baseURL 默认值的 ofetch 实例;在服务端则被构建模板注入为 globalThis.$fetch,并能对内部 API 路由做免网络的直接调用。使用时有两条铁律需要牢记——在组件中请求数据时用 useFetch 或 useAsyncData + $fetch,避免 SSR 二次请求;在服务端需要透传用户身份信息(Cookie/Headers)时使用 useRequestFetch,而不是期望裸 $fetch 替你转发。理解这两点,就能在全栈开发中写出既高效又安全的接口调用代码。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00