基于文件的路由系统原理

Nuxt4 的路由系统是整个框架"约定优于配置"哲学的最佳体现——你创建一个文件,路由就自动存在。本章深入解析 pages/ 目录到路由表的完整映射规则、NuxtPage 组件的工作原理、路由级代码分割机制,以及 definePageMeta 的全部配置能力。

1. pages/ 目录到路由表的自动映射

1.1 映射规则总览

Nuxt4 使用 app/pages/ 目录结构自动生成 Vue Router 路由表。映射规则非常直观:

app/pages/
├── index.vue              → /
├── about.vue              → /about
├── contact.vue            → /contact
├── videos/
│   ├── index.vue          → /videos
│   ├── [id].vue           → /videos/:id
│   └── upload.vue         → /videos/upload
├── studio/
│   ├── index.vue          → /studio
│   └── projects.vue       → /studio/projects
└── admin/
    ├── index.vue          → /admin
    └── users.vue          → /admin/users

核心规则

  • 文件路径 → URL 路径(去掉 .vue 后缀)
  • index.vue → 目录的默认页(/
  • 子目录 → URL 的子路径
  • 文件名即路由名

1.2 路由表自动生成过程

nuxt dev 启动时,Nuxt 会扫描 app/pages/ 目录,执行以下步骤:

1. 扫描 app/pages/ 下所有 .vue 文件
2. 根据文件路径生成路由配置(path、name、component)
3. 将路由配置写入 .nuxt/routes.mjs(虚拟模块)
4. Vue Router 读取该配置创建路由实例
5. 文件变更时(新增/删除/重命名),自动重新生成

生成的路由配置大致如下:

// .nuxt/routes.mjs(自动生成,不要手动修改)
export default [
  {
    name: 'index',
    path: '/',
    component: () => import('../app/pages/index.vue'),
  },
  {
    name: 'videos',
    path: '/videos',
    component: () => import('../app/pages/videos/index.vue'),
  },
  {
    name: 'videos-id',
    path: '/videos/:id',
    component: () => import('../app/pages/videos/[id].vue'),
  },
]

1.3 路由名称的生成规则

路由 name 由文件路径自动生成,规则是将路径分隔符替换为 -

文件路径路由 name路由 path
pages/index.vueindex/
pages/about.vueabout/about
pages/videos/index.vuevideos/videos
pages/videos/[id].vuevideos-id/videos/:id
pages/videos/[id]/edit.vuevideos-id-edit/videos/:id/edit
pages/admin/users.vueadmin-users/admin/users

1.4 没有 pages/ 目录的情况

如果项目不需要路由(比如只有一个页面),可以不创建 pages/ 目录。此时 Nuxt 不会引入 Vue Router,app.vue 就是唯一的页面:

<!-- app/app.vue — 无路由模式 -->
<template>
  <div>
    <h1>单页应用</h1>
    <!-- 没有 <NuxtPage />,不需要路由 -->
  </div>
</template>

这在构建小型工具或 Widget 时非常有用,可以减少 ~20KB 的 Bundle 体积(Vue Router 不会被打包)。

1.5 自定义路由扩展

如果自动生成的路由不满足需求,可以通过 app/router.options.ts 扩展:

// app/router.options.ts
import type { RouterConfig } from '@nuxt/schema'
 
export default <RouterConfig>{
  // 自定义滚动行为
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) return savedPosition
    if (to.hash) return { el: to.hash, behavior: 'smooth' }
    return { top: 0, behavior: 'smooth' }
  },
 
  // 添加额外路由(与文件系统路由合并)
  routes: (_routes) => [
    ..._routes,
    {
      name: 'legacy-video',
      path: '/v/:id',  // 兼容旧版 URL
      redirect: (to) => ({ name: 'videos-id', params: { id: to.params.id } }),
    },
  ],
}

2. NuxtPage 组件工作原理

2.1 NuxtPage 是什么

