> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Nuxt 3 完全指南
- URL: https://blog.vercanti.com/nuxt-3-wan-quan-zhi-nan/
- Published: 2026-08-28T14:35:25.000Z
- Updated: 2026-08-28T14:58:46.000Z
- Description: Nuxt 3 是基于 Vue 3 的全栈框架，内置 SSR、SSG、文件路由、自动导入等能力。底层使用 Nitro 服务器引擎和 Vite 构建工具。 在 nuxt.config.ts 中配置渲染模式： pages/ 目录下的 .vue 文件自动生成路由，规则如下： 动态路由示例： 捕获所有路由（...slug.vue）： 在页面中指定布局： <NuxtLink> 是对 Vue Router <RouterLink> 的封装，增加了预取等能力。 useFetch 是 Nuxt 对 $fetch 的封装，自动处理 SSR 去重、响应式和 TypeScrip
- Author: yellowdog
- Tags: 前端开发, Vue生态

> 官方文档：<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` 中配置渲染模式：

```typescript
// 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          | 路由参数嵌入文件名       |

动态路由示例：

```vue
<!-- 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`）：

```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

```vue
<!-- app.vue -->
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

```

```vue
<!-- layouts/default.vue -->
<template>
  <div>
    <header>公共头部</header>
    <slot />
    <footer>公共底部</footer>
  </div>
</template>

```

在页面中指定布局：

```vue
<!-- 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>                 |

```vue
<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 推断。

```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）               |

```typescript
// 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 数据源（如数据库、文件系统）。

```typescript
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` 的封装，适合在事件处理器或非顶层调用中使用（不会自动去重）。

```typescript
// 在 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）。

```typescript
// composables/useCounter.ts
export const useCounter = () => useState<number>('counter', () => 0)

// 在任意组件中使用，状态跨组件共享
const counter = useCounter()
counter.value++

```

### useRuntimeConfig

访问 `nuxt.config.ts` 中定义的环境变量。`runtimeConfig.public` 中的变量客户端可见，其余仅服务端可见。

```typescript
// 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

```typescript
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 操作，服务端和客户端均可使用。

```typescript
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 方法。

```typescript
// 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

```typescript
// 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)             | 重定向           |

```typescript
// 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         | 数字前缀控制执行顺序 |

```typescript
// 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/ 路由中间件

路由中间件在客户端路由跳转前执行，可用于权限验证、重定向等。

```typescript
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const token = useCookie('auth-token')

  if (!token.value) {
    return navigateTo('/login')
  }
})

```

在页面中应用中间件：

```typescript
// pages/admin/index.vue
definePageMeta({
  middleware: ['auth'],            // 具名中间件
  // middleware: 'auth',          // 单个可以用字符串
})

```

全局中间件（自动应用到所有路由）：

```typescript
// middleware/log.global.ts  （文件名加 .global 后缀）
export default defineNuxtRouteMiddleware((to) => {
  console.log(`[导航] 前往 ${to.path}`)
})

```

### server/middleware/ 服务端中间件

服务端中间件对所有进入 Nitro 服务器的请求生效，在 API 路由之前执行。

```typescript
// 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 选项

```typescript
// 对非关键数据使用 lazy，避免阻塞页面渲染
const { data: comments, pending } = await useFetch('/api/comments', {
  lazy: true,
})

// 对仅需客户端数据（如用户个人信息）使用 server: false
const { data: profile } = await useFetch('/api/profile', {
  server: false,
})

```

### 使用 pick 减少服务端到客户端的数据传输

```typescript
// 只传输展示所需的字段，不传输数据库完整记录
const { data: posts } = await useFetch('/api/posts', {
  pick: ['id', 'title', 'summary', 'publishedAt'],
})

```

### 统一处理错误

```typescript
// 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`。

```typescript
// 错误写法
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> 组件包裹      |

```vue
<!-- 使用 ClientOnly 包裹仅客户端内容，避免 hydration mismatch -->
<template>
  <ClientOnly>
    <BrowserOnlyComponent />
    <template #fallback>
      <div>加载中...</div>
    </template>
  </ClientOnly>
</template>

```

### useFetch 的 key 冲突

同一个 key 的请求在 SSR payload 中只保存一份，如果多处使用相同 URL 但不同 options，需要手动指定唯一 key：

```typescript
// 页面 A 和页面 B 都请求 /api/posts，但参数不同时
const { data } = await useFetch('/api/posts', {
  query: { category: 'tech' },
  key: 'posts-tech',  // 手动指定唯一 key
})

```

### definePageMeta 只能使用静态值

`definePageMeta` 在编译时静态分析，不能使用运行时变量：

```typescript
// 错误：不能使用变量
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` 实现客户端缓存：

```ts
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` 用条件逻辑在中间件内部实现，而非动态传递中间件名。

---

## 参见

[Vue3入门](https://blog.vercanti.com/vue-3-ru-men-zhi-nan/)  
[Vite初级指南](https://blog.vercanti.com/vite-chu-ji-zhi-nan/)  
[Pinia完全指南](https://blog.vercanti.com/pinia-wan-quan-zhi-nan/)