Nuxt 中的服务端数据获取
如果你正在构建一个 Nuxt 应用,并且疑惑为什么数据会被获取两次——或者为什么共享相同 key 的组件会出现意外行为——你并不孤单。Nuxt SSR 数据获取有一些特定的规则,即使是经验丰富的开发者也容易踩坑。
本文将解释 useAsyncData 和 useFetch 在 Nuxt 4 中的工作原理,涵盖 payload 水合(hydration)、导航行为、key 管理,以及最容易引起混淆的陷阱。
核心要点
- Nuxt 在服务端运行
useFetch和useAsyncData,将响应序列化到 HTML payload 中,并在客户端进行水合而无需重新获取 - 共享相同 key 的组件会共享相同的响应式状态——为独立的数据实例使用不同的 key
- 在当前的 Nuxt 4 版本中,某些选项(handler、transform、pick、getCachedData、default、deep)在共享 key 的调用之间必须匹配
- 使用
useFetch或useAsyncData进行 SSR 安全的数据获取;将$fetch保留给事件处理器和仅客户端代码
Nuxt 如何执行服务端数据获取
当你在页面或组件中调用 useFetch 或 useAsyncData 时,Nuxt 会在初始请求期间在服务端运行该获取操作。服务器将响应序列化为嵌入在 HTML 中的 payload。当客户端进行水合时,它会读取这个 payload 而不是重新获取——从而消除重复的网络请求。
const { data } = await useFetch('/api/products')
这一行代码在服务端执行,将结果嵌入页面,并在客户端无缝水合。没有双重获取,没有水合不匹配。
阻塞式获取 vs. 懒加载获取
默认情况下,Nuxt 会阻塞导航,直到 awaited 的数据获取完成。这确保你的页面在渲染时数据已经可用。
对于客户端导航,你可以选择懒加载获取:
const { data, status } = useLazyFetch('/api/comments')
使用懒加载获取时,导航默认不会阻塞,获取操作在后台运行。你需要在模板中使用 status ref 来处理加载状态。
理解 Nuxt 数据获取的 Key
Key 是 Nuxt 缓存和去重请求的核心机制。每个 useFetch 调用使用 URL 作为其默认 key。对于 useAsyncData,你需要显式提供 key,或让 Nuxt 为你生成一个确定性的 key。
这里是关键部分:共享相同 key 的组件会共享相同的状态。这包括 data、error、status 和 pending refs。
// 组件 A
const { data } = await useAsyncData('users', () => $fetch('/api/users'))
// 组件 B - 与组件 A 共享状态
const { data } = await useAsyncData('users', () => $fetch('/api/users'))
两个组件接收相同的响应式 refs。改变一个,另一个也会反映出来。
Key 一致性规则
在当前的 Nuxt 4 版本中,当多个调用共享一个 key 时,Nuxt 会强制某些选项保持一致。这些选项必须匹配:
- Handler 函数
transform函数pick数组getCachedData函数default值deep选项
这些选项可以安全地不同:
serverlazyimmediatededupewatch
违反一致性会触发开发警告和不可预测的行为。
Discover how at OpenReplay.com.
安全的 Key 策略
对于路由特定的数据,在 key 中包含路由参数:
const route = useRoute()
const { data } = await useAsyncData(
`product-${route.params.id}`,
() => $fetch(`/api/products/${route.params.id}`)
)
对于不应共享状态的独立实例,使用不同的 key:
const { data: sidebar } = await useAsyncData('users-sidebar', fetchUsers)
const { data: main } = await useAsyncData('users-main', fetchUsers)
Nuxt 数据缓存和去重行为
Nuxt 会自动去重具有匹配 key 的并发请求。如果三个组件同时请求相同的 key,只会触发一个网络请求。
dedupe 选项控制刷新行为:
const { data, refresh } = await useFetch('/api/data', {
dedupe: 'cancel' // 在启动新请求之前取消待处理的请求
})
在 Nuxt 4.2 及更高版本中,取消支持得到了显著改进。当支持时,在快速导航期间,来自先前路由的过时响应可以被取消或忽略,减少过时数据短暂出现的风险。
更多详情:https://nuxt.com/docs/api/composables/use-fetch
常见陷阱
混淆 Nuxt 的 useFetch 与其他库
Nuxt 的 useFetch 不同于 @vueuse/core 的 useFetch 或类似工具。Nuxt 的版本会自动处理 SSR payload 水合。使用其他库的 useFetch 会完全绕过这一机制,导致双重获取和水合不匹配。
在 Setup 中不使用 useAsyncData 而直接使用 $fetch
在 <script setup> 中直接调用 $fetch 会在服务端和客户端都运行:
// ❌ 获取两次
const data = await $fetch('/api/users')
// ✅ 获取一次,正确水合
const { data } = await useFetch('/api/users')
将 $fetch 保留给事件处理器和仅客户端交互。
使用冲突选项复用 Key
这会触发警告和 bug:
// ❌ deep 选项冲突
await useAsyncData('users', fetchUsers, { deep: false })
await useAsyncData('users', fetchUsers, { deep: true })
在 server: false 时期望水合前有数据
当你设置 server: false 时,数据在水合完成之前会保持 null——即使你 await 了该组合式函数。
结论
Nuxt 4 的数据获取模型以服务端执行、payload 水合和基于 key 的缓存为中心。保持 key 稳定且每个数据源唯一。在组件之间共享 key 时确保选项一致性。使用 useFetch 或 useAsyncData 进行 SSR 安全的获取,将 $fetch 保留给客户端交互。
掌握这些模式,你就能避免困扰大多数 Nuxt 开发者的双重获取和状态共享 bug。
常见问题
这通常发生在你在 script setup 中直接使用 $fetch 而不是 useFetch 或 useAsyncData 时。直接的 $fetch 调用会在服务端和客户端都运行。将你的获取操作包装在 useFetch 或 useAsyncData 中以利用 payload 水合,这样只在服务端获取一次并在客户端复用该数据。
当直接从 URL 获取数据时使用 useFetch——它会根据 URL 自动处理 key。当你需要自定义逻辑、组合多个 API 调用或显式控制缓存 key 时使用 useAsyncData。两者都提供相同的 SSR 水合优势。
共享相同 key 的组件会共享相同的响应式状态。要保持数据独立,为每个组件使用唯一的 key。例如,当两个组件获取相同的端点但需要独立状态时,使用 users-sidebar 和 users-main 而不是只用 users。
dedupe 选项控制 Nuxt 如何处理多个刷新调用。将 dedupe 设置为 cancel 会在启动新请求之前中止任何待处理的请求。这有助于避免快速用户交互期间的竞态条件,并确保在支持取消时较新的响应优先。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.