Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

Vue 生态 / Vue Ecosystem

1. Vue 生态知识体系 / Vue Ecosystem Knowledge System

2. Vue 组件、模板与编译原理 / Vue Components, Templates, and Compilation

3. Vue 3 响应式系统与 Composition API / Vue 3 Reactivity System and Composition API

4. Vue Router、表单与组件架构 / Vue Router, Forms, and Component Architecture

5. Pinia 状态管理与持久化 / Pinia State Management and Persistence

6. Nuxt 路由、渲染与项目结构 / Nuxt Routing, Rendering, and Project Structure

7. Nuxt 数据获取、Server API 与缓存 / Nuxt Data Fetching, Server APIs, and Caching

8. Vue 测试、性能与生产工程 / Vue Testing, Performance, and Production Engineering

本页目录

Nuxt 路由、渲染与项目结构 / Nuxt Routing, Rendering, and Project Structure ​

📅 创建时间:2026-07-28 🏷️ 标签:#Nuxt #Routing #SSR #Nitro #Layers #Middleware 📚 前置知识:[[./03-router-forms-and-component-architecture]] 📚 后续知识:[[./06-nuxt-data-server-cache]]


📋 本章目标 ​

  • 掌握 Nuxt 4 文件路由约定的完整语法(动态路由、嵌套路由、路由分组、catch-all)
  • 理解 routeRules 配置对路由行为的精确控制(重定向、ISR、代理等)
  • 理解 server/ 目录结构以及 Nitro 引擎的跨平台部署抽象
  • 掌握 Nitro 缓存层(cachedEventHandler、SWR)的配置与适用场景
  • 建立 useFetch、useAsyncData、$fetch 的场景决策树
  • 理解 SSR、SSG、CSR、SWR 四种渲染模式的适用场景与 routeRules 配置
  • 掌握 Nuxt Layers 的概念、引入方式与多项目复用场景
  • 理解路由中间件的分类、执行顺序与典型应用(认证守卫、重定向)

第1部分:Nuxt 4 文件路由约定 ​

1.1 pages/ 目录即路由表 ​

Nuxt 基于文件系统自动生成 Vue Router 配置。pages/ 目录下的每一个 .vue 文件都对应一个路由——不需要手写路由配置。这是 Nuxt 最核心的"约定优于配置"设计。

