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> 是对 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 方法(GETPOST 等)
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

useAsyncDatauseFetch 更底层,适合非 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 时 datanull
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/ 目录的代码中调用 windowdocument,报 ReferenceError: window is not defined
原因: Server Component 和 Nitro 服务端代码运行在 Node.js 环境,没有浏览器全局对象。
解决: 浏览器 API 只在 .client.vue 组件或 onMounted/useNuxtApp().hook('page:finish') 回调中使用;使用 import.meta.client 做环境判断。

陷阱:definePageMeta 中使用动态变量无效

现象: 尝试在 definePageMeta 中用响应式变量设置 layoutmiddleware,运行时不生效。
原因: definePageMeta 由 Nuxt 在编译期静态分析,不支持运行时动态值,类似 Vue 的 defineProps
解决: layout 动态切换用 setPageLayout()middleware 用条件逻辑在中间件内部实现,而非动态传递中间件名。


参见

Vue3入门
Vite初级指南
Pinia完全指南

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog