Skip to content

文件路由

Nuxt 基于文件系统自动生成路由,无需手动配置路由表。这是 Nuxt 最核心的"约定"——创建文件就是创建路由。

为什么用文件路由?

手动配置路由(Vue Router)文件路由(Nuxt)
创建 .vue 文件创建 .vue 文件
router.ts 中写路由配置不需要,自动生成
配置嵌套路由、动态路由按目录结构约定,自动处理
忘了改路由配置 → 页面 404不会忘,文件在路由就在

核心理念

文件路径 = 路由路径。你只需要关心"页面文件放在哪里",Nuxt 帮你处理路由配置。

基本路由

app/pages/ 目录下的 .vue 文件自动映射为路由:

文件路径路由说明
pages/index.vue/index.vue 是特殊文件名,代表目录的根路径
pages/about.vue/about文件名即路由路径
pages/contact.vue/contact同上
pages/blog/index.vue/blog子目录的首页
pages/blog/hello.vue/blog/hello子目录的子页面

index.vue 的特殊含义

无论在哪个目录下,index.vue 都代表该目录的根路由。就像网站首页总是 index.html 一样。

快速判断

把文件路径中的 app/pages 去掉,把 index.vue 替换为 /,就是路由路径。

动态路由

什么是动态路由?

有些页面的 URL 有一部分是"变化的"——比如博客文章 /blog/123/blog/456,数字部分是文章 ID,但页面结构一样。这种"某一段是变量"的路由就是动态路由。

语法:[参数名]

用方括号包裹参数名,这个文件就能匹配该位置的任意值

text
文件路径                           → 路由模式              → 能匹配的 URL
─────────────────────────────────────────────────────────────────────────
pages/blog/[id].vue               → /blog/:id            → /blog/123
                                                         → /blog/my-post
                                                         → /blog/任何值

pages/user/[name].vue             → /user/:name          → /user/alice
                                                         → /user/bob

pages/category/[slug].vue         → /category/:slug      → /category/tech
                                                         → /category/life

文件命名 → 路由模式的转换规则

文件命名含义对应的路由模式说明
[id].vue这一段是变量,变量名是 id/:id匹配一个路径段
[name].vue这一段是变量,变量名是 name/:name方括号里的名字就是参数名
index.vue这是目录的默认页/无参数
about.vue固定路径/about无参数

简单记忆

[xxx] = "这里可以是任何值",方括号里的 xxx 是你给这个变量起的名字。

路由模式中的 :id 是什么?

这是 Vue Router 的动态路径参数语法(也叫"冒号参数")。/blog/:id 中的 :id 表示"这个位置是一个名为 id 的变量"。Nuxt 的工作就是把你写的 [id].vue 自动转换成 Vue Router 的 /:id 语法——你只需要会写 [id].vue,不需要手写 :id

在页面中获取参数

vue
<!-- app/pages/blog/[id].vue -->
<script setup>
const route = useRoute()
const id = route.params.id   // 类型:string | string[]
</script>

<template>
  <div>
    <h1>博客文章</h1>
    <p>文章 ID: {{ id }}</p>
  </div>
</template>

运行示例

用户访问的 URLroute.params 的值页面显示
/blog/123{ id: '123' }文章 ID: 123
/blog/hello-world{ id: 'hello-world' }文章 ID: hello-world
/blog/❌ 不匹配动态路由至少要有一个值

INFO

常见初学者错误:在 [id].vue 模板中直接使用 id<script> 中没有通过 useRoute() 定义 必须先 const route = useRoute()const id = route.params.id,否则模板中 {{ id }} 会是 undefined

为什么 route.params.idstring 而不是 number

URL 中的所有内容都是字符串。即使你访问 /blog/123id 也是 '123'(string),不是 123(number)。如果需要数字,手动转换:

ts
const id = Number(route.params.id)

INFO

注意route.params 在 SSR 和客户端都能使用 是 SSR 安全的

一个文件中能有多个动态参数吗?

可以!每个路径段都可以独立是动态的:

text
文件路径                                → 路由模式                → 访问示例
──────────────────────────────────────────────────────────────────────────
pages/blog/[category]/[id].vue         → /blog/:category/:id   → /blog/tech/123
pages/shop/[shopId]/product/[pid].vue  → /shop/:shopId/product/:pid → /shop/5/product/42

获取多个参数:

vue
<!-- app/pages/blog/[category]/[id].vue -->
<script setup>
const route = useRoute()
const category = route.params.category  // 'tech'
const id = route.params.id              // '123'
</script>

<template>
  <div>
    <p>分类:{{ category }}</p>
    <p>文章 ID:{{ id }}</p>
  </div>
</template>

