8.1 四种渲染模式对比
Nuxt 3 支持多种渲染模式,每种模式适用于不同场景。理解它们的工作原理对选择正确方案至关重要:
| 模式 | 全称 | HTML 生成时机 | 适用场景 |
|---|---|---|---|
| SSR | Server-Side Rendering | 每次请求时服务器实时生成 | 动态内容、需要登录态的页面、实时数据 |
| SSG | Static Site Generation | 构建时生成,部署静态文件 | 博客、文档、营销页面(内容不常变化) |
| ISR | Incremental Static Regeneration | 构建时生成 + 定时重新生成 | 电商产品页、新闻(需要定时更新) |
| CSR | Client-Side Rendering | 浏览器运行 JS 后生成 | 后台管理、数据实时变化、无 SEO 需求 |
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 的响应式系统的过程。
<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>
Nuxt 会根据 URL 和选项自动生成缓存 key。在同一次请求中,多个组件调用相同 URL 的 useFetch 只会发出一次 HTTP 请求。如果需要强制不同缓存,在 useFetch 的选项中传入 key 参数覆盖默认 key。
8.7 错误处理与重试
生产环境中,网络请求可能因多种原因失败。Nuxt 提供了统一的错误处理机制,配合 NuxtErrorBoundary 组件可以实现优雅降级。
error 是响应式的 Ref<FetchError | null>。FetchError 包含 statusCode(HTTP 状态码)、statusMessage、data(服务端返回的错误详情)。<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 只有在 <script setup> 的顶层调用时才会在服务端执行(SSR)。如果放在 onMounted 内、事件处理器内、或条件语句中,则只在客户端执行。如果你想手动触发,使用 immediate: false 加 execute(),或在事件处理器中使用 $fetch。
Nuxt 3 的数据获取体系围绕三个 API 构建:useFetch(简洁,适合 HTTP 请求,自动生成缓存 key)、useAsyncData(灵活,支持任意异步函数,需手动指定缓存 key)、$fetch(无 SSR、无缓存,用于用户交互触发的请求)。Nuxt 在 SSR 时自动序列化数据,通过 Payload 注入 HTML,避免客户端重复请求。水合(Hydration)是 SSR 的关键机制:确保服务端和客户端渲染结果一致,才能避免水合不匹配错误。混合渲染(routeRules)让你可以按路由细粒度控制 SSR/SSG/ISR/CSR 策略。