Chapter 08

Nuxt 数据获取与渲染模式

深入理解 SSR、SSG、ISR、CSR 的核心差异,掌握 useFetch/useAsyncData 的正确用法,以及状态水合的原理

8.1 四种渲染模式对比

Nuxt 3 支持多种渲染模式,每种模式适用于不同场景。理解它们的工作原理对选择正确方案至关重要:

模式全称HTML 生成时机适用场景
SSRServer-Side Rendering每次请求时服务器实时生成动态内容、需要登录态的页面、实时数据
SSGStatic Site Generation构建时生成,部署静态文件博客、文档、营销页面(内容不常变化)
ISRIncremental Static Regeneration构建时生成 + 定时重新生成电商产品页、新闻(需要定时更新)
CSRClient-Side Rendering浏览器运行 JS 后生成后台管理、数据实时变化、无 SEO 需求
Nuxt 3 混合渲染

Nuxt 3 最强大的特性之一是路由级别的渲染模式:你可以为不同路由设置不同的渲染模式。例如首页用 SSG,产品列表用 ISR(1小时更新),用户中心用 CSR(不需要 SEO)。

export default defineNuxtConfig({
  routeRules: {
    '/':           { prerender: true },           // SSG 预渲染
    '/blog/**':    { isr: 3600 },               // ISR:1小时重新生成
    '/dashboard/**': { ssr: false },            // CSR:不使用 SSR
    '/api/**':     { cors: true, headers: { 'cache-control': 's-maxage=60' } }
  }
})

8.2 useFetch:最常用的数据获取方式

useFetch 是 Nuxt 3 内置的数据获取 Composable,在服务器端执行请求,并将结果序列化后传到客户端(避免重复请求):

<script setup lang="ts">
// 基础用法
const { data, pending, error, refresh } = await useFetch('/api/users')

// 携带查询参数(响应式)
const page = ref(1)
const { data: posts } = await useFetch('/api/posts', {
  query: { page, limit: 10 },     // page 是 ref,自动响应变化
  watch: [page],                    // 监听 page 变化自动重新请求
  pick: ['id', 'title', 'createdAt'], // 只选取部分字段
})

// POST 请求
const { data: newPost, execute: createPost } = await useFetch('/api/posts', {
  method: 'POST',
  body: { title: '新文章', content: '...' },
  immediate: false  // 不立即执行
})

// 转换响应数据
const { data: userNames } = await useFetch('/api/users', {
  transform(users) {
    return users.map(u => u.name)  // 只需要名字数组
  }
})
</script>

<template>
  <div v-if="pending">加载中...</div>
  <div v-else-if="error">错误:{{ error.message }}</div>
  <ul v-else>
    <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
  </ul>
  <button @click="refresh()">刷新</button>
</template>

8.3 useAsyncData:更细粒度的控制

当需要使用非 HTTP 的数据源(如数据库查询、文件系统)或需要更复杂的缓存控制时,使用 useAsyncData

<script setup lang="ts">
// useAsyncData 第一个参数是缓存 key(跨组件共享)
const { data: users } = await useAsyncData('users', () =>
  $fetch('/api/users')
)

// 动态 key(路由参数不同时创建不同缓存)
const route = useRoute()
const { data: post } = await useAsyncData(
  `post-${route.params.id}`,
  () => $fetch(`/api/posts/${route.params.id}`)
)

// 服务端专有数据(使用 server/ 工具函数)
const { data: dbData } = await useAsyncData('db-data', async () => {
  // 这段代码只在服务器运行,可以直接访问数据库
  if (process.server) {
    const db = useDatabase()
    return db.find({ active: true })
  }
})
</script>

8.4 useFetch vs useAsyncData vs $fetch

API底层适用场景特点
useFetch(url)useAsyncData + $fetch最常用,请求 HTTP API简洁,自动生成缓存 key
useAsyncData(key, fn)原生需要自定义缓存 key 或非 HTTP 数据源灵活,可以用任何异步函数
$fetch(url)ofetch 库在事件处理器中手动请求(不在 setup 顶层)无缓存,适合用户操作触发的请求
<script setup>
// ✅ 在 setup 顶层:用 useFetch(SSR + 缓存)
const { data: posts } = await useFetch('/api/posts')

// ✅ 在事件处理器中:用 $fetch(无 SSR,无缓存)
async function deletePost(id: number) {
  await $fetch(`/api/posts/${id}`, { method: 'DELETE' })
  await refresh()  // 手动刷新列表
}
</script>

8.5 状态水合(Hydration)

水合是 SSR 中的关键概念:服务器生成 HTML 并传给浏览器,浏览器接管 HTML 并激活 Vue 的响应式系统的过程。

为什么需要水合
服务器只输出静态 HTML,没有事件监听和响应式能力。浏览器收到 HTML 后,需要将 Vue 实例"挂载"到现有 DOM 上,而不是重新创建 DOM——这个过程就是水合。
Payload 序列化
Nuxt 在 SSR 时会将 useFetch/useAsyncData 的结果序列化嵌入 HTML(作为 <script> 标签中的 JSON)。浏览器水合时直接使用这些数据,不需要重新发请求。
水合不匹配
如果服务端渲染的 HTML 与客户端 Vue 生成的虚拟 DOM 不匹配,会触发水合错误(hydration mismatch)。常见原因是在模板中使用了仅浏览器可用的 API(如 window、localStorage)。
<script setup>
// ❌ 水合错误:服务端没有 window,渲染结果不同
const width = ref(window?.innerWidth)  // 服务端 window 是 undefined

// ✅ 方式1:使用 ClientOnly 组件包裹
// <ClientOnly><BrowserOnlyWidget /></ClientOnly>

// ✅ 方式2:在 onMounted 后才访问浏览器 API
const width = ref(0)
onMounted(() => { width.value = window.innerWidth })

// ✅ 方式3:Nuxt 的 useNuxtApp
const nuxtApp = useNuxtApp()
if (nuxtApp.$isClient) {
  // 只在客户端执行
}
</script>

8.6 Cookie 与状态共享

<script setup>
// useCookie:SSR/客户端通用的 Cookie 操作(自动同步)
const token = useCookie('auth-token', {
  maxAge: 60 * 60 * 24 * 7,   // 7天(秒)
  httpOnly: true,              // 仅 HTTP,JS 无法访问
  secure: true,               // 仅 HTTPS
  sameSite: 'lax'
})

// useState:跨组件共享 SSR 状态(比 ref 更适合 SSR)
// key 唯一标识,相同 key 的组件共享同一状态
const counter = useState('counter', () => 0)
counter.value++
</script>
useFetch 的 key 去重

Nuxt 会根据 URL 和选项自动生成缓存 key。在同一次请求中,多个组件调用相同 URL 的 useFetch 只会发出一次 HTTP 请求。如果需要强制不同缓存,在 useFetch 的选项中传入 key 参数覆盖默认 key。