Nuxt 3 完全指南
Nuxt 3 是基于 Vue 3 的全栈框架,内置 SSR、SSG、文件路由、自动导入等能力。底层使用 Nitro 服务器引擎和 Vite 构建工具。 在 nuxt.config.ts 中配置渲染模式: pages/ 目录下的 .vue 文件自动生成路由,规则如下: 动态路由示例: 捕获所有路由(...slug.vue): 在页面中指定布局: <NuxtLink> 是对 Vue Router <RouterLink> 的封装,增加了预取等能力。 useFetch 是 Nuxt 对 $fetch 的封装,自动处理 SSR 去重、响应式和 TypeScrip
官方文档:https://nuxt.com/docs
适用版本:Nuxt 3.x(2026-05-07 核实)
Nuxt 3 是基于 Vue 3 的全栈框架,内置 SSR、SSG、文件路由、自动导入等能力。底层使用 Nitro 服务器引擎和 Vite 构建工具。
核心概念
渲染模式对比
| 模式 | 全称 | 说明 | 适用场景 |
|---|---|---|---|
| SSR | Server-Side Rendering | 每次请求在服务端渲染 HTML | 动态内容、SEO 要求高 |
| SSG | Static Site Generation | 构建时预渲染所有页面 | 内容不频繁变化的站点 |
| SPA | Single Page Application | 仅客户端渲染,无服务端 HTML | 后台管理、无 SEO 需求 |
| ISR | Incremental Static Regeneration | 静态页面按需/定时重新生成 | 内容定期更新的大型站点 |
| Hybrid | 混合渲染 | 每个路由单独配置渲染策略 | 复杂应用,部分页面需 SSR |
在 nuxt.config.ts 中配置渲染模式:
// nuxt.config.ts
export default defineNuxtConfig({
// 全局 SSR 开关
ssr: true,
// Hybrid 模式:按路由配置
routeRules: {
'/': { prerender: true }, // SSG
'/blog/**': { isr: 3600 }, // ISR,每小时重新生成
'/admin/**': { ssr: false }, // SPA
'/api/**': { cors: true },
},
})
Nuxt 3 目录结构
.
├── app.vue # 根组件(可选,默认使用 NuxtPage)
├── nuxt.config.ts # 框架配置文件
├── pages/ # 文件路由,每个 .vue 文件对应一个路由
├── components/ # 自动导入的组件
├── composables/ # 自动导入的组合式函数(useXxx)
├── utils/ # 自动导入的工具函数
├── layouts/ # 页面布局模板
├── middleware/ # 路由中间件
├── plugins/ # 插件(客户端/服务端)
├── server/ # 服务端代码(Nitro)
│ ├── api/ # API 路由(/api/ 前缀)
│ ├── routes/ # 自定义服务端路由(无前缀)
│ └── middleware/ # 服务端中间件
├── public/ # 静态资源(直接映射到根路径)
└── assets/ # 需要经过构建处理的资源(CSS、图片)
文件路由
路由规则
pages/ 目录下的 .vue 文件自动生成路由,规则如下:
| 文件路径 | 生成的路由 | 说明 |
|---|---|---|
pages/index.vue |
/ |
首页 |
pages/about.vue |
/about |
静态路由 |
pages/blog/index.vue |
/blog |
子目录首页 |
pages/blog/[id].vue |
/blog/:id |
动态路由 |
pages/blog/[...slug].vue |
/blog/:slug(.*)* |
捕获所有(catch-all) |
pages/id.vue |
/:id? |
可选动态路由 |
pages/user-[id].vue |
/user-:id |
路由参数嵌入文件名 |
动态路由示例:
<!-- pages/blog/[id].vue -->
<script setup lang="ts">
const route = useRoute()
// 访问 /blog/123 时,route.params.id === '123'
const { id } = route.params
</script>
<template>
<div>文章 ID:{{ $route.params.id }}</div>
</template>
捕获所有路由([...slug].vue):
<!-- pages/docs/[...slug].vue -->
<script setup lang="ts">
const route = useRoute()
// 访问 /docs/a/b/c 时,route.params.slug === ['a', 'b', 'c']
</script>
NuxtPage 与 NuxtLayout
<!-- app.vue -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
<!-- layouts/default.vue -->
<template>
<div>
<header>公共头部</header>
<slot />
<footer>公共底部</footer>
</div>
</template>
在页面中指定布局:
<!-- pages/admin/index.vue -->
<script setup lang="ts">
definePageMeta({
layout: 'admin', // 使用 layouts/admin.vue
})
</script>
NuxtLink 参数
<NuxtLink> 是对 Vue Router <RouterLink> 的封装,增加了预取等能力。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
to |
string | RouteLocationRaw |
— | 目标路由(必填) |
href |
string |
— | to 的别名 |
target |
string |
— | 同 <a> 的 target(如 _blank) |
rel |
string |
— | 链接的 rel 属性 |
noRel |
boolean |
false |
禁止自动添加 rel="noopener noreferrer" |
prefetch |
boolean |
true |
进入视口时预取页面资源 |
prefetchOn |
'visibility' | 'interaction' |
'visibility' |
预取触发时机 |
noPrefetch |
boolean |
false |
禁用预取 |
activeClass |
string |
'router-link-active' |
路由匹配时的 class |
exactActiveClass |
string |
'router-link-exact-active' |
精确匹配时的 class |
replace |
boolean |
false |
使用 router.replace 而非 push |
ariaCurrentValue |
string |
'page' |
精确匹配时 aria-current 的值 |
external |
boolean |
false |
强制视为外部链接,渲染为 <a> |
<template>
<NuxtLink to="/blog/123" prefetch :prefetch-on="'interaction'">
查看文章
</NuxtLink>
<NuxtLink href="https://example.com" target="_blank" external>
外部链接
</NuxtLink>
</template>
数据获取
useFetch
useFetch 是 Nuxt 对 $fetch 的封装,自动处理 SSR 去重、响应式和 TypeScript 推断。
const { data, pending, error, refresh } = await useFetch('/api/posts')
完整参数表格:
| 参数 | 类型 | 说明 |
|---|---|---|
method |
string |
HTTP 方法(GET、POST 等) |
body |
object | string |
请求体,自动序列化为 JSON |
query |
object |
URL 查询参数(自动拼接) |
params |
object |
query 的别名 |
headers |
object |
请求头 |
baseURL |
string |
基础 URL |
key |
string |
去重键,默认根据 URL 自动生成 |
lazy |
boolean |
true 时不阻塞导航,数据异步填充 |
server |
boolean |
false 时仅在客户端执行 |
immediate |
boolean |
false 时不立即执行,需手动调用 execute() |
default |
() => T |
数据加载前的默认值 |
transform |
(data: T) => U |
对返回数据进行转换 |
pick |
string[] |
只保留返回对象中的指定字段(减少传输量) |
watch |
Ref[] | false |
监听响应式依赖,变化时自动重新请求;false 禁用监听 |
getCachedData |
(key) => data |
自定义缓存策略 |
deep |
boolean |
深度响应式(默认 true) |
// pages/blog/[id].vue
const route = useRoute()
const { data: post, pending } = await useFetch(`/api/posts/${route.params.id}`, {
// 监听路由参数变化自动重新请求
watch: [() => route.params.id],
// 只保留需要的字段
pick: ['title', 'content', 'author'],
// 转换数据格式
transform: (data) => ({
...data,
publishedAt: new Date(data.publishedAt),
}),
})
useAsyncData
useAsyncData 比 useFetch 更底层,适合非 HTTP 数据源(如数据库、文件系统)。
const { data, pending, error, refresh, execute } = await useAsyncData(
'posts-list', // 唯一键(必填)
() => fetchPosts(), // 异步处理函数
options // 可选配置
)
| 参数 | 类型 | 说明 |
|---|---|---|
key |
string |
唯一标识,用于去重和缓存(必填) |
handler |
() => Promise<T> |
数据获取函数(必填) |
lazy |
boolean |
不阻塞导航 |
server |
boolean |
false 仅客户端执行 |
immediate |
boolean |
false 不立即执行 |
default |
() => T |
数据加载前的默认值 |
transform |
(data: T) => U |
转换函数 |
pick |
string[] |
只保留指定字段 |
watch |
Ref[] |
依赖变化时重新执行 |
getCachedData |
(key) => data |
自定义缓存读取逻辑 |
deep |
boolean |
深度响应式 |
$fetch
$fetch 是对 ofetch 的封装,适合在事件处理器或非顶层调用中使用(不会自动去重)。
// 在 setup 顶层之外使用
async function submitForm(data: FormData) {
const result = await $fetch('/api/submit', {
method: 'POST',
body: data,
})
return result
}
客户端 vs 服务端数据获取时机
| 场景 | 行为 |
|---|---|
| SSR 首次请求 | useFetch / useAsyncData 在服务端执行,数据内嵌到 HTML payload |
| 客户端导航 | 数据在客户端重新获取(服务端已有的 key 直接复用) |
server: false |
仅在客户端执行,SSR 时 data 为 null |
lazy: true |
不阻塞路由导航,页面先渲染,数据到达后填充 |
immediate: false |
需手动调用 execute() 或 refresh() 触发 |
Composables 与 Utils
useState
useState 是 SSR 安全的共享状态,服务端渲染的值会自动传递给客户端(hydration)。
// composables/useCounter.ts
export const useCounter = () => useState<number>('counter', () => 0)
// 在任意组件中使用,状态跨组件共享
const counter = useCounter()
counter.value++
useRuntimeConfig
访问 nuxt.config.ts 中定义的环境变量。runtimeConfig.public 中的变量客户端可见,其余仅服务端可见。
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// 仅服务端可访问
dbPassword: process.env.DB_PASSWORD,
// 客户端和服务端均可访问
public: {
apiBase: process.env.API_BASE || 'http://localhost:3000',
},
},
})
// 在组件或 composable 中使用
const config = useRuntimeConfig()
console.log(config.public.apiBase) // 客户端服务端均可
console.log(config.dbPassword) // 仅服务端有值
useRoute / useRouter
const route = useRoute()
const router = useRouter()
// 访问路由信息
console.log(route.path) // '/blog/123'
console.log(route.params.id) // '123'
console.log(route.query.page) // '1'
// 编程式导航
router.push('/blog')
router.push({ name: 'blog-id', params: { id: '456' } })
router.replace('/login')
router.back()
useCookie
SSR 安全的 Cookie 操作,服务端和客户端均可使用。
const token = useCookie('auth-token', options)
token.value = 'new-token' // 自动设置 Cookie
token.value = null // 删除 Cookie
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
string |
— | Cookie 名称(必填) |
maxAge |
number |
— | 有效期(秒) |
expires |
Date |
— | 过期时间 |
httpOnly |
boolean |
false |
禁止 JS 访问 |
secure |
boolean |
false |
仅 HTTPS 传输 |
domain |
string |
— | Cookie 域 |
path |
string |
'/' |
Cookie 路径 |
sameSite |
'strict' | 'lax' | 'none' |
— | SameSite 策略 |
default |
() => T |
— | Cookie 不存在时的默认值 |
readonly |
boolean |
false |
只读,不允许修改 |
encode |
(val) => string |
— | 自定义编码函数 |
decode |
(val) => T |
— | 自定义解码函数 |
服务端 API(server/ 目录)
server/api/ 与 server/routes/ 的区别
| 目录 | URL 前缀 | 适用场景 |
|---|---|---|
server/api/ |
/api/ 自动添加 |
应用内 API,通常配合 useFetch 使用 |
server/routes/ |
无前缀,直接映射 | 自定义路径(如 /sitemap.xml、/rss.xml) |
文件命名规则:server/api/users.[method].ts 只处理对应 HTTP 方法。
// server/api/users.get.ts -> GET /api/users
// server/api/users.post.ts -> POST /api/users
// server/api/users/[id].ts -> 所有方法 /api/users/:id
defineEventHandler
// server/api/posts/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const query = getQuery(event)
const post = await db.posts.findById(id)
if (!post) {
throw createError({ statusCode: 404, statusMessage: 'Post not found' })
}
return post // 自动序列化为 JSON
})
H3 常用工具函数
| 函数 | 说明 |
|---|---|
readBody(event) |
读取并解析请求体(异步) |
readFormData(event) |
读取 FormData |
getQuery(event) |
获取 URL 查询参数对象 |
getRouterParam(event, name) |
获取路由参数 |
getHeader(event, name) |
获取请求头 |
setHeader(event, name, value) |
设置响应头 |
setResponseStatus(event, code) |
设置响应状态码 |
getCookie(event, name) |
获取请求 Cookie |
setCookie(event, name, value, opts) |
设置响应 Cookie |
createError({ statusCode, statusMessage }) |
抛出 HTTP 错误 |
sendRedirect(event, url, code) |
重定向 |
// server/api/login.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { username, password } = body
const user = await authenticate(username, password)
if (!user) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
}
setCookie(event, 'auth-token', user.token, {
httpOnly: true,
maxAge: 60 * 60 * 24 * 7,
})
setResponseStatus(event, 201)
return { success: true, userId: user.id }
})
插件与中间件
plugins/ 插件
插件在 Nuxt 应用初始化时执行,可用于注册全局组件、添加 Vue 插件、提供全局属性。
文件命名约定:
| 文件名 | 执行时机 |
|---|---|
plugins/my-plugin.ts |
客户端和服务端均执行 |
plugins/my-plugin.client.ts |
仅客户端执行 |
plugins/my-plugin.server.ts |
仅服务端执行 |
plugins/01.first.ts |
数字前缀控制执行顺序 |
// plugins/toast.client.ts
import Toast from 'vue-toastification'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(Toast)
// 提供全局方法,通过 useNuxtApp() 访问
return {
provide: {
toast: (msg: string) => Toast.success(msg),
},
}
})
// 在组件中使用
const { $toast } = useNuxtApp()
$toast('操作成功')
middleware/ 路由中间件
路由中间件在客户端路由跳转前执行,可用于权限验证、重定向等。
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const token = useCookie('auth-token')
if (!token.value) {
return navigateTo('/login')
}
})
在页面中应用中间件:
// pages/admin/index.vue
definePageMeta({
middleware: ['auth'], // 具名中间件
// middleware: 'auth', // 单个可以用字符串
})
全局中间件(自动应用到所有路由):
// middleware/log.global.ts (文件名加 .global 后缀)
export default defineNuxtRouteMiddleware((to) => {
console.log(`[导航] 前往 ${to.path}`)
})
server/middleware/ 服务端中间件
服务端中间件对所有进入 Nitro 服务器的请求生效,在 API 路由之前执行。
// server/middleware/cors.ts
export default defineEventHandler((event) => {
setHeader(event, 'Access-Control-Allow-Origin', '*')
setHeader(event, 'Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE')
})
最佳实践
合理使用 lazy 和 server 选项
// 对非关键数据使用 lazy,避免阻塞页面渲染
const { data: comments, pending } = await useFetch('/api/comments', {
lazy: true,
})
// 对仅需客户端数据(如用户个人信息)使用 server: false
const { data: profile } = await useFetch('/api/profile', {
server: false,
})
使用 pick 减少服务端到客户端的数据传输
// 只传输展示所需的字段,不传输数据库完整记录
const { data: posts } = await useFetch('/api/posts', {
pick: ['id', 'title', 'summary', 'publishedAt'],
})
统一处理错误
// composables/useApi.ts
export function useApi() {
const { $fetch } = useNuxtApp()
return {
async get<T>(url: string) {
return $fetch<T>(url).catch((err) => {
if (err.statusCode === 401) {
navigateTo('/login')
}
throw err
})
},
}
}
踩坑与注意事项
SSR 时 window / document 不可用
服务端没有浏览器 API,直接访问会抛出 ReferenceError。
// 错误写法
const width = window.innerWidth // SSR 报错
// 正确写法 1:使用 process.client 判断
if (process.client) {
const width = window.innerWidth
}
// 正确写法 2:在 onMounted 中访问(onMounted 只在客户端执行)
onMounted(() => {
const width = window.innerWidth
})
// 正确写法 3:使用 Nuxt 内置的 useHead 替代直接操作 document
useHead({
title: '页面标题',
})
将仅客户端的代码放入 .client.vue 组件或 .client.ts 插件中,Nuxt 会自动处理。
Hydration Mismatch
Hydration mismatch 指服务端渲染的 HTML 与客户端 Vue 接管时的虚拟 DOM 不一致。常见原因:
| 原因 | 解决方案 |
|---|---|
| 服务端与客户端渲染了不同内容 | 确保初始数据一致,使用 useState 共享状态 |
使用了 Math.random() 或 Date.now() |
使用固定值或仅客户端计算 |
浏览器自动修改 DOM(如 <table> 中插入 <tbody>) |
保证 HTML 结构符合规范 |
| 第三方插件修改了 DOM | 将相关代码移到 onMounted |
使用了 v-if 根据 process.client 判断 |
改用 <ClientOnly> 组件包裹 |
<!-- 使用 ClientOnly 包裹仅客户端内容,避免 hydration mismatch -->
<template>
<ClientOnly>
<BrowserOnlyComponent />
<template #fallback>
<div>加载中...</div>
</template>
</ClientOnly>
</template>
useFetch 的 key 冲突
同一个 key 的请求在 SSR payload 中只保存一份,如果多处使用相同 URL 但不同 options,需要手动指定唯一 key:
// 页面 A 和页面 B 都请求 /api/posts,但参数不同时
const { data } = await useFetch('/api/posts', {
query: { category: 'tech' },
key: 'posts-tech', // 手动指定唯一 key
})
definePageMeta 只能使用静态值
definePageMeta 在编译时静态分析,不能使用运行时变量:
// 错误:不能使用变量
const layoutName = 'admin'
definePageMeta({ layout: layoutName })
// 正确:使用静态字符串
definePageMeta({ layout: 'admin' })
// 如需动态切换 layout,使用 setPageLayout()
const { setPageLayout } = useLayout()
setPageLayout('admin')
常见陷阱
陷阱:useFetch 在客户端导航时重复请求
现象: SSR 页面初次加载正常,客户端路由跳转到同一页面时,useFetch 再次发起相同请求。
原因: Nuxt 的 useFetch 在 SSR 时用 key 将数据序列化到 payload,客户端 hydration 后若 key 未命中(通常是动态参数变化)会重新请求。
解决: 确保 key 包含所有影响请求的变量,或用 getCachedData 实现客户端缓存:
const { data } = await useFetch('/api/user', {
key: `user-${userId}`,
getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key], // 复用 SSR payload
});
陷阱:Server Component 中访问浏览器 API
现象: 在 .server.vue 组件或 server/ 目录的代码中调用 window、document,报 ReferenceError: window is not defined。
原因: Server Component 和 Nitro 服务端代码运行在 Node.js 环境,没有浏览器全局对象。
解决: 浏览器 API 只在 .client.vue 组件或 onMounted/useNuxtApp().hook('page:finish') 回调中使用;使用 import.meta.client 做环境判断。
陷阱:definePageMeta 中使用动态变量无效
现象: 尝试在 definePageMeta 中用响应式变量设置 layout 或 middleware,运行时不生效。
原因: definePageMeta 由 Nuxt 在编译期静态分析,不支持运行时动态值,类似 Vue 的 defineProps。
解决: layout 动态切换用 setPageLayout();middleware 用条件逻辑在中间件内部实现,而非动态传递中间件名。