┌─────────────────────────────────────────────────────────────┐
│                Nuxt 4 文件路由约定全景                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  pages/                                                     │
│  ├── index.vue                    → /                       │
│  ├── about.vue                    → /about                  │
│  ├── projects/                                              │
│  │   ├── index.vue                → /projects               │
│  │   ├── [id].vue                 → /projects/:id           │
│  │   ├── [id]/                                              │
│  │   │   └── settings.vue         → /projects/:id/settings  │
│  │   └── new.vue                  → /projects/new           │
│  ├── blog/                                                 │
│  │   ├── [category]/                                        │
│  │   │   └── [slug].vue          → /blog/:category/:slug   │
│  │   └── [...slug].vue           → /blog/* (catch-all)     │
│  ├── (auth)/                                                │
│  │   ├── login.vue               → /login (group stripped) │
│  │   └── register.vue            → /register               │
│  └── (dashboard)/                                           │
│      ├── analytics.vue           → /analytics              │
│      └── settings.vue            → /settings               │
│                                                             │
│  核心原则:目录结构 = 路由结构,文件名 = 路由参数             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

1.2 动态路由:[param].vue 与 [...slug].vue ​

动态路由用方括号包裹参数名。参数值通过 useRoute().params 获取:

vue
<!-- pages/projects/[id].vue -->
<script setup lang="ts">
const route = useRoute()

// route.params.id 的类型是 string | string[]
// 访问 /projects/42 → route.params.id === '42'
const projectId = computed(() => String(route.params.id))
</script>

<template>
  <div>
    <h1>Project {{ projectId }}</h1>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

可选参数使用双括号 [[param]]:

text
pages/
├── [lang]/                  # 必须有 lang 段
│   └── index.vue
└── [[lang]]/                # lang 段可选,/ 或 /zh 都匹配
    └── index.vue
1
2
3
4
5

Catch-all 路由使用 [...slug].vue 匹配任意深度的路径段:

vue
<!-- pages/docs/[...slug].vue -->
<script setup lang="ts">
const route = useRoute()
// 访问 /docs/guide/nuxt/routing
// route.params.slug → ['guide', 'nuxt', 'routing']
</script>
1
2
3
4
5
6

1.3 路由分组:(group)/ 目录 ​

括号包裹的目录名不会出现在 URL 中,仅用于组织文件:

┌─────────────────────────────────────────────────────────────┐
│                    路由分组的工作原理                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  文件结构                    →  生成的 URL                    │
│  ─────────────────────────────────────────                   │
│  pages/                                                     │
│  ├── (public)/                                              │
│  │   ├── index.vue           →  /                           │
│  │   ├── about.vue           →  /about                      │
│  │   └── blog/                                              │
│  │       └── [slug].vue      →  /blog/:slug                 │
│  ├── (auth)/                                                │
│  │   ├── login.vue           →  /login                      │
│  │   └── register.vue        →  /register                   │
│  └── (dashboard)/                                           │
│      ├── analytics.vue       →  /analytics                  │
│      └── users/                                             │
│          └── [id].vue         →  /users/:id                  │
│                                                             │
│  每个分组可以拥有独立的 layout、middleware                   │
│  同组页面共享分组级的 app.vue 或 layout 配置                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

路由分组的典型用途:

  • 布局隔离:(public) 组使用营销布局,(dashboard) 组使用后台布局
  • 中间件作用域:(auth) 组统一挂载认证中间件
  • 代码组织:大型项目按领域拆分 pages 子目录

1.4 routeRules:声明式路由行为配置 ​

Nuxt 4 通过 nuxt.config.ts 中的 routeRules 对单个路由或路由模式进行精细化控制:

ts
// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // 直接指定路由路径
    '/': { prerender: true },
    '/blog/**': { swr: 3600 },
    '/admin/**': { ssr: false },

    // 使用路由名称匹配(推荐,重构安全)
    '/projects/[id]': {
      ssr: true,
      cache: { maxAge: 60 }
    },

    // 重定向
    '/old-docs/**': { redirect: '/docs/**' },

    // 代理到外部服务
    '/api/external/**': { proxy: 'https://api.external.com/**' },

    // ISR (Incremental Static Regeneration)
    '/products/**': { isr: 600 },
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

routeRules 支持的完整配置项:

┌─────────────────────────────────────────────────────────────┐
│                  routeRules 配置项一览                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  │ 配置项       │ 类型                │ 作用                │
│  ├─────────────┼────────────────────┼─────────────────────┤
│  │ redirect     │ string | object    │ 服务端/客户端重定向  │
│  │ ssr          │ boolean            │ 是否启用服务端渲染    │
│  │ prerender    │ boolean | object   │ 构建时预渲染为静态    │
│  │ swr          │ number | boolean   │ 过期后后台重新验证    │
│  │ isr          │ number | boolean   │ 增量静态再生成        │
│  │ cache        │ object             │ 缓存控制(CDN/浏览器)  │
│  │ cors         │ boolean            │ 自动添加 CORS 头      │
│  │ headers      │ object             │ 自定义响应头          │
│  │ proxy        │ string | object    │ 代理到后端服务        │
│  │ experimental │ object             │ 实验性功能            │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

1.5 嵌套路由与 <NuxtPage> ​

当 pages/ 下的目录包含 index.vue 和其他子页面时,父级目录需要一个布局文件来渲染子路由:

vue
<!-- pages/projects/index.vue -->
<template>
  <div>
    <h2>项目列表</h2>
    <NuxtLink to="/projects/1">项目 #1</NuxtLink>
  </div>
</template>
1
2
3
4
5
6
7

嵌套路由的另一种方式是通过 <NuxtPage> 实现父-子布局:

vue
<!-- pages/parent.vue -->
<template>
  <div class="parent-layout">
    <h1>父级页面</h1>
    <!-- 子路由内容在此渲染 -->
    <NuxtPage />
  </div>
</template>
1
2
3
4
5
6
7
8

对应的文件结构:

text
pages/
├── parent.vue            # 使用 <NuxtPage> 渲染子路由
└── parent/
    └── child.vue          # /parent/child → 渲染在 parent.vue 的 <NuxtPage> 中
1
2
3
4

第2部分:server/ 目录与 Nitro 引擎 ​

2.1 Nitro 的架构定位 ​

Nitro 是 Nuxt 的服务端引擎,负责将 server/ 目录下的代码编译为可部署到多种平台的通用服务端 bundle:

┌─────────────────────────────────────────────────────────────┐
│                  Nitro 引擎架构全景                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                   server/ 目录                        │   │
│  │  server/api/        → /api/* HTTP 端点               │   │
│  │  server/routes/     → 非 /api 前缀的 HTTP 端点       │   │
│  │  server/middleware/  → 请求级中间件                   │   │
│  │  server/utils/      → 服务端工具函数                  │   │
│  │  server/plugins/    → Nitro 插件(生命周期钩子)      │   │
│  └───────────────────────┬─────────────────────────────┘   │
│                          ↓                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                Nitro 核心                             │   │
│  │  • h3 事件系统(defineEventHandler)                 │   │
│  │  • 跨平台预设(presets)——自动适配部署目标           │   │
│  │  • 文件系统路由(基于 server/api/ 和 server/routes/)│   │
│  │  • 缓存层(cachedEventHandler / SWR)                │   │
│  │  • 存储层(unstorage —— KV / FS / Redis 等驱动)    │   │
│  │  • 任务调度(cron / scheduled tasks)                │   │
│  └───────────────────────┬─────────────────────────────┘   │
│                          ↓                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              跨平台部署预设(Presets)                │   │
│  │                                                      │   │
│  │  cloudflare-pages  cloudflare-module-preset          │   │
│  │  vercel             vercel-edge                       │   │
│  │  netlify            netlify-edge                      │   │
│  │  node-server        aws-lambda                        │   │
│  │  deno-server        bun                               │   │
│  │  ... (30+ presets)                                   │   │
│  │                                                      │   │
│  │  → Nitro 自动将 server/ 代码转译为平台适配格式        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

2.2 Server Routes:server/api/ 与 server/routes/ ​

Server API 路由定义在 server/api/ 下,自动挂载到 /api/* 路径。Nitro 使用 h3 的事件处理模型:

ts
// server/api/projects/[id].get.ts
export default defineEventHandler(async (event) => {
  // getRouterParam 安全提取路由参数
  const id = getRouterParam(event, 'id')

  // readBody 解析请求体(POST/PUT/PATCH)
  // getQuery 读取 query 参数
  // getHeader / setHeader 操作请求/响应头

  if (!id) {
    throw createError({
      statusCode: 400,
      statusMessage: '缺少项目 ID',
    })
  }

  const project = await db.project.findUnique({ where: { id } })

  if (!project) {
    throw createError({ statusCode: 404 })
  }

  return project
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

HTTP 方法约定:

  • [name].get.ts → 处理 GET 请求
  • [name].post.ts → 处理 POST 请求
  • [name].put.ts → 处理 PUT 请求
  • [name].delete.ts → 处理 DELETE 请求
  • [name].patch.ts → 处理 PATCH 请求
  • [name].ts → 处理所有 HTTP 方法

server/routes/ 与 server/api/ 机制相同,但不带 /api 前缀:

ts
// server/routes/health.ts → GET /health
export default defineEventHandler(() => {
  return { status: 'ok', timestamp: Date.now() }
})

// server/routes/sitemap.xml.ts → GET /sitemap.xml
export default defineEventHandler(async (event) => {
  setHeader(event, 'content-type', 'application/xml')
  const urls = await getSitemapUrls()
  return generateSitemapXml(urls)
})
1
2
3
4
5
6
7
8
9
10
11

2.3 Server Middleware:请求级中间件 ​

Server middleware 在每次 HTTP 请求到达 Server Route 之前执行,用于跨切面的请求处理:

ts
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  // 仅对 /api/ 开头的请求执行认证
  if (!event.path.startsWith('/api/')) return

  const token = getHeader(event, 'authorization')?.replace('Bearer ', '')

  if (token) {
    try {
      const user = await verifyToken(token)
      // 将用户信息注入事件上下文
      event.context.user = user
    } catch {
      // token 无效时不阻断请求——交给具体 handler 决定
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ts
// server/middleware/rate-limit.ts
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()

export default defineEventHandler((event) => {
  const ip = getRequestIP(event) || 'unknown'
  const now = Date.now()
  const record = rateLimitMap.get(ip)

  if (!record || now > record.resetAt) {
    rateLimitMap.set(ip, { count: 1, resetAt: now + 60_000 })
    return
  }

  if (record.count > 100) {
    throw createError({ statusCode: 429, statusMessage: '请求过于频繁' })
  }

  record.count++
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Server middleware 的执行顺序:按照 server/middleware/ 目录下文件名的字母顺序执行。可以通过数字前缀控制顺序(01.auth.ts、02.rate-limit.ts、03.logging.ts)。

2.4 Nitro 缓存层:cachedEventHandler 与 SWR ​

Nitro 内置了强大的缓存原语,可以在 handler 级别配置缓存策略:

ts
// server/api/stats.get.ts
import { cachedEventHandler } from '#internal/nitro'

export default cachedEventHandler(
  async (event) => {
    // 耗时计算——如聚合查询
    const stats = await computeDashboardStats()
    return stats
  },
  {
    // 缓存键:基于请求的唯一标识
    getKey: (event) => `dashboard:stats`,

    // 缓存有效期(秒)
    maxAge: 300, // 5 分钟

    // 可选:基于响应状态码决定是否缓存
    shouldCache: (event, response) => {
      return response?.status === 200
    },

    // SWR:过期后先返回旧缓存,后台重新验证
    swr: true, // 或 swr: 3600(SWR 窗口期 1 小时)
  }
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

SWR(Stale-While-Revalidate) 模式的核心行为:

┌─────────────────────────────────────────────────────────────┐
│                  SWR 缓存策略时序                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  时间轴:                                                    │
│                                                             │
│  T+0     首次请求 → 缓存 MISS → 执行 handler → 写入缓存     │
│  T+60    缓存 HIT → 直接返回(< maxAge,新鲜)              │
│  T+300   缓存 STALE → 但仍在 SWR 窗口内                     │
│          → 立即返回旧缓存 + 后台异步执行 handler 刷新        │
│  T+301   后续请求 → 返回 T+300 刷新后的新缓存                │
│  T+3600  SWR 窗口过期 → 缓存 MISS → 同步执行 handler        │
│                                                             │
│  关键优势:用户永远不会因为缓存过期而等待 handler 执行       │
│  代价:可能短暂返回过期数据(最终一致性)                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

缓存适用场景判断:

  • 数据对实时性要求不高(排行榜、统计数据、配置信息)
  • 计算成本高但结果相对稳定
  • 外部 API 调用有速率限制或延迟高
  • 不适合:用户专属数据、实时交易、认证状态

第3部分:useFetch vs useAsyncData vs $fetch ​

3.1 三条数据路径的定位 ​

Nuxt 提供了三种数据获取方式,它们的运行环境和数据流各不相同:

┌─────────────────────────────────────────────────────────────┐
│           useFetch / useAsyncData / $fetch 决策树            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────── 需要获取数据 ────────────────────┐    │
│  │                                                      │    │
│  │  发生在什么时候?                                    │    │
│  │  ├── 页面加载时(SSR 阶段)                          │    │
│  │  │   ├── 请求 /api/* 接口?                          │    │
│  │  │   │   └── → useFetch()         【首选】          │    │
│  │  │   └── 非 API 数据(组合多源、ORM 调用、复杂逻辑) │    │
│  │  │       └── → useAsyncData()     【首选】          │    │
│  │  │                                                   │    │
│  │  └── 用户交互时(按钮点击、表单提交、滚动加载)       │    │
│  │      ├── 需要更新 SSR payload 缓存?                 │    │
│  │      │   └── → useFetch() 或 useAsyncData()          │    │
│  │      └── 纯客户端操作、不需要缓存去重?               │    │
│  │          └── → $fetch()           【首选】          │    │
│  │                                                      │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                             │
│  核心区分:                                                  │
│  • useFetch/useAsyncData:服务端执行一次 → payload → 客户端  │
│    水合复用,避免重复请求                                     │
│  • $fetch:每次调用都执行,不做 SSR payload 去重              │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

3.2 useFetch:页面数据的首选 ​

useFetch 封装了 $fetch,自动处理 SSR payload 传递——服务器渲染时将结果序列化到 window.__NUXT__,客户端水合时直接复用,不发起重复请求:

vue
<script setup lang="ts">
// 基础用法:URL 字符串
const { data, pending, error, refresh } = await useFetch('/api/projects')

// 响应式 URL:参数变化时自动重新请求
const route = useRoute()
const { data: project } = await useFetch(
  () => `/api/projects/${route.params.id}`,
  {
    // 请求选项
    method: 'GET',
    query: { include: 'members' },

    // 响应转换
    transform: (response) => {
      return response.projects.map(normalizeProject)
    },

    // 缓存 key —— 决定数据如何在 payload 中存储与复用
    key: () => `project-${route.params.id}`,

    // 只在服务端执行
    server: true,

    // 懒加载:先渲染页面,pending 状态下显示骨架屏
    lazy: true,

    // 只在水合后从客户端请求
    // server: false,
  }
)
</script>

<template>
  <div>
    <div v-if="pending">加载中...</div>
    <div v-else-if="error">加载失败:{{ error.message }}</div>
    <div v-else>
      <pre>{{ data }}</pre>
    </div>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

lazy: true vs 默认 await:

vue
<script setup lang="ts">
// 默认行为:await —— 阻塞导航直到数据就绪
// 适合:数据是页面核心内容,没有数据展示空页面无意义
const { data } = await useFetch('/api/dashboard')

// lazy: true —— 不阻塞导航,先展示 pending UI
// 适合:非核心数据、可能较慢的请求、希望快速呈现页面骨架
const { data: recommendations, pending } = useFetch('/api/recommendations', {
  lazy: true
})
</script>
1
2
3
4
5
6
7
8
9
10
11

3.3 useAsyncData:自定义逻辑的通用容器 ​

当数据源不是标准的 /api/* 端点时,useAsyncData 是更灵活的选择:

ts
// 场景一:组合多个数据源
const { data } = await useAsyncData('user-context', async () => {
  const [profile, permissions, settings] = await Promise.all([
    $fetch('/api/users/me'),
    $fetch('/api/users/me/permissions'),
    $fetch('/api/users/me/settings'),
  ])
  return { profile, permissions, settings }
})

// 场景二:调用 ORM / 数据库查询(server/ 层)
const { data: posts } = await useAsyncData(
  () => `posts-${page.value}`,
  () => db.post.findMany({
    where: { published: true },
    skip: (page.value - 1) * 10,
    take: 10,
  })
)

// 场景三:带复杂转换逻辑的数据处理
const { data: report } = await useAsyncData('monthly-report', async () => {
  const raw = await $fetch('/api/reports/monthly')
  return {
    ...raw,
    // 日期转换
    periodStart: new Date(raw.periodStart),
    // 数值格式化
    revenue: formatCurrency(raw.revenue),
    // 排序
    topItems: raw.items.sort((a, b) => b.value - a.value).slice(0, 10),
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

key 的重要性:key 用于在 Nuxt payload 中唯一标识这份数据。如果 key 不稳定或缺少区分参数,会导致错误的数据共享:

ts
// 错误:所有页面共享同一个 key,导致数据错乱
const { data } = await useAsyncData('project', () =>
  $fetch(`/api/projects/${route.params.id}`)
)

// 正确:key 包含区分参数
const { data } = await useAsyncData(
  () => `project:${route.params.id}`,
  () => $fetch(`/api/projects/${route.params.id}`)
)
1
2
3
4
5
6
7
8
9
10

3.4 $fetch:纯客户端操作与事件处理 ​

$fetch 不做任何 SSR payload 缓存,每次调用都发起真实的 HTTP 请求。适合不需要服务端去重的场景:

ts
// 场景一:用户交互触发的 mutation
async function saveProject() {
  try {
    const result = await $fetch('/api/projects', {
      method: 'POST',
      body: formData.value,
    })
    await navigateTo(`/projects/${result.id}`)
  } catch (err) {
    showError(err)
  }
}

// 场景二:Server API 内调用外部服务
// server/api/weather/[city].get.ts
export default defineEventHandler(async (event) => {
  const city = getRouterParam(event, 'city')
  return $fetch(`https://api.weather.com/v1/${city}`, {
    headers: { Authorization: `Bearer ${useRuntimeConfig().weatherKey}` }
  })
})

// 场景三:纯客户端请求(server: false 标记)
// 明确说明该数据只在浏览器端可用
const { data: clientData } = await useFetch('/api/device-info', {
  server: false  // 跳过 SSR 阶段
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

3.5 数据 API 对比总结 ​

┌─────────────────────────────────────────────────────────────┐
│         三种数据 API 的行为对比                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  │ 特性              │ useFetch    │ useAsyncData │ $fetch    │
│  ├──────────────────┼────────────┼─────────────┼──────────┤
│  │ SSR payload 去重  │ 自动        │ 自动         │ 无        │
│  │ 阻塞导航(await)   │ 支持        │ 支持         │ -         │
│  │ 懒加载(lazy)      │ 支持        │ 支持         │ -         │
│  │ 响应式 URL 重请求 │ 支持        │ 手动实现     │ -         │
│  │ 自定义转换逻辑    │ transform   │ 函数体内     │ 调用方    │
│  │ 类型推断          │ 需泛型      │ 自动推断     │ 自动推断  │
│  │ 缓存管理          │ key 控制    │ key 控制     │ 无        │
│  │ 适用数据源        │ /api/* URL  │ 任意异步函数 │ 任意 URL  │
│  │ 服务端调用        │ SSR 阶段    │ SSR 阶段     │ 任意位置  │
│  │ 客户端调用        │ 水合后按需  │ 水合后按需   │ 任意位置  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

第4部分:SSR / SSG / CSR / SWR 混合渲染 ​

4.1 四种渲染模式的本质差异 ​

Nuxt 4 支持在同一个应用中混合使用多种渲染策略。每个路由可以独立配置其渲染行为。

┌─────────────────────────────────────────────────────────────┐
│                四种渲染模式运行时对比                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌── SSR (Server-Side Rendering) ─────────────────────────┐ │
│  │  每次请求 → Nitro 执行组件 → 生成 HTML → 返回浏览器     │ │
│  │  数据新鲜度:实时       服务器负载:高                  │ │
│  │  适用:个性化内容、实时数据、需要 SEO 的动态页面        │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                             │
│  ┌── SSG / Prerender (Static Site Generation) ────────────┐ │
│  │  构建时 → 执行组件 → 生成 HTML 文件 → 部署为静态文件    │ │
│  │  数据新鲜度:构建时     服务器负载:零(纯静态托管)     │ │
│  │  适用:文档、博客、营销页等内容不频繁变化的页面         │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                             │
│  ┌── CSR (Client-Side Rendering) ─────────────────────────┐ │
│  │  服务器返回空壳 HTML → 浏览器下载 JS → 客户端渲染      │ │
│  │  数据新鲜度:实时       服务器负载:极低(仅静态文件)   │ │
│  │  适用:登录后的 dashboard、不需要 SEO 的交互工具       │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                             │
│  ┌── SWR / ISR (Stale-While-Revalidate / Incremental) ────┐ │
│  │  首次请求 → SSR 生成 HTML + 缓存                       │ │
│  │  后续请求 → 返回缓存 HTML(同时后台刷新)              │ │
│  │  数据新鲜度:准实时     服务器负载:低(缓存命中率高)  │ │
│  │  适用:产品列表、文章内容等"可略微过时"的页面           │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

4.2 routeRules 配置每种模式 ​

ts
// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // ========== SSR(默认)==========
    // 不配置 routeRules 即为 SSR,或显式设置
    '/dashboard/**': { ssr: true },

    // ========== SSG / Prerender ==========
    '/': { prerender: true },
    '/about': { prerender: true },

    // 批量预渲染:为动态路由的每个已知参数值生成静态页面
    '/blog/[slug]': {
      prerender: {
        // 从数据源获取所有可能的 slug
        getStaticPaths: async () => {
          const posts = await $fetch('/api/posts')
          return posts.map(p => ({ params: { slug: p.slug } }))
        }
      }
    },

    // ========== CSR(纯客户端渲染)==========
    '/admin/**': { ssr: false },

    // ========== SWR(过期后后台刷新)==========
    // swr: 3600 → 缓存 1 小时后"软化",后续请求先返回旧缓存再后台刷新
    '/products/**': { swr: 3600 },

    // swr: true → 始终先返回缓存,后台刷新(缓存永不过期)
    '/api/stats': { swr: true },

    // ========== ISR(增量静态再生成)==========
    // 类似 SWR,但生成的 HTML 会持久化到 CDN/存储层
    '/blog/**': { isr: 1800 }, // 30 分钟后重新生成

    // ========== 带过期时间的缓存 ==========
    // 适合 CDN 边缘缓存
    '/assets/**': {
      headers: { 'Cache-Control': 'public, max-age=31536000, immutable' }
    },
    '/api/posts': {
      cache: { maxAge: 60 }  // CDN/浏览器缓存 60 秒
    },
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

4.3 混合渲染策略设计 ​

实际项目中,应根据每个路由的数据特性和访问模式选择渲染策略:

┌─────────────────────────────────────────────────────────────┐
│              典型的混合渲染项目配置                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  路由                  │ 策略      │ 理由                    │
│  ─────────────────────┼──────────┼───────────────────────── │
│  / (首页)             │ prerender │ 内容稳定,访问量最大     │
│  /about /pricing      │ prerender │ 静态营销页面              │
│  /blog/*              │ ISR 600   │ 偶尔更新,SEO 重要       │
│  /blog (列表页)       │ SWR 300   │ 聚合页,可略微过时        │
│  /products/*          │ SWR 3600  │ 商品详情,更新频率低      │
│  /search              │ SSR       │ 实时搜索结果              │
│  /dashboard/*         │ SSR       │ 个性化数据,必须实时      │
│  /admin/*             │ CSR       │ 认证后使用,不需要 SEO    │
│                                                             │
│  关键原则:                                                  │
│  1. 越靠前的页面(首页、列表)越倾向预渲染 / 缓存            │
│  2. 越个性化的页面(dashboard、设置)越倾向 SSR / CSR        │
│  3. 公共内容永远优先考虑缓存,私有内容永远跳过缓存           │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

4.4 客户端渲染的降级策略 ​

当 SSR 失败时(第三方 API 超时、服务端错误),需要优雅降级到客户端渲染:

vue
<script setup lang="ts">
// 方案一:使用 NuxtErrorBoundary 隔离错误
// 在 layout 或 app.vue 中包裹
</script>

<template>
  <NuxtErrorBoundary>
    <!-- SSR 错误时显示 fallback,不影响其他区域 -->
    <ProductDetail :product-id="productId" />

    <template #error="{ error, clearError }">
      <div class="error-fallback">
        <p>加载失败:{{ error?.message }}</p>
        <button @click="clearError">重试</button>
      </div>
    </template>
  </NuxtErrorBoundary>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vue
<script setup lang="ts">
// 方案二:ClientOnly 组件包裹浏览器专属内容
</script>

<template>
  <div>
    <!-- 服务端渲染占位符 -->
    <ClientOnly fallback-tag="div">
      <div class="skeleton">加载中...</div>

      <template #fallback>
        <!-- 客户端水合后才渲染真实组件 -->
        <HeavyChart :data="chartData" />
      </template>
    </ClientOnly>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ts
// 方案三:在 setup 中处理 SSR 失败
const { data, error } = await useFetch('/api/recommendations', {
  // SSR 请求超时
  timeout: 3000,
  // 失败时在客户端重试
  onResponseError({ response }) {
    if (process.server) {
      // 服务端静默失败,交给客户端重试
      console.warn('SSR 数据获取失败,将降级为客户端渲染')
    }
  }
})

// 客户端重试逻辑:如果 SSR 阶段数据为空,客户端自动重试
if (!data.value && process.client) {
  await refresh()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

第5部分:Nuxt Layers ​

5.1 Layer 的概念与设计意图 ​

Nuxt Layer 是一种可复用的 Nuxt 项目配置单元。一个 Layer 可以包含 pages、components、composables、server API、plugins、middleware 等一切 Nuxt 项目可以拥有的内容。

┌─────────────────────────────────────────────────────────────┐
│                  Nuxt Layer 概念模型                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                Layer 可以包含什么?                   │   │
│  │                                                      │   │
│  │  app/            pages/          server/             │   │
│  │  ├─ app.vue      ├─ index.vue    ├─ api/             │   │
│  │  ├─ components/  ├─ about.vue    ├─ middleware/      │   │
│  │  ├─ composables/ ├─ ...          └─ utils/           │   │
│  │  ├─ layouts/                                          │   │
│  │  ├─ middleware/    public/         nuxt.config.ts     │   │
│  │  ├─ plugins/       └─ favicon.ico                     │   │
│  │  └─ assets/                                            │   │
│  │                                                      │   │
│  │  一个 Layer = 一个"迷你 Nuxt 项目"                    │   │
│  │  可以被其他 Nuxt 项目引入并合并                        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌── 主项目 ──────────────────────────────────────────────┐ │
│  │                                                        │ │
│  │  extends: [layerA, layerB, layerC]                     │ │
│  │                                                        │ │
│  │  合并规则:                                             │ │
│  │  • 组件/Composable/Plugin → 全部可用(同名时主项目覆盖)│ │
│  │  • pages/ → 合并(冲突时构建报错)                      │ │
│  │  • nuxt.config.ts → 深度合并                           │ │
│  │  • server/ → 合并所有 handler                          │ │
│  │                                                        │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

5.2 Layer 的引入方式 ​

方式一:本地相对路径

ts
// nuxt.config.ts
export default defineNuxtConfig({
  extends: [
    // 本地 layer(同仓库内)
    './layers/base',
    './layers/theme-corporate',
  ]
})
1
2
3
4
5
6
7
8

方式二:npm 包

ts
// nuxt.config.ts
export default defineNuxtConfig({
  extends: [
    // 通过 npm 安装的 layer 包
    '@my-org/nuxt-layer-base',
    'nuxt-layer-dashboard',
  ]
})
1
2
3
4
5
6
7
8

npm 包形式的 layer,其 package.json 不需要特殊配置,只需将 Nuxt 文件放在包根目录即可。使用方可按需选择引入哪些功能。

方式三:Git 仓库 / Git Submodule

ts
// nuxt.config.ts
export default defineNuxtConfig({
  extends: [
    // GitHub 仓库(通过 giget 下载)
    'github:my-org/nuxt-layer-base',
    // GitLab 仓库
    'gitlab:my-org/nuxt-layer-theme',
  ]
})
1
2
3
4
5
6
7
8
9
bash
# 或使用 git submodule 获得更好的版本锁定
git submodule add https://github.com/my-org/nuxt-layer-base.git layers/base
1
2

5.3 Layer 的典型应用场景 ​

场景一:多项目共享基础配置(白标应用)

┌─────────────────────────────────────────────────────────────┐
│              白标应用:Layer 实现多品牌复用                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌── Layer: base ────────────────────────────────────────┐  │
│  │  共享的 pages 结构、composables、server API            │  │
│  │  components/ (基础 UI 组件库)                          │  │
│  │  server/api/ (通用业务 API)                            │  │
│  │  middleware/ (统一的认证逻辑)                          │  │
│  └────────────┬───────────────┬──────────────────────────┘  │
│               │               │                              │
│      ┌────────▼────┐  ┌───────▼────────┐                    │
│      │ Layer: 品牌A │  │ Layer: 品牌B   │                    │
│      │ 主题/颜色     │  │ 主题/颜色       │                    │
│      │ 品牌组件      │  │ 品牌组件        │                    │
│      │ 内容定制      │  │ 内容定制        │                    │
│      └──────┬───────┘  └───────┬────────┘                    │
│             │                  │                              │
│     ┌───────▼──────┐  ┌───────▼────────┐                    │
│     │ Project: A   │  │ Project: B     │                    │
│     │ extends:     │  │ extends:       │                    │
│     │ [base, 品牌A]│  │ [base, 品牌B]  │                    │
│     └──────────────┘  └────────────────┘                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

示例配置:

ts
// layers/base/nuxt.config.ts
export default defineNuxtConfig({
  // 基础配置:所有品牌共享
  modules: ['@nuxtjs/i18n', '@pinia/nuxt'],
  css: ['~/assets/css/base.css'],
  runtimeConfig: {
    public: {
      apiBase: process.env.API_BASE_URL,
    }
  },
  app: {
    head: {
      htmlAttrs: { lang: 'zh-CN' },
      meta: [
        { name: 'viewport', content: 'width=device-width, initial-scale=1' }
      ]
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ts
// layers/theme-brand-a/nuxt.config.ts
export default defineNuxtConfig({
  // 品牌 A 的覆盖配置
  css: ['~/assets/css/brand-a-theme.css'],
  app: {
    head: {
      title: 'Brand A Platform',
      link: [{ rel: 'icon', href: '/brand-a-favicon.ico' }]
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
ts
// project-a/nuxt.config.ts
export default defineNuxtConfig({
  extends: [
    '../layers/base',
    '../layers/theme-brand-a',
  ]
})
1
2
3
4
5
6
7

场景二:共享 UI 组件库与设计系统

ts
// layers/design-system/nuxt.config.ts
export default defineNuxtConfig({
  components: [
    // 自动导入 layer 内所有组件
    { path: '~/components', pathPrefix: false },
  ],
  // 注入全局 CSS 变量
  css: ['~/assets/tokens.css'],
})

// layers/design-system/composables/useTheme.ts
export function useTheme() {
  const colorMode = useColorMode()
  const isDark = computed(() => colorMode.value === 'dark')

  function toggleTheme() {
    colorMode.preference = isDark.value ? 'light' : 'dark'
  }

  return { colorMode, isDark, toggleTheme }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

场景三:共享 Server API 与认证逻辑

ts
// layers/auth/server/middleware/session.ts
export default defineEventHandler(async (event) => {
  const sessionToken = getCookie(event, 'session')

  if (sessionToken) {
    const session = await getSession(sessionToken)
    if (session) {
      event.context.session = session
      event.context.user = session.user
    }
  }
})

// layers/auth/composables/useAuth.ts
export function useAuth() {
  const { data: user, refresh } = useFetch('/api/auth/me')

  async function login(credentials: { email: string; password: string }) {
    await $fetch('/api/auth/login', { method: 'POST', body: credentials })
    await refresh()
  }

  async function logout() {
    await $fetch('/api/auth/logout', { method: 'POST' })
    user.value = null
  }

  const isAuthenticated = computed(() => !!user.value)

  return { user, isAuthenticated, login, logout }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

5.4 Layer 优先级与覆盖规则 ​

多个 Layer 按 extends 数组的顺序合并,后面的 Layer 覆盖前面的:

┌─────────────────────────────────────────────────────────────┐
│                  Layer 合并优先级                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  extends: [layerA, layerB, layerC]                          │
│                                                             │
│  优先级(从低到高):                                        │
│  layerA < layerB < layerC < 主项目(nuxt.config.ts)        │
│                                                             │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  主项目始终拥有最高优先级                              │   │
│  │  → 可以覆盖任何 Layer 中的配置、组件、页面            │   │
│  │                                                      │   │
│  │  app.vue:主项目有 app.vue → 使用主项目               │   │
│  │  components:同名组件 → 主项目覆盖 Layer              │   │
│  │  composables:同名 composable → 主项目覆盖 Layer      │   │
│  │  nuxt.config:深度合并,主项目值优先                  │   │
│  └──────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

第6部分:Nuxt 中间件 ​

6.1 路由中间件的分类 ​

Nuxt 的中间件运行在客户端导航和服务端首次渲染时,用于在页面渲染前执行逻辑(认证检查、重定向、分析跟踪等):

┌─────────────────────────────────────────────────────────────┐
│                  Nuxt 路由中间件体系                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌── 定义方式 ────────────────────────────────────────────┐ │
│  │                                                        │ │
│  │  middleware/                    ← 全局目录              │ │
│  │  ├── auth.ts                   ← 命名中间件             │ │
│  │  ├── analytics.ts                                      │ │
│  │  └── 01.redirects.global.ts    ← 全局中间件(.global)   │ │
│  │                                                        │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                             │
│  ┌── 使用方式 ────────────────────────────────────────────┐ │
│  │                                                        │ │
│  │  1. definePageMeta({ middleware: 'auth' })  单个页面    │ │
│  │  2. definePageMeta({ middleware: ['auth','log'] }) 多个 │ │
│  │  3. 文件名后缀 .global.ts → 每页自动执行                 │ │
│  │  4. nuxt.config.ts → router.options.middleware 全局注册 │ │
│  │                                                        │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                             │
│  ┌── 执行时机 ────────────────────────────────────────────┐ │
│  │                                                        │ │
│  │  客户端导航:在页面组件创建之前执行                     │ │
│  │  服务端 SSR:在渲染 HTML 之前执行                       │ │
│  │                                                        │ │
│  │  流程:                                                 │ │
│  │  导航触发 → 全局中间件 → 命名中间件(按数组顺序)       │ │
│  │  → 页面 setup → 页面渲染                               │ │
│  │                                                        │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

6.2 命名中间件:按需挂载 ​

命名中间件定义在 middleware/ 目录,使用时在页面的 definePageMeta 中显式引用:

ts
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const { isAuthenticated } = useAuth()

  // 未登录 → 重定向到登录页
  if (!isAuthenticated.value && to.path !== '/login') {
    // 登录后可以回到目标页面
    return navigateTo({
      path: '/login',
      query: { redirect: to.fullPath }
    })
  }

  // 已登录但访问登录页 → 重定向到首页
  if (isAuthenticated.value && to.path === '/login') {
    return navigateTo('/')
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ts
// middleware/role-check.ts
export default defineNuxtRouteMiddleware(async (to) => {
  const { user } = useAuth()

  // 异步获取用户角色
  const { data: permissions } = await useFetch('/api/users/me/permissions')

  const requiredRole = to.meta.requiredRole as string | undefined

  if (requiredRole && !permissions.value?.roles?.includes(requiredRole)) {
    // abortNavigation 阻止导航但不重定向
    return abortNavigation('你没有访问此页面的权限')
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

页面中使用:

vue
<script setup lang="ts">
definePageMeta({
  middleware: ['auth', 'role-check'],
  // 页面级别的 meta 信息可被中间件读取
  requiredRole: 'admin',
})
</script>
1
2
3
4
5
6
7

中间件组合模式:

ts
// middleware/authenticated.ts
// 将多个中间件逻辑组合为一个
export default defineNuxtRouteMiddleware(async (to, from) => {
  // 先执行认证检查
  const authMiddleware = await import('./auth')
  const authResult = authMiddleware.default(to, from)
  if (authResult) return authResult

  // 再执行角色检查
  const roleMiddleware = await import('./role-check')
  return roleMiddleware.default(to, from)
})
1
2
3
4
5
6
7
8
9
10
11
12

6.3 全局中间件:每次导航执行 ​

全局中间件的文件名以 .global.ts 结尾,每次路由变化都会执行:

ts
// middleware/01.analytics.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
  // 页面浏览埋点
  if (process.client) {
    // 仅在客户端执行(SSR 阶段不需要)
    nextTick(() => {
      useAnalytics().trackPageView({
        path: to.fullPath,
        title: to.meta.title as string || document.title,
        referrer: from?.fullPath,
      })
    })
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ts
// middleware/02.redirects.global.ts
const redirects: Record<string, string> = {
  '/old-home': '/',
  '/v1/docs': '/docs',
  '/blog/2023': '/blog',
}

export default defineNuxtRouteMiddleware((to) => {
  const target = redirects[to.path]
  if (target) {
    return navigateTo(target, { redirectCode: 301 })
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13

6.4 中间件的执行顺序与注意事项 ​

执行顺序规则:

  1. 全局中间件按文件名字母顺序执行(建议用数字前缀如 01.、02. 控制)
  2. 命名中间件按 definePageMeta 中数组的顺序执行
  3. 所有中间件执行完毕后才进入页面组件的 setup

关键注意事项:

ts
// 1. 中间件在 SSR 和客户端都会执行——注意环境区分
export default defineNuxtRouteMiddleware(() => {
  // 错误:SSR 阶段没有 window
  // const width = window.innerWidth

  // 正确:区分环境
  if (process.client) {
    // 只能在客户端执行的逻辑
  }
})

// 2. 中间件中的 return 值会终止导航链
// navigateTo() → 重定向
// abortNavigation() → 停止且不重定向
// 无返回值 / undefined → 正常通过

// 3. 避免在中间件中做重量级数据请求
// 这会阻塞所有页面的导航——应放在页面组件的 setup 中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

6.5 中间件 vs Server Middleware vs 路由守卫的边界 ​

┌─────────────────────────────────────────────────────────────┐
│            三层"中间件"的职责边界                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  │ 层级          │ 执行位置    │ 主要用途                    │
│  ├──────────────┼───────────┼─────────────────────────────┤
│  │ 路由中间件    │ 客户端+SSR │ 认证守卫、重定向、页面埋点  │
│  │ (middleware/) │            │ 阻止访问、路由级权限控制    │
│  ├──────────────┼───────────┼─────────────────────────────┤
│  │ Server Mid.  │ Nitro 服务端│ 请求认证、速率限制、日志    │
│  │ (server/      │            │ CORS、Header 注入            │
│  │  middleware/) │            │ → 安全边界,不可跳过         │
│  ├──────────────┼───────────┼─────────────────────────────┤
│  │ Vue Router    │ 客户端导航 │ beforeResolve、afterEach    │
│  │ 守卫          │ (插件注册) │ 细粒度导航控制              │
│  │              │            │ → 路由中间件通常是首选       │
│                                                             │
│  安全原则:认证和授权必须在 Server Middleware 中再次验证     │
│  路由中间件改善用户体验(提前跳转),但不能作为安全边界      │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

核心总结 ​

总结1:文件路由的本质是"目录即路由表" ​

Nuxt 4 将 pages/ 目录结构直接映射为 Vue Router 配置。理解动态路由([id])、可选参数([[lang]])、catch-all([...slug])和路由分组((group))四种模式,就能覆盖绝大多数路由场景。routeRules 在路由层之上提供了声明式的行为控制(重定向、缓存、代理),不需在页面组件中硬编码这些逻辑。

总结2:Nitro 让服务端代码"写一次,处处部署" ​

server/api/ 和 server/routes/ 定义 HTTP 端点,server/middleware/ 处理请求级切面。Nitro 通过预设(presets)自动适配 30+ 部署平台——相同的 defineEventHandler 代码可以不加修改地部署到 Node.js、Cloudflare Workers、Vercel、AWS Lambda 等。cachedEventHandler 和 SWR 在 handler 层提供了与部署平台无关的缓存能力。

总结3:三种数据 API 各司其职 ​

useFetch 用于请求 /api/* 接口的页面数据,自动处理 SSR payload 去重。useAsyncData 是通用容器,适合组合多数据源、调用 ORM、执行复杂转换。$fetch 不做任何缓存去重,适合用户事件触发的 mutation 和 Server API 内部调用。选择的关键不是"哪个更好",而是"数据在什么时候、从哪一端获取"。

总结4:混合渲染是 Nuxt 的核心竞争力 ​

同一应用内,首页和文档预渲染为静态文件,产品列表使用 SWR 缓存,dashboard 实时 SSR,后台管理纯 CSR——每个路由独立选择最适合的渲染策略。routeRules 的 prerender、swr、isr、ssr: false 四种配置覆盖了从纯静态到纯动态的完整光谱。

总结5:Layers 实现"不重复造轮子"的工程化 ​

Layer 是"可复用的 Nuxt 项目片段"——包含 pages、components、composables、server 等一切 Nuxt 资源。通过 Git submodule、npm 包或本地路径引入,Layer 的配置、组件、页面会与主项目合并。多品牌白标应用、共享设计系统、统一认证逻辑是 Layer 的典型应用场景。

总结6:中间件不能替代 Server 端的安全验证 ​

路由中间件(middleware/)在页面渲染前执行,适合认证守卫、重定向、页面埋点等 UI 层面的控制。但它在客户端是可绕过或调试的——真正的安全认证和授权必须在 Server Middleware 和 Server API 中独立验证。路由中间件改善体验,Server Middleware 保障安全。


章节测试 ​

测试1:以下哪种文件结构不生成合法的 Nuxt 路由? ​

A. pages/projects/[id].vue → /projects/:id B. pages/(auth)/login.vue → /auth/login C. pages/blog/[...slug].vue → /blog/* D. pages/index.vue → /

测试2:cachedEventHandler 的 SWR 模式与普通缓存的最大区别是什么? ​

A. SWR 缓存永不过期 B. SWR 在缓存过期后先返回旧数据,同时后台刷新 C. SWR 的缓存存储在 CDN 而不是服务端内存 D. SWR 只能在 Edge Functions 环境中使用

测试3:何时应该使用 $fetch 而不是 useFetch? ​

测试4:一个在线商城项目的路由渲染策略如何设计?为以下页面选择合适的渲染模式并说明理由: ​

  • 首页(产品推荐、Banner)
  • 商品详情页
  • 搜索结果页
  • 用户订单列表
  • 后台管理面板

测试5:Nuxt Layer 的优先级规则是什么?如果 Layer A 和 Layer B 都定义了同名的 components/Button.vue,主项目也定义了一个,最终会使用哪个? ​

测试6:全局中间件 .global.ts 和命名中间件在执行顺序上的关系是怎样的? ​


参考答案 ​

测试1答案 ​

答案:B。路由分组 (auth) 的括号目录名不会出现在 URL 中,因此 pages/(auth)/login.vue 生成的路由是 /login,不是 /auth/login。

测试2答案 ​

答案:B。SWR 的核心价值在于"过期不等待"——缓存过期后,请求立即收到旧缓存数据,同时后台异步执行 handler 刷新缓存。用户感知到的延迟永远是缓存读取的速度,而不是 handler 执行的速度。A 不准确(SWR 可以设置窗口期),C 和 D 都是错误的。

测试3答案 ​

$fetch 适合以下场景:

  • 用户交互触发的写操作(表单提交、删除、更新)——不需要 SSR payload 缓存
  • Server API 内部调用外部服务——此时已在服务端,没有"去重"需求
  • 纯客户端请求——明确不需要 SSR 阶段的数据
  • 事件处理函数中的一次性请求——不需要响应式追踪

核心判断:如果数据需要在 SSR 阶段获取并传递到客户端水合复用,用 useFetch/useAsyncData;如果只是某次用户操作的瞬态请求,用 $fetch。

测试4答案 ​

页面渲染模式理由
首页(产品推荐)SWR 300 或 ISR 600内容更新不频繁、访问量最大、SEO 重要
商品详情页SWR 3600 或 ISR商品信息相对稳定、SEO 重要、可用缓存大幅降低服务端负载
搜索结果页SSR搜索参数组合无限、结果实时变化、需要个性化推荐
用户订单列表SSR个性化数据、必须实时准确、涉及用户隐私不能缓存
后台管理面板CSR (ssr: false)认证后使用、不需要 SEO、以交互体验为主

测试5答案 ​

优先级:主项目 > extends 中靠后的 Layer > extends 中靠前的 Layer。

对于 extends: [layerA, layerB]:layerB 的优先级高于 layerA。主项目的内容拥有最高优先级。

如果 Layer A、Layer B、主项目都定义了 components/Button.vue,最终会使用主项目的版本。如果只有 Layer A 和 Layer B 定义,主项目没有,则使用 Layer B 的版本(B 在数组中位置更靠后,优先级更高)。

测试6答案 ​

全局中间件(.global.ts)先于命名中间件执行。

完整执行顺序:

  1. 全局中间件按文件名字母顺序执行(01.analytics.global.ts → 02.redirects.global.ts)
  2. 命名中间件按 definePageMeta 中 middleware 数组的顺序执行(['auth', 'role-check'] → 先 auth 后 role-check)
  3. 所有中间件执行完毕后,进入页面组件的 setup

如果任何中间件返回 navigateTo() 或 abortNavigation(),后续中间件和页面组件都不会执行。


相关笔记 ​

  • [[./03-router-forms-and-component-architecture]] — Vue Router 基础与组件架构
  • [[./06-nuxt-data-server-cache]] — Nuxt 数据获取、Server API 与缓存深度
  • [[./04-pinia-state-management]] — Pinia 状态管理(中间件常配合 Store 使用)
  • [[../02-react-and-nextjs/00-overview]] — React/Next.js 生态对比参考

下一步学习 ​

  • [ ] 阅读 Nuxt 数据获取、Server API 与缓存 — 深入数据层
  • [ ] 在本地创建一个 Nuxt 4 项目,尝试配置不同的 routeRules 渲染策略
  • [ ] 实现一个完整的认证中间件(登录守卫 + 角色检查 + 全局日志)
  • [ ] 将一个现有的 Vue 组件库封装为 Nuxt Layer,并在另一个项目中引用

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇5. Pinia 状态管理与持久化 / Pinia State Management and Persistence
下一篇7. Nuxt 数据获取、Server API 与缓存 / Nuxt Data Fetching, Server APIs, and Caching

持续记录,持续成长

Copyright © Tidenflow