典型场景

  • 博客文章:/blog/[id].vue/blog/my-first-post
  • 用户资料:/user/[name].vue/user/alice
  • 商品详情:/product/[slug].vue/product/nuxt-t-shirt

通配路由

什么是通配路由?

动态路由 [id] 只能匹配一个路径段(如 /blog/123),但有时候你不知道 URL 有多少层——比如文档系统 /docs/a/docs/a/b/docs/a/b/c 都要匹配。这就需要通配路由。

语法:[...slug]

在参数名前加 ...,表示"匹配剩余的所有路径段":

text
文件路径                           → 路由模式                → 能匹配的 URL
─────────────────────────────────────────────────────────────────────────────────
pages/docs/[...path].vue           → /docs/:path(.*)*       → /docs/a          ✅
                                                            → /docs/a/b        ✅
                                                            → /docs/a/b/c/d    ✅
                                                            → /docs/任意/多/层  ✅

pages/user/[...slug].vue           → /user/:slug(.*)*       → /user/alice              ✅
                                                            → /user/alice/settings     ✅
                                                            → /user/alice/settings/pwd ✅

[id] vs [...slug] 对比

动态路由 [id]通配路由 [...slug]
语法[参数名][...参数名]
匹配一个路径段任意数量路径段(≥1)
参数类型stringstring[](数组)
路由模式/:id/:slug(.*)*
例子/blog/123 ✅ /blog/1/2/docs/a ✅ /docs/a/b/c
能否匹配空值/blog/ 不匹配/docs/ 不匹配(至少要一段)

INFO

路由模式中的 (.*)* 是什么意思? 这是 Vue Router 内部使用的正则表达式 (.*)* 表示"匹配任意字符零次或多次"。你不需要手写这个——Nuxt 看到 [...slug] 就会自动生成 /:slug(.*)*。你只需要理解

你写的文件名Nuxt 自动生成的路由模式含义
[id].vue/:id匹配 1 个路径段
[...slug].vue/:slug(.*)*匹配任意多个路径段
[[...slug]].vue/:slug(.*)*匹配 0 或多个路径段

在页面中获取通配参数

通配路由的参数是数组,不是字符串:

vue
<!-- app/pages/docs/[...path].vue -->
<script setup>
const route = useRoute()
const pathSegments = route.params.path  // 类型:string[]

// 拼接回完整路径
const fullPath = (pathSegments as string[]).join('/')
</script>

<template>
  <div>
    <h1>文档页面</h1>
    <p>路径段:{{ pathSegments }}</p>
    <p>完整路径:{{ fullPath }}</p>
  </div>
</template>

运行示例

用户访问的 URLroute.params.path页面显示
/docs/getting-started['getting-started']路径段:getting-started
/docs/api/composables['api', 'composables']路径段:api, composables
/docs/a/b/c/d['a', 'b', 'c', 'd']路径段:a, b, c, d

INFO

通配参数是数组:动态路由 [id] 的参数是 string 通配路由 [...slug] 的参数是 string[]。用的时候注意类型差异

ts
// 动态路由 [id]
route.params.id    // string | string[]

// 通配路由 [...slug]
route.params.slug  // string[]  (总是数组)

// 安全取值方式
const segments = Array.isArray(route.params.slug)
? route.params.slug
: [route.params.slug]

可选通配路由:[[...slug]]

双括号 [[...slug]] 表示通配路由是可选的——即使没有额外路径段也能匹配:

text
文件路径                           → 路由模式                → 能匹配的 URL
─────────────────────────────────────────────────────────────────────────────────
pages/docs/[[...path]].vue         → /docs/:path(.*)*       → /docs        ✅ 匹配!
                                                            → /docs/a      ✅
                                                            → /docs/a/b    ✅

[...slug] vs [[...slug]]

