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 混合渲染
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。