&lt;NuxtPage /&gt; 是 Nuxt4 对 Vue Router &lt;RouterView /&gt; 的封装,它做了以下增强:

  • 自动处理页面过渡动画(pageTransition
  • 自动处理 Keep-Alive 缓存
  • 自动处理 Suspense(异步组件加载)
  • 支持嵌套路由的多层级渲染
<!-- app/app.vue -->
<template>
  <NuxtLayout>
    <NuxtPage />  <!-- 渲染当前路由匹配的页面组件 -->
  </NuxtLayout>
</template>

2.2 NuxtPage 的 Props

<template>
  <NuxtPage
    :page-key="route.fullPath"
    :transition="{
      name: 'page',
      mode: 'out-in',
    }"
    :keepalive="{
      max: 10,
      include: ['VideosPage'],
    }"
    :foobar="123"
  />
</template>
Prop类型说明
pageKeystring | ((route) => string)控制页面组件的重新渲染时机
transitionTransitionProps | false页面过渡动画配置
keepaliveKeepAliveProps | boolean页面缓存配置
其他 propsany透传给页面组件

2.3 pageKey 的妙用

pageKey 决定了页面组件何时重新创建(而非复用):

<!-- 场景:视频详情页,从 /videos/1 切换到 /videos/2 -->
 
<!-- ❌ 默认行为:组件复用,只有 route.params 变化 -->
<!-- 如果页面内部没有 watch route.params,内容不会更新 -->
 
<!-- ✅ 用 pageKey 强制重新创建 -->
<NuxtPage :page-key="route => route.fullPath" />
<!-- 或者在页面内部用 definePageMeta -->
<!-- app/pages/videos/[id].vue -->
<script setup lang="ts">
definePageMeta({
  key: route => route.fullPath,  // 每次 id 变化都重新创建组件
})
</script>

2.4 Suspense 集成

&lt;NuxtPage /&gt; 内部使用了 &lt;Suspense&gt;,因此页面组件可以使用顶层 await

<!-- app/pages/videos/[id].vue -->
<script setup lang="ts">
// 顶层 await — Suspense 会等待数据加载完成再渲染
const { data: video } = await useFetch(`/api/videos/${route.params.id}`)
</script>
 
<template>
  <div>
    <h1>{{ video?.title }}</h1>
  </div>
</template>

在数据加载期间,Nuxt 会保持上一个页面的内容(而不是显示空白)。

3. 路由懒加载策略与代码分割

3.1 默认行为:每个页面一个 chunk

Nuxt4 默认为每个页面生成一个独立的 JS chunk:

.output/public/_nuxt/
├── entry.js              (核心框架代码)
├── index.js              (首页)
├── about.js              (关于页)
├── videos.js             (视频列表页)
├── videos-id.js          (视频详情页)
├── studio.js             (创作工作台)
└── admin.js              (管理后台)

这意味着:

  • 用户访问首页时,只下载首页的代码
  • 导航到视频列表页时,才下载 videos.js
  • 未访问的页面代码永远不会被下载

3.2 预加载策略

Nuxt4 通过 &lt;link rel="prefetch"&gt; 自动预加载当前页面中链接指向的页面代码

<!-- 首页中有一个链接指向 /videos -->
<NuxtLink to="/videos">浏览视频</NuxtLink>
 
<!-- Nuxt 会自动在 <head> 中注入 -->
<!-- <link rel="prefetch" href="/_nuxt/videos.js" /> -->

你可以通过 prefetch prop 控制此行为:

<!-- 禁用预加载(移动端节省流量) -->
<NuxtLink to="/videos" :prefetch="false">浏览视频</NuxtLink>
 
<!-- 全局禁用预加载 -->
<!-- nuxt.config.ts: experimental: { defaults: { nuxtLink: { prefetch: false } } } -->

3.3 手动代码分割

对于页面内的大型组件,使用 defineAsyncComponent 或 Nuxt 的 Lazy 前缀:

<template>
  <!-- 方式 1:Lazy 前缀(Nuxt 约定) -->
  <LazyVideoPlayer v-if="showPlayer" :url="videoUrl" />
 
  <!-- 方式 2:defineAsyncComponent -->
  <AsyncHeavyChart v-if="showChart" :data="chartData" />
</template>
 
<script setup lang="ts">
const AsyncHeavyChart = defineAsyncComponent(
  () => import('~/components/HeavyChart.vue')
)
</script>