必选通配 [...slug]可选通配 [[...slug]]
/docs❌ 不匹配✅ 匹配(slug 为空数组 []
/docs/a✅ 匹配✅ 匹配
参数值至少有一个元素可能是空数组 []

通配路由适合"不确定有多少层级"的场景

比如文档系统可能有 /docs/getting-started,也可能有 /docs/api/composables/use-fetch

三种路由参数语法完整对比

语法文件示例匹配规则参数类型匹配示例不匹配示例
[param][id].vue匹配 1 个路径段string/blog/123{ id: '123' }/blog//blog/a/b
[...param][...slug].vue匹配 ≥1 个路径段string[]/docs/a/b{ slug: ['a', 'b'] }/docs/
[[...param]][[...slug]].vue匹配 ≥0 个路径段string[]/docs{ slug: [] }

嵌套路由

使用同名文件 + 同名目录创建嵌套路由:

text
pages/
├── users/
│   ├── index.vue         → /users(用户列表)
│   └── [id].vue          → /users/:id(用户详情)
└── users.vue             → users 的父组件(必须包含 <NuxtPage />)

嵌套路由的关键

users.vue 文件和 users/ 目录同时存在,Nuxt 就会创建嵌套路由。父组件(users.vue)必须包含 <NuxtPage />,子页面的内容在这里渲染。

pages/users.vue(父组件):

vue
<template>
  <div>
    <h1>用户管理</h1>
    <!-- 子页面(users/index.vue 或 users/[id].vue)在这里渲染 -->
    <NuxtPage />
  </div>
</template>

pages/users/index.vue

vue
<template>
  <div>用户列表</div>
</template>

pages/users/[id].vue

vue
<script setup>
const route = useRoute()
</script>

<template>
  <div>用户详情:{{ route.params.id }}</div>
</template>

嵌套路由的渲染过程

  1. 用户访问 /users/123
  2. Nuxt 渲染 users.vue(父组件)
  3. <NuxtPage /> 的位置渲染 [id].vue(子组件)

父组件始终显示,子组件在父组件的 <NuxtPage /> 位置切换。

什么时候用嵌套路由?

  • 多个页面共享同一个"外壳"(如用户管理的侧边导航)
  • 不想用布局(布局是全局的,嵌套路由是局部的)
  • 子页面之间需要共享状态或导航

嵌套路由的目录结构

text
pages/
├── parent/               → 子路由内容
│   ├── child.vue          → /parent/child
│   └── another.vue        → /parent/another
└── parent.vue             → 父路由(必须包含 <NuxtPage />)

INFO

常见错误:创建了 pages/users/ 目录但忘了创建 pages/users.vue 此时子路由不会嵌套,而是直接渲染

页面命名规则

路由名称根据文件路径自动生成:

文件路径路由名称规则
pages/index.vueindex根页面名就是 index
pages/about.vueabout文件名即路由名
pages/blog/[id].vueblog-id动态参数用 - 代替 /
pages/user/[name].vueuser-name同上

路由名称有什么用?

navigateTorouter.push 中可以用名称导航,而不是硬编码路径:

ts
router.push({ name: 'blog-id', params: { id: '123' } })

好处是:如果路径变了(如 /blog 改为 /articles),只要路由名称不变,代码不用改。

禁用文件路由

如果你不需要文件路由(如单页应用),可以删除 pages/ 目录,只用 app.vue

什么时候不需要文件路由?

  • 纯 SPA 应用(不需要 SSR)
  • 只有一个页面的应用(如登录页)
  • 路由完全由代码控制

删除 pages/ 目录后,<NuxtPage /> 不再需要,app.vue 直接写内容。

自定义路由

如果文件路由不满足需求,可以在 nuxt.config.ts 中使用 hooks 自定义:

ts
export default defineNuxtConfig({
  hooks: {
    'pages:extend'(pages) {
      // 添加自定义路由
      pages.push({
        name: 'custom',
        path: '/custom-route',
        file: '~/app/pages/custom.vue',
      })

      // 移除某个路由
      const index = pages.findIndex(p => p.path === '/remove-me')
      if (index > -1) pages.splice(index, 1)
    },
  },
})

pages:extend 钩子

在 Nuxt 生成路由后、应用启动前执行,你可以修改路由数组。这在以下场景有用:

  • 根据环境变量决定是否注册某个路由
  • 动态添加/移除路由
  • 修改路由的元信息

路由验证

使用 definePageMeta.validate 验证动态参数:

vue
<script setup>
definePageMeta({
  validate: async (route) => {
    // 必须是数字
    return /^\d+$/.test(route.params.id as string)
  },
})
</script>

验证失败会跳转到 404 页面。

为什么需要路由验证?

  • 防止用户输入非法 URL(如 /blog/abc 而不是 /blog/123
  • 提前拦截无效请求,不用在页面逻辑中处理
  • 比在页面中判断更早(在路由层就拦截了)

常见问题

创建了页面但访问 404

  1. 文件是否在 app/pages/ 下(不是根目录的 pages/
  2. app.vue 中是否有 <NuxtPage />
  3. 重启开发服务器

嵌套路由子页面不显示

  1. 是否同时存在 users.vueusers/ 目录
  2. 父组件中是否有 <NuxtPage />
  3. 子页面的路径是否正确

动态路由参数类型不对

route.params 中的值总是 stringstring[],需要手动转换类型。

知识脉络

text
核心概念 → 你在这里:文件路由

              ├─→ 下一步:路由导航

              ├─→ 下一步:路由中间件

              └─→ 相关:视图与布局(05章)

基于 Nuxt 4 官方文档整理编写