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。

8.7 错误处理与重试

生产环境中,网络请求可能因多种原因失败。Nuxt 提供了统一的错误处理机制,配合 NuxtErrorBoundary 组件可以实现优雅降级。

error 对象结构
useFetch/useAsyncData 返回的 error 是响应式的 Ref<FetchError | null>。FetchError 包含 statusCode(HTTP 状态码)、statusMessagedata(服务端返回的错误详情)。
NuxtErrorBoundary
Nuxt 内置的错误边界组件,捕获子组件的未处理错误,显示备用 UI,防止整个页面崩溃。类似 React 的 ErrorBoundary。
createError()
Nuxt 服务端的错误工厂函数,创建标准格式的 HTTP 错误(包含 statusCode 和 message),可以被客户端的错误处理机制捕获并展示。
<script setup lang="ts">
const { data, error, refresh } = await useFetch('/api/posts')

// 错误类型判断
const errorMessage = computed(() => {
  if (!error.value) return null
  if (error.value.statusCode === 404) return '资源不存在'
  if (error.value.statusCode === 401) return '请先登录'
  return '网络错误,请稍后重试'
})
</script>

<template>
  <!-- NuxtErrorBoundary 捕获子组件的 throw error -->
  <NuxtErrorBoundary>
    <template #error="{ error, clearError }">
      <div class="error-fallback">
        <p>{{ error.message }}</p>
        <button @click="clearError">重试</button>
      </div>
    </template>
    <PostList />
  </NuxtErrorBoundary>

  <!-- useFetch 错误处理 -->
  <div v-if="error" class="error-state">
    <p>{{ errorMessage }}</p>
    <button @click="refresh">重新加载</button>
  </div>
</template>

8.8 服务端 API 路由与数据获取结合

Nuxt 3 的 server/api/ 目录与 useFetch 无缝配合,类型会自动推导——前端知道 API 返回的数据类型,无需额外的类型声明。

// 服务端 API:获取文章列表
export default defineEventHandler(async (event) => {
  const query = getQuery(event)
  const page  = Number(query.page ?? 1)

  // 返回的类型会被 Nuxt 自动推导
  return {
    posts: await db.findPosts({ page }),
    total: await db.countPosts()
  }
})
<script setup lang="ts">
const page = ref(1)

// TypeScript 类型自动推导,无需手动声明
const { data, pending } = await useFetch('/api/posts', {
  query: { page },        // 响应式参数,page 变化时自动重请求
  watch: [page]
})
// data.value 类型为 { posts: Post[], total: number } | null
</script>
常见误区:在客户端直接调用 useFetch 但期望 SSR

useFetch 只有在 <script setup> 的顶层调用时才会在服务端执行(SSR)。如果放在 onMounted 内、事件处理器内、或条件语句中,则只在客户端执行。如果你想手动触发,使用 immediate: falseexecute(),或在事件处理器中使用 $fetch

本章小结

Nuxt 3 的数据获取体系围绕三个 API 构建:useFetch(简洁,适合 HTTP 请求,自动生成缓存 key)、useAsyncData(灵活,支持任意异步函数,需手动指定缓存 key)、$fetch(无 SSR、无缓存,用于用户交互触发的请求)。Nuxt 在 SSR 时自动序列化数据,通过 Payload 注入 HTML,避免客户端重复请求。水合(Hydration)是 SSR 的关键机制:确保服务端和客户端渲染结果一致,才能避免水合不匹配错误。混合渲染(routeRules)让你可以按路由细粒度控制 SSR/SSG/ISR/CSR 策略。