Lazy 前缀是 Nuxt 的自动功能——任何在 components/ 中注册的组件都可以加 Lazy 前缀,变为异步加载。

4. definePageMeta 完整配置项

4.1 所有配置项

<script setup lang="ts">
definePageMeta({
  // ── 布局 ──
  layout: 'default',              // 指定布局组件
  // layout: false,                // 禁用布局
 
  // ── 中间件 ──
  middleware: 'auth',             // 单个中间件
  // middleware: ['auth', 'admin'], // 多个中间件
  // middleware: (to, from) => {}, // 内联中间件
 
  // ── 页面过渡 ──
  pageTransition: {
    name: 'slide-left',
    mode: 'out-in',
  },
  // pageTransition: false,        // 禁用过渡
 
  // ── 布局过渡 ──
  layoutTransition: {
    name: 'layout',
    mode: 'out-in',
  },
 
  // ── Keep-Alive ──
  keepalive: true,
  // keepalive: { max: 10 },
 
  // ── 页面 Key ──
  key: route => route.fullPath,
 
  // ── 路由校验 ──
  validate: async (route) => {
    return /^\d+$/.test(route.params.id as string)
  },
 
  // ── 页面标题 ──
  title: '视频详情',
 
  // ── 路由别名 ──
  alias: ['/v/:id'],
 
  // ── 重定向 ──
  // redirect: '/new-path',
 
  // ── 自定义 Meta 扩展 ──
  // requiredRole: 'admin',
  // breadcrumb: '视频管理',
})
</script>

4.2 validate — 路由参数校验

validate 在导航到页面之前执行,返回 false 或抛出错误会触发 404 页面:

<script setup lang="ts">
definePageMeta({
  validate: async (route) => {
    const id = route.params.id as string
 
    // 校验格式
    if (!/^\d+$/.test(id)) return false
 
    // 也可以异步校验(如检查资源是否存在)
    // const exists = await $fetch(`/api/videos/${id}/exists`)
    // if (!exists) return false
 
    return true
  },
})
</script>

返回值的不同处理:

// 返回 false → 触发 404 页面
return false
 
// 返回 true → 允许导航
return true
 
// 抛出错误 → 触发错误页面并显示自定义信息
throw createError({
  statusCode: 403,
  statusMessage: '无权访问此视频',
})

4.3 alias — 路由别名

一个页面可以通过多个 URL 访问:

<!-- app/pages/videos/[id].vue -->
<script setup lang="ts">
definePageMeta({
  alias: ['/v/:id', '/video/:id'],
  // /videos/123、/v/123、/video/123 都能访问这个页面
})
</script>

这在 SEO 迁移或兼容旧链接时非常有用。

4.4 definePageMeta 的编译时特性

重要definePageMeta 是一个编译时宏,它的内容在构建时就被提取到路由配置中,因此有以下限制:

<script setup lang="ts">
// ❌ 错误:不能引用外部变量
const role = 'admin'
definePageMeta({
  middleware: role,  // 💥 构建时无法解析变量值
})
 
// ❌ 错误:不能使用导入的值
import { AUTH_MIDDLEWARE } from '~/constants'
definePageMeta({
  middleware: AUTH_MIDDLEWARE,  // 💥 编译时无法解析
})
 
// ✅ 正确:只使用字面量
definePageMeta({
  middleware: 'auth',
  layout: 'admin',
})
 
// ✅ 正确:内联函数可以(运行时执行)
definePageMeta({
  middleware: (to, from) => {
    // 这里可以使用动态逻辑
    if (!useAuth().isLoggedIn.value) return navigateTo('/login')
  },
  validate: async (route) => {
    return /^\d+$/.test(route.params.id as string)
  },
})
</script>

本章小结

  • 文件系统路由pages/ 下的文件路径直接映射为 URL,index.vue 是目录默认页
  • NuxtPage:增强版 &lt;RouterView /&gt;,集成 Suspense、过渡动画、Keep-Alive
  • 代码分割:每个页面自动生成独立 chunk,NuxtLink 自动预加载目标页代码
  • definePageMeta:编译时宏,支持布局/中间件/过渡/校验/别名等全部页面级配置

下一章深入动态路由、嵌套路由和路由分组的高级用法。