基于文件的路由系统原理
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.vue | index | / |
pages/about.vue | about | /about |
pages/videos/index.vue | videos | /videos |
pages/videos/[id].vue | videos-id | /videos/:id |
pages/videos/[id]/edit.vue | videos-id-edit | /videos/:id/edit |
pages/admin/users.vue | admin-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 是什么
<NuxtPage /> 是 Nuxt4 对 Vue Router <RouterView /> 的封装,它做了以下增强:
- 自动处理页面过渡动画(
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 | 类型 | 说明 |
|---|---|---|
pageKey | string | ((route) => string) | 控制页面组件的重新渲染时机 |
transition | TransitionProps | false | 页面过渡动画配置 |
keepalive | KeepAliveProps | boolean | 页面缓存配置 |
| 其他 props | any | 透传给页面组件 |
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 集成
<NuxtPage /> 内部使用了 <Suspense>,因此页面组件可以使用顶层 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 通过 <link rel="prefetch"> 自动预加载当前页面中链接指向的页面代码:
<!-- 首页中有一个链接指向 /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:增强版
<RouterView />,集成 Suspense、过渡动画、Keep-Alive - 代码分割:每个页面自动生成独立 chunk,
NuxtLink自动预加载目标页代码 - definePageMeta:编译时宏,支持布局/中间件/过渡/校验/别名等全部页面级配置
下一章深入动态路由、嵌套路由和路由分组的高级用法。