欢迎光临
我们一直在努力

Vue Router 4 从入门到实战:路由、导航传参、嵌套与守卫完整指南

Vue Router 4 从入门到实战:路由、导航传参、嵌套与守卫完整指南

  • 前言
  • 1. 路由、SPA 与组件分类
    • 1.1 什么是路由
    • 1.2 单页应用 SPA 与多页应用 MPA
    • 1.3 页面组件与复用组件
  • 2. Vue Router 基本使用与模块封装
    • 2.1 先理解“4+2”完整流程
    • 2.2 核心 API、参数与返回值
    • 2.3 创建页面、配置规则并提供出口
    • 2.4 路由的执行机制与空白页排查
    • 2.5 抽离并封装路由模块
  • 3. 声明式导航与路由传参
    • 3.1 RouterLink 与激活样式
    • 3.2 查询参数:适合筛选条件和多个可选值
    • 3.3 动态路由参数:让资源标识成为路径的一部分
    • 3.4 查询参数与动态参数对比
  • 4. 重定向、404 与历史模式
    • 4.1 使用 redirect 避免默认路径空白
    • 4.2 配置 Vue Router 4 的 404 兜底页
    • 4.3 Hash 模式与 HTML5 History 模式
  • 5. 编程式导航与传参
    • 5.1 useRouter、useRoute 与 router.push
    • 5.2 编程式导航携带查询参数与动态参数
  • 6. 嵌套路由:在页面内部继续切换页面
    • 6.1 嵌套路由的结构与渲染流程
    • 6.2 children、相对 path 与默认子页面
  • 7. 路由守卫与访问权限
    • 7.1 beforeEach 的执行时机、参数与返回值
    • 7.2 从布尔判断到可维护的登录拦截
  • 8. 完整项目整合与运行复盘
    • 8.1 项目目标与文件结构
    • 8.2 整合路由表、嵌套关系与守卫
    • 8.3 整合入口、导航和页面组件
    • 8.4 从启动到导航完成的执行链
  • 总结

前言

在传统多页面网站中,用户点击链接后,浏览器通常会向服务器请求一个新的 HTML 页面;而在 Vue 单页应用中,页面的外壳通常始终是同一个 HTML,真正变化的是页面内部被渲染的组件。于是,一个关键问题出现了:浏览器地址变化后,应用应该显示哪个组件?

Vue Router 正是用来解决这个问题的。它把 URL 与 Vue 页面组件建立映射,并提供导航、参数传递、重定向、嵌套路由、历史记录模式和权限守卫等能力。掌握 Vue Router 不能只停留在“会复制配置”,还要理解浏览器地址、路由表、当前路由对象和 <router-view> 之间如何协作。

本文使用 Vue 3 + Vue Router 4 + <script setup> 语法,从基础概念一路讲到完整项目整合。阅读完成后,不但可以独立搭建路由模块,还能判断查询参数与动态参数的使用边界,处理刷新 404、二级页面空白、命名路由传参和登录拦截等常见问题。

说明: 由于 CSDN 对 Vue 代码块的语法高亮支持存在一定限制,本文中的 Vue 模板代码统一使用 html 类型进行代码块标注,仅用于优化代码高亮显示效果,不影响代码内容及实际运行结果。

1. 路由、SPA 与组件分类

1.1 什么是路由

生活中的路由器维护着某种“去往哪里”的映射关系:数据到达路由器后,设备会依据网络地址决定下一步把数据转发到哪里。前端路由也采用了相似思想,只不过映射两端从“网络地址与设备”变成了“URL 路径与页面组件”。

前端路由是一组 URL 与页面内容之间的映射规则。URL 发生变化时,路由器查找匹配规则,并把对应组件渲染到指定出口。

例如,应用可以维护下面这组关系:

URL 路径页面组件页面含义
/find Find.vue 发现音乐
/my My.vue 我的音乐
/friend Friend.vue 朋友

Vue 本身负责组件和响应式视图,Vue Router 是 Vue 官方提供的客户端路由解决方案。它作为单独的模块安装,却能够通过 Vue 插件机制注册到应用中。注册成功后,应用就可以使用 <RouterLink>、<RouterView>、useRoute()、useRouter() 等能力。

一次典型的路由切换包含四个角色:

  • 浏览器 URL:描述用户当前要访问的位置。
  • 路由表 routes:保存 path 与 component 的映射规则。
  • 路由器实例 router:监听导航、解析 URL、执行守卫并计算匹配结果。
  • 路由出口 <RouterView>:接收匹配结果并渲染对应组件。

1.2 单页应用 SPA 与多页应用 MPA

SPA 是 Single Page Application 的缩写,中文叫单页应用。这里的“单页”不是说应用只有一个功能页面,而是说多个业务页面通常共享同一个 HTML 入口。用户切换功能时,JavaScript 更新当前页面中的组件,不必每次都加载一份全新的 HTML。

MPA 是 Multi Page Application 的缩写。它通常由多个 HTML 页面构成,访问不同功能时会请求不同的页面文档。传统资讯站、服务端模板站点经常采用这种方式。

两种模式并没有绝对的高下之分,关键在于业务需求:

对比维度SPA 单页应用MPA 多页应用
HTML 组织 通常只有一个 HTML 入口 通常有多个 HTML 页面
页面更新 按需替换组件或局部内容 常见做法是加载新的完整页面
页面切换体验 切换流畅,容易保留应用状态 会发生文档级导航,状态通常需要重新恢复
前后端协作 前后端职责分离清晰,前端工程化程度高 可以由服务端直接组织页面,链路直观
首屏加载 初始资源较多时可能偏慢,可通过拆包和懒加载优化 单个页面可按请求输出,首屏通常更直接
SEO 纯客户端渲染不占优势,可借助 SSR 或 SSG 改善 服务端直接输出完整内容,通常更利于抓取
适合场景 后台系统、交互密集型产品、Web 应用 内容站、页面相对独立的网站、服务端模板项目

SPA 的本质是“一个 HTML 入口承载多个客户端视图”,Vue Router 则负责让 URL 与这些视图保持同步。

1.3 页面组件与复用组件

Vue 并不强制把 .vue 文件分成两类,但在工程实践中,人们通常按职责划分目录,以降低维护成本:

类型常用目录主要职责是否直接参与路由配置复用倾向
页面组件 src/views 表示一个相对完整的业务页面 通常较低
复用组件 src/components 组装页面,例如导航栏、列表项、弹窗 通常不是 通常较高

这种分类只是工程约定,两者本质上都是 Vue 组件。Find.vue 放入 views,是因为它会直接与 /find 这样的地址建立映射;SongCard.vue 放入 components,是因为它可能同时出现在发现页、歌单页和搜索页中。

初始目录可以这样组织:

src/
├── components/
│ └── SongCard.vue
├── views/
│ ├── Find.vue
│ ├── Friend.vue
│ └── My.vue
├── App.vue
└── main.js

2. Vue Router 基本使用与模块封装

2.1 先理解“4+2”完整流程

第一次接触 Vue Router,可以先用“4 个固定步骤 + 2 个核心步骤”建立全局认识。

四个固定步骤是:

  • 安装 vue-router。
  • 导入创建路由所需的函数。
  • 创建路由器实例。
  • 使用 app.use(router) 注册路由插件。
  • 两个核心步骤是:

  • 创建页面组件,并在 routes 中配置 URL 与组件的映射关系。
  • 在合适位置放置 <RouterView>,为匹配到的组件提供渲染出口。
  • 阶段复盘如下:

    阶段要做什么缺少后的结果
    安装 把 Vue Router 加入项目依赖 无法导入路由 API
    导入 引入 createRouter 和历史模式函数 无法创建路由器
    创建 提供 history 与 routes 没有可运行的路由规则
    注册 执行 app.use(router) 路由组件和组合式 API 无法正常工作
    配规则 建立 path -> component 映射 URL 无法匹配页面
    给出口 放置 <RouterView> 虽然能匹配路由,但页面没有显示位置

    安装 Vue Router 4:

    npm install vue-router@4

    如果项目使用 Yarn 或 pnpm,对应命令是 yarn add vue-router@4 或 pnpm add vue-router@4。

    2.2 核心 API、参数与返回值

    在写完整代码前,先把核心 API 的职责分清:

    API 或配置作用主要参数返回值或结果常见错误
    createRouter(options) 创建路由器实例 options.history、options.routes 等 Router 实例 忘记提供 history,或把 Vue Router 3 的 new VueRouter() 写法混进来
    createWebHashHistory(base?) 创建 Hash 历史模式 可选基础路径 base RouterHistory 误以为 Hash URL 不带 #
    createWebHistory(base?) 创建 HTML5 History 模式 可选基础路径 base RouterHistory 线上服务器没有配置回退到 index.html
    routes 保存路由记录 由路由记录对象组成的数组 被路由器解析为匹配器 把它误写成单个对象,或写错 component 引用
    app.use(router) 把路由插件注册到 Vue 应用 路由器实例 返回 Vue 应用实例,便于链式调用 在 app.mount() 之后才注册
    <RouterView> 渲染当前层级匹配到的组件 常规基础用法不需要参数 输出匹配组件的视图 忘记添加,导致地址变化但页面空白

    createRouter() 的核心配置可以写成下面这样:

    import { createRouter, createWebHashHistory } from 'vue-router'

    const router = createRouter({
    history: createWebHashHistory(),
    routes: []
    })

    这里的 history 决定 URL 如何与浏览器历史记录协作,routes 决定哪些 URL 可以匹配哪些组件。createRouter() 会返回一个路由器实例,后续的导航、守卫和注册都围绕这个实例进行。

    2.3 创建页面、配置规则并提供出口

    先创建三个最小页面组件。它们结构相同,但代表三个独立页面:

    <!– src/views/Find.vue –>
    <template>
    <section class="find">
    <h2>发现音乐</h2>
    <p>这里展示推荐内容。</p>
    </section>
    </template>

    <!– src/views/My.vue –>
    <template>
    <section class="my">
    <h2>我的音乐</h2>
    <p>这里展示用户收藏的音乐。</p>
    </section>
    </template>

    <!– src/views/Friend.vue –>
    <template>
    <section class="friend">
    <h2>朋友</h2>
    <p>这里展示朋友动态。</p>
    </section>
    </template>

    为了先看清完整链路,可以暂时在 main.js 中创建路由:

    import { createApp } from 'vue'
    import { createRouter, createWebHashHistory } from 'vue-router'
    import App from './App.vue'

    import Find from '@/views/Find.vue'
    import Friend from '@/views/Friend.vue'
    import My from '@/views/My.vue'

    const router = createRouter({
    history: createWebHashHistory(),
    routes: [
    { path: '/find', component: Find },
    { path: '/my', component: My },
    { path: '/friend', component: Friend }
    ]
    })

    const app = createApp(App)

    app.use(router)
    app.mount('#app')

    然后在根组件中提供一级路由出口:

    <!– src/App.vue –>
    <template>
    <main>
    <RouterView />
    </main>
    </template>

    启动项目后访问 /#/find,路由器会把 Find.vue 渲染到 App.vue 的 <RouterView> 位置。使用 createWebHashHistory() 时,地址中的 # 是预期结果,它表示当前应用采用 Hash 模式。

    2.4 路由的执行机制与空白页排查

    当用户访问 /#/friend 时,Vue Router 的执行过程可以概括为:

  • 从 URL 中读取客户端路径 /friend。
  • 遍历并匹配 routes 中的路由记录。
  • 命中 { path: '/friend', component: Friend }。
  • 运行与本次导航相关的守卫。
  • 更新当前路由对象。
  • 把 Friend 组件渲染到当前层级的 <RouterView>。
  • URL 决定“要去哪里”,路由表决定“那里对应什么组件”,<RouterView> 决定“组件显示在哪里”。

    页面空白通常不是 Vue Router “没有反应”,而是链路中的某一环断开了:

    现象可能原因排查方式
    地址变化但内容不显示 缺少 <RouterView> 检查当前布局组件是否提供出口
    访问某路径后空白 routes 中没有匹配项 对照地址和 path,检查大小写与斜杠
    控制台提示组件未定义 页面组件没有正确导入 检查导入路径和 component 变量
    <RouterLink> 不识别 未执行 app.use(router) 确保注册发生在 app.mount() 前
    根路径 / 打开后空白 没有为 / 配置组件或重定向 添加首页路由或 redirect

    2.5 抽离并封装路由模块

    把全部路由代码长期堆在 main.js 中,会让应用入口同时承担创建应用、配置路由、权限控制等多种职责。更常见的做法是新建 src/router/index.js,让入口文件只负责组装应用。

    // src/router/index.js
    import { createRouter, createWebHashHistory } from 'vue-router'
    import Find from '@/views/Find.vue'
    import Friend from '@/views/Friend.vue'
    import My from '@/views/My.vue'

    const router = createRouter({
    history: createWebHashHistory(),
    routes: [
    { path: '/find', component: Find },
    { path: '/my', component: My },
    { path: '/friend', component: Friend }
    ]
    })

    export default router

    // src/main.js
    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'

    createApp(App)
    .use(router)
    .mount('#app')

    脚手架项目中,@ 通常被配置为 src 目录的别名,因此 @/views/Find.vue 等价于从 src/views/Find.vue 导入。需要注意,@ 并不是 JavaScript 原生语法,它来自 Vite 或构建工具的别名配置;如果自建工程没有配置别名,就应使用相对路径,或先在构建配置中声明别名。

    3. 声明式导航与路由传参

    3.1 RouterLink 与激活样式

    让用户手动修改地址栏显然不现实。Vue Router 提供全局组件 <RouterLink> 实现声明式导航:开发者在模板中声明目标地址,组件负责生成链接并触发客户端路由切换。

    <template>
    <nav>
    <RouterLink to="/find">发现音乐</RouterLink>
    <RouterLink to="/my">我的音乐</RouterLink>
    <RouterLink to="/friend">朋友</RouterLink>
    </nav>

    <RouterView />
    </template>

    RouterLink 默认会渲染为可访问的 <a> 元素,但点击后由 Vue Router 接管导航,通常不会重新加载整份 HTML。它最重要的属性是 to:

    写法to 的类型适用场景示例
    静态字符串 string 目标地址固定 to="/find"
    动态字符串 JavaScript 表达式 地址由变量拼接 :to="'/friend/' + id"
    路由位置对象 object 使用 name、query、params 等结构化配置 :to="{ name: 'Friend' }"

    导航成功后,匹配链接会自动获得两个类名:

    类名含义嵌套路由中的表现
    router-link-active 当前链接处于激活状态 当前地址匹配子路由时,祖先路由链接也可能激活
    router-link-exact-active 当前链接被精确激活 只标记最精确匹配的链接,不包含祖先路由

    初学时常把二者理解为“模糊匹配”和“精确匹配”。更准确地说,Vue Router 会比较路由记录和路径参数;当子路由激活时,它的祖先路由记录也属于匹配链,因此父级链接拥有 router-link-active,但通常没有 router-link-exact-active。

    <style>
    nav a {
    color: #333;
    text-decoration: none;
    margin-right: 24px;
    }

    nav a.router-link-active {
    color: #fff;
    background: #e5484d;
    }

    nav a.router-link-exact-active {
    font-weight: 700;
    }
    </style>

    如果只需要普通导航高亮,给其中一个类设置样式即可。也可以通过 active-class、exact-active-class 修改单个链接的类名,或在 createRouter() 中使用 linkActiveClass、linkExactActiveClass 进行全局配置。

    3.2 查询参数:适合筛选条件和多个可选值

    查询参数位于 URL 的 ? 后面,例如 /friend?id=10086&tab=post。它适合搜索关键字、分页、排序、筛选条件等信息,因为参数名清晰、数量灵活,而且不需要提前在 path 中占位。

    字符串传参:

    <RouterLink to="/friend?id=10086&tab=post">
    查看朋友动态
    </RouterLink>

    对象传参:

    <RouterLink
    :to="{
    path: '/friend',
    query: {
    id: 10086,
    tab: 'post'
    }
    }"

    >

    查看朋友动态
    </RouterLink>

    对象形式不需要手动拼接 ? 和 &,参数较多时可读性更好。目标组件通过 useRoute() 获取当前路由,再从 route.query 中读取参数:

    <!– src/views/Friend.vue –>
    <script setup>
    import { useRoute } from 'vue-router'

    const route = useRoute()
    </script>

    <template>
    <section>
    <h2>朋友动态</h2>
    <p>用户 ID:{{ route.query.id }}</p>
    <p>当前标签:{{ route.query.tab }}</p>
    </section>
    </template>

    useRoute() 不接收参数,返回当前路由位置的响应式对象。常用字段如下:

    字段含义示例结果
    route.path 不带查询串的路径 /friend
    route.fullPath 包含查询串和 Hash 的完整路径 /friend?id=10086#comment
    route.query 查询参数对象 { id: '10086' }
    route.params 动态路径参数对象 { fid: '10086' }
    route.name 当前命名路由名称 Friend
    route.meta 路由记录合并后的元信息 { requiresAuth: true }

    需要特别注意:URL 中的查询值通常是字符串或字符串数组。即使传入 id: 10086,读取时也可能得到 '10086'。业务需要数值时,应显式执行 Number(route.query.id),并处理参数缺失或转换失败的情况。

    3.3 动态路由参数:让资源标识成为路径的一部分

    如果多个页面具有相同结构,只是资源 ID 不同,例如朋友 10010 与朋友 10086 的详情页,就没有必要为每个人配置一条路由。可以在路径中使用以 : 开头的动态字段:

    const routes = [
    {
    path: '/friend/:fid',
    name: 'FriendDetail',
    component: Friend
    }
    ]

    这里的 :fid 是参数占位符,参数名应有业务语义。/friend/10010 和 /friend/10086 都能命中同一条路由,实际值分别进入 route.params.fid。

    字符串传参:

    <RouterLink to="/friend/10010">
    查看朋友 10010
    </RouterLink>

    对象传参:

    <RouterLink
    :to="{
    name: 'FriendDetail',
    params: {
    fid: 10010
    }
    }"

    >

    查看朋友 10010
    </RouterLink>

    目标页面接收参数:

    <script setup>
    import { useRoute } from 'vue-router'

    const route = useRoute()
    </script>

    <template>
    <p>当前朋友 ID:{{ route.params.fid }}</p>
    </template>

    对象写法中,params 应与命名路由 name 配合。不要写成 { path: '/friend', params: { fid: 10010 } } 并期待 Vue Router 自动拼接路径,因为指定 path 时 params 会被忽略。要么使用 { name, params },要么直接把具体值写进字符串路径。

    当 /friend/10010 切换到 /friend/10086 时,Vue Router 可能复用同一个 Friend 组件实例。route 本身具有响应性,模板会更新;如果还要根据新 ID 重新请求数据,可以监听 () => route.params.fid,或使用 onBeforeRouteUpdate() 处理参数变化。

    3.4 查询参数与动态参数对比

    两种传参方式都可以携带一个或多个值,区别主要在于参数的业务含义和 URL 结构:

    对比项查询参数 query动态参数 params
    URL 示例 /friend?id=10086&tab=post /friend/10086
    是否提前配置占位符 不需要 需要配置 /friend/:fid
    声明式对象写法 { path, query } 或 { name, query } 推荐 { name, params }
    接收方式 route.query.id route.params.fid
    适合内容 搜索、分页、排序、多个可选筛选项 用户 ID、文章 ID 等资源身份
    参数缺失 通常仍能访问原路径 必填参数缺失时通常无法匹配对应路由
    多参数能力 支持,表达较直观 也支持,如 /user/:uid/post/:pid

    选择参数方式时,不要只看“参数有几个”。决定因素应是:这个值是在描述资源身份,还是在补充筛选与展示条件。

    4. 重定向、404 与历史模式

    4.1 使用 redirect 避免默认路径空白

    应用第一次打开时,浏览器常处于根路径 /。如果路由表只配置了 /find、/my 和 /friend,根路径就没有匹配组件。最简单的处理方式是添加重定向:

    const routes = [
    { path: '/', redirect: '/find' },
    { path: '/find', component: Find }
    ]

    redirect 表示“命中当前路由后,创建一次到目标位置的新导航”。它既可以是字符串,也可以是路由位置对象或返回路由位置的函数:

    写法示例适用场景
    字符串 redirect: '/find' 固定目标路径
    对象 redirect: { name: 'Find' } 使用命名路由,降低路径耦合
    函数 redirect: to => ({ path: '/search', query: { q: to.params.text } }) 目标地址依赖原路由参数

    重定向与别名不同:重定向会让当前地址进入新的目标路由;别名则允许多个 URL 命中同一个路由记录。处理默认首页时通常应该使用 redirect。

    4.2 配置 Vue Router 4 的 404 兜底页

    当用户访问不存在的地址时,应该显示明确的 404 页面,而不是空白。先创建页面:

    <!– src/views/NotFound.vue –>
    <template>
    <section>
    <h2>404</h2>
    <p>你访问的页面不存在。</p>
    <RouterLink to="/">返回首页</RouterLink>
    </section>
    </template>

    再把捕获所有地址的路由放入路由表:

    import NotFound from '@/views/NotFound.vue'

    const routes = [
    // 其他业务路由……
    {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound
    }
    ]

    /:pathMatch(.*)* 是 Vue Router 4 的捕获所有路径写法:pathMatch 是参数名,(.*) 是自定义匹配表达式,最后的 * 表示参数可重复。Vue Router 3 中常见的 { path: '*' } 不应直接照搬到 Vue Router 4 项目。

    虽然路由匹配器能够按规则判断优先级,但把 404 记录放在业务路由之后更符合阅读习惯,也能清楚表达“前面全部未匹配时才进入兜底页”的意图。

    4.3 Hash 模式与 HTML5 History 模式

    Vue Router 4 常用的浏览器历史模式有两种:

    import {
    createRouter,
    createWebHashHistory,
    createWebHistory
    } from 'vue-router'

    const hashRouter = createRouter({
    history: createWebHashHistory(),
    routes: []
    })

    const historyRouter = createRouter({
    history: createWebHistory(),
    routes: []
    })

    对比项Hash 模式HTML5 History 模式
    创建函数 createWebHashHistory() createWebHistory()
    URL 示例 https://example.com/#/find https://example.com/find
    是否带 #
    服务器是否收到客户端路径 # 后内容不会作为请求路径发给服务器 会直接请求 /find
    部署要求 通常无需为前端路由做特殊回退 服务器必须把未知前端路径回退到 index.html
    SEO 与 URL 观感 URL 不够自然,SEO 通常较弱 URL 自然,更适合常规 Web 部署

    History 模式最常见的坑是:站内点击一切正常,刷新二级地址却返回服务器 404。原因是站内导航由 Vue Router 处理,而刷新时浏览器会直接向服务器请求 /find。服务器如果把它当成真实文件路径,自然找不到资源。

    解决原则是:先让服务器查找静态文件和后端接口;如果请求不属于它们,就返回 SPA 的 index.html,再由 Vue Router 在浏览器中解析路径。Vite 开发服务器通常已经处理了回退,所以开发环境正常并不能证明生产环境配置正确。

    5. 编程式导航与传参

    5.1 useRouter、useRoute 与 router.push

    <RouterLink> 适合用户点击链接的场景,但登录成功自动回首页、提交表单后进入详情、倒计时结束后跳转等行为,需要 JavaScript 主动发起导航,这就是编程式导航。

    在 <script setup> 中,通过 useRouter() 获取路由器实例:

    <script setup>
    import { useRouter } from 'vue-router'

    const router = useRouter()

    function goToFriend() {
    router.push('/friend')
    }
    </script>

    <template>
    <button @click="goToFriend">去朋友页</button>
    </template>

    三个容易混淆的对象需要明确区分:

    对象或 API关注点典型操作返回值
    useRouter() 整个路由器 push、replace、back、注册守卫 Router 实例
    useRoute() 当前已经激活的路由位置 读取 path、query、params、meta 响应式路由对象
    router.push(to) 发起一次导航,并增加历史记录 传字符串或路由位置对象 Promise,可等待导航完成

    router 是“导航控制器”,route 是“当前地址快照的响应式表示”。要跳转用 router,要读取当前参数用 route。

    router.push() 的目标既可以是字符串,也可以是对象:

    router.push('/friend')

    router.push({ path: '/friend' })

    router.push({ name: 'FriendQuery' })

    因为 router.push() 返回 Promise,在关闭弹窗、显示成功提示等操作必须等待导航完成时,可以使用 await router.push(…)。如果不想在浏览器历史中新增记录,可改用 router.replace(…);用户按后退键时不会回到被替换的位置。

    5.2 编程式导航携带查询参数与动态参数

    编程式导航与声明式导航使用相同的路由位置结构。查询参数可以用字符串或对象传递:

    router.push('/friend?id=110&tab=post')

    router.push({
    path: '/friend',
    query: {
    id: 101,
    tab: 'post'
    }
    })

    目标页依然通过 route.query.id 和 route.query.tab 接收。动态参数同样有两种形式:

    router.push('/friend/110')

    router.push({
    name: 'FriendDetail',
    params: {
    fid: 101
    }
    })

    目标页通过 route.params.fid 接收。无论导航是由 <RouterLink> 还是 router.push() 发起,接参方式完全一致,因为参数最终都会进入同一个当前路由对象。

    下面用登录成功跳转展示一个真实场景:

    <script setup>
    import { useRouter } from 'vue-router'

    const router = useRouter()

    async function handleLogin() {
    const loginSucceeded = true

    if (loginSucceeded) {
    await router.push({ path: '/find', query: { welcome: '1' } })
    }
    }
    </script>

    <template>
    <button @click="handleLogin">登录</button>
    </template>

    常见错误可以集中记忆:

    错误写法或现象原因正确处理
    在 setup 中写 this.$router <script setup> 中没有组件 this 使用 useRouter()
    写 { path: '/friend', params: { fid: 1 } } path 与 params 的组合不会自动插值 使用 { name, params } 或字符串完整路径
    读取 router.query 查询参数属于当前路由对象 使用 route.query
    参数变化但数据没有重新请求 同一路由组件可能被复用 监听具体参数或使用 onBeforeRouteUpdate()
    重复点击当前地址后仍执行后续逻辑 没有等待导航结果或判断导航失败 按业务需要 await router.push() 并处理结果

    6. 嵌套路由:在页面内部继续切换页面

    6.1 嵌套路由的结构与渲染流程

    许多应用都存在“页面中还有子页面”的结构。例如,一级导航进入“发现音乐”后,页面内部还要在“推荐、排行榜、歌单”之间切换。对应 URL 可以设计为:

    /find
    ├── /find/recommend
    ├── /find/ranking
    └── /find/songlist

    此时会存在两层出口:

    App.vue 的 <RouterView>
    └── 渲染 Find.vue
    └── Find.vue 的 <RouterView>
    ├── 渲染 Recommend.vue
    ├── 渲染 Ranking.vue
    └── 渲染 SongList.vue

    每一层路由记录对应一层 <RouterView>。父路由组件先进入外层出口,子路由组件再进入父组件内部的出口。

    实现嵌套路由需要三步:

  • 创建子页面组件 Recommend.vue、Ranking.vue 和 SongList.vue。
  • 在父路由记录中通过 children 配置子路由。
  • 在父页面 Find.vue 中放置子导航和内层 <RouterView>。
  • 6.2 children、相对 path 与默认子页面

    路由配置如下:

    import Find from '@/views/Find.vue'
    import Recommend from '@/views/Recommend.vue'
    import Ranking from '@/views/Ranking.vue'
    import SongList from '@/views/SongList.vue'

    const routes = [
    {
    path: '/find',
    component: Find,
    redirect: '/find/recommend',
    children: [
    {
    path: 'recommend',
    component: Recommend
    },
    {
    path: 'ranking',
    component: Ranking
    },
    {
    path: 'songlist',
    component: SongList
    }
    ]
    }
    ]

    children 接收一个路由记录数组,结构与顶层 routes 类似。子路由的 path 推荐写成不带 / 的相对路径:父路径 /find 与子路径 recommend 会组合成 /find/recommend。

    如果子路径写成 /recommend,它会被当作根路径,不再自动拼接父路径。这个能力有特殊用途,但不适合当前“URL 与组件层级一致”的场景。

    redirect: '/find/recommend' 用于处理 /find:父组件虽然能够显示,但没有子路由命中时,父组件内部的 <RouterView> 会是空的。重定向到默认子页面即可避免这类“二级区域空白”。另一种做法是配置 { path: '', component: Recommend } 作为空路径子路由,两种方案的地址表现不同:

    方案访问 /find 后的地址内层显示结果适合场景
    父路由 redirect 变为 /find/recommend 推荐页 希望 URL 明确体现默认栏目
    空路径子路由 保持 /find 推荐页 希望父路径本身就是默认子页地址

    父页面中的导航需要指向完整目标地址,并提供子路由出口:

    <!– src/views/Find.vue –>
    <template>
    <section class="find">
    <h2>发现音乐</h2>

    <nav>
    <RouterLink to="/find/recommend">推荐</RouterLink>
    <RouterLink to="/find/ranking">排行榜</RouterLink>
    <RouterLink to="/find/songlist">歌单</RouterLink>
    </nav>

    <RouterView />
    </section>
    </template>

    嵌套路由最常见的三个问题正好对应三条规则:

    问题原因规则
    子页面变成根级地址 子 path 错写成 /ranking 常规嵌套场景下子 path 不加 /
    点击后没有进入预期子页 导航目标漏写父路径 使用 /find/ranking 等完整路径,或配置命名子路由
    访问父路径时内层空白 没有默认子路由 添加父级重定向或空路径子路由

    7. 路由守卫与访问权限

    7.1 beforeEach 的执行时机、参数与返回值

    “我的音乐”通常只有登录用户可以访问。权限判断不能只写在页面按钮上,因为用户还可以直接输入 URL。Vue Router 提供导航守卫,在路由真正确认前统一执行检查。

    全局前置守卫使用 router.beforeEach() 注册:

    router.beforeEach((to, from) => {
    // 根据条件返回不同结果
    })

    这个 API 接收一个守卫函数。每次导航触发时,守卫按注册顺序执行;异步守卫完成前,导航会保持等待。beforeEach() 的返回值是一个移除该守卫的函数,常规应用通常不需要手动移除。

    守卫参数与回调返回值如下:

    项目含义常见用途
    to 即将进入的标准化目标路由 判断目标名称、路径、参数和 meta
    from 当前正要离开的标准化路由 记录来源、判断返回逻辑
    return false 取消当前导航 权限不足且希望停留原页
    return true 或 return undefined 放行本次导航 校验通过;也可以直接不写 return
    return '/login' 重定向到字符串路径 简单登录跳转
    return { name: 'Login' } 重定向到路由位置对象 推荐用于命名路由及附加查询参数

    路由守卫不是普通事件监听器。它必须明确“取消、放行或重定向”,否则错误的分支结构可能造成导航悬空或无限重定向。

    7.2 从布尔判断到可维护的登录拦截

    先看一个最小示例。isLogin 用于模拟登录状态;未登录且准备访问 /my 时取消导航:

    const isLogin = true

    router.beforeEach((to) => {
    if (!isLogin && to.path === '/my') {
    window.alert('请先登录')
    return false
    }

    return true
    })

    这个示例能解释返回值,却不适合页面较多的项目。更可维护的做法是用路由元信息 meta 标记是否需要登录,再统一重定向到登录页:

    const routes = [
    {
    path: '/my',
    name: 'My',
    component: My,
    meta: {
    requiresAuth: true
    }
    },
    {
    path: '/login',
    name: 'Login',
    component: Login
    }
    ]

    router.beforeEach((to) => {
    const isLogin = localStorage.getItem('isLogin') === '1'

    if (to.meta.requiresAuth && !isLogin) {
    return {
    name: 'Login',
    query: {
    redirect: to.fullPath
    }
    }
    }
    })

    这里把原目标写入 redirect 查询参数,登录成功后即可返回用户原本想去的页面。守卫没有显式返回时就是 undefined,表示正常放行。

    务必避免下面这种无限重定向逻辑:未登录时访问任何页面都跳登录页,而登录页本身再次被拦截。可通过 meta.requiresAuth 只保护必要页面,或额外判断 to.name !== 'Login'。

    前端守卫改善的是页面访问流程,不能替代后端鉴权。敏感数据接口仍必须在服务端验证令牌或会话,不能因为前端隐藏了页面就直接返回私密数据。

    8. 完整项目整合与运行复盘

    8.1 项目目标与文件结构

    现在把前面的知识整合成一个最小音乐站路由项目。完整流程是:应用启动后注册路由;根路径重定向到推荐页;一级导航通过 <RouterLink> 切换;朋友页同时演示查询参数和动态参数;发现页包含三条子路由;我的音乐由全局前置守卫保护;未知路径进入 404 页面。

    src/
    ├── router/
    │ └── index.js
    ├── views/
    │ ├── Find.vue
    │ ├── Friend.vue
    │ ├── Login.vue
    │ ├── My.vue
    │ ├── NotFound.vue
    │ ├── Ranking.vue
    │ ├── Recommend.vue
    │ └── SongList.vue
    ├── App.vue
    └── main.js

    选择 History 模式是为了获得自然 URL;若部署环境暂时无法配置服务器回退,把 createWebHistory() 换成 createWebHashHistory() 即可。

    8.2 整合路由表、嵌套关系与守卫

    // src/router/index.js
    import { createRouter, createWebHistory } from 'vue-router'

    import Find from '@/views/Find.vue'
    import Friend from '@/views/Friend.vue'
    import Login from '@/views/Login.vue'
    import My from '@/views/My.vue'
    import NotFound from '@/views/NotFound.vue'
    import Ranking from '@/views/Ranking.vue'
    import Recommend from '@/views/Recommend.vue'
    import SongList from '@/views/SongList.vue'

    const routes = [
    {
    path: '/',
    redirect: '/find/recommend'
    },
    {
    path: '/find',
    name: 'Find',
    component: Find,
    redirect: '/find/recommend',
    children: [
    {
    path: 'recommend',
    name: 'Recommend',
    component: Recommend
    },
    {
    path: 'ranking',
    name: 'Ranking',
    component: Ranking
    },
    {
    path: 'songlist',
    name: 'SongList',
    component: SongList
    }
    ]
    },
    {
    path: '/friend',
    name: 'FriendQuery',
    component: Friend
    },
    {
    path: '/friend/:fid',
    name: 'FriendDetail',
    component: Friend
    },
    {
    path: '/my',
    name: 'My',
    component: My,
    meta: {
    requiresAuth: true
    }
    },
    {
    path: '/login',
    name: 'Login',
    component: Login
    },
    {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound
    }
    ]

    const router = createRouter({
    history: createWebHistory(),
    routes
    })

    router.beforeEach((to) => {
    const isLogin = localStorage.getItem('isLogin') === '1'

    if (to.meta.requiresAuth && !isLogin) {
    return {
    name: 'Login',
    query: {
    redirect: to.fullPath
    }
    }
    }
    })

    export default router

    这段配置的关键点可以用一张表复盘:

    配置解决的问题最终表现
    根路径 redirect 首次访问 / 没有页面 自动进入 /find/recommend
    children 发现页内部继续切换栏目 子组件进入 Find.vue 的出口
    FriendQuery 查询参数演示 /friend?tab=post
    FriendDetail 动态参数演示 /friend/10086
    meta.requiresAuth 标记受保护页面 /my 会被守卫检查
    beforeEach 统一执行登录校验 未登录跳到 /login
    /:pathMatch(.*)* 捕获未知地址 渲染 404 页面

    8.3 整合入口、导航和页面组件

    应用入口只负责安装路由并挂载应用:

    // src/main.js
    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'

    createApp(App)
    .use(router)
    .mount('#app')

    根组件提供一级导航和一级出口:

    <!– src/App.vue –>
    <template>
    <header>
    <h1>简易音乐站</h1>

    <nav>
    <RouterLink to="/find">发现音乐</RouterLink>
    <RouterLink
    :to="{
    name: 'FriendQuery',
    query: { tab: 'post', page: 1 }
    }"

    >

    朋友
    </RouterLink>
    <RouterLink to="/my">我的音乐</RouterLink>
    </nav>
    </header>

    <main>
    <RouterView />
    </main>
    </template>

    <style>
    nav {
    display: flex;
    gap: 16px;
    }

    .router-link-active {
    color: #e5484d;
    font-weight: 700;
    }
    </style>

    发现页负责二级导航和二级出口:

    <!– src/views/Find.vue –>
    <template>
    <section>
    <h2>发现音乐</h2>

    <nav>
    <RouterLink to="/find/recommend">推荐</RouterLink>
    <RouterLink to="/find/ranking">排行榜</RouterLink>
    <RouterLink to="/find/songlist">歌单</RouterLink>
    </nav>

    <RouterView />
    </section>
    </template>

    三个子页面保持简单,让重点落在路由结构上:

    <!– src/views/Recommend.vue –>
    <template>
    <article>
    <h3>推荐</h3>
    <p>展示个性化推荐歌曲。</p>
    </article>
    </template>

    <!– src/views/Ranking.vue –>
    <template>
    <article>
    <h3>排行榜</h3>
    <p>展示热门歌曲排行榜。</p>
    </article>
    </template>

    <!– src/views/SongList.vue –>
    <template>
    <article>
    <h3>歌单</h3>
    <p>展示精选歌单。</p>
    </article>
    </template>

    朋友页同时兼容查询参数路由和动态参数路由:

    <!– src/views/Friend.vue –>
    <script setup>
    import { useRoute, useRouter } from 'vue-router'

    const route = useRoute()
    const router = useRouter()

    function openFriendDetail() {
    router.push({
    name: 'FriendDetail',
    params: {
    fid: 10086
    }
    })
    }
    </script>

    <template>
    <section>
    <h2>朋友</h2>
    <p>查询标签:{{ route.query.tab ?? '未提供' }}</p>
    <p>朋友 ID:{{ route.params.fid ?? '未提供' }}</p>
    <button @click="openFriendDetail">查看朋友 10086</button>
    </section>
    </template>

    受保护页面只展示业务内容,权限逻辑统一留在守卫中:

    <!– src/views/My.vue –>
    <template>
    <section>
    <h2>我的音乐</h2>
    <p>只有登录用户能够访问这里。</p>
    </section>
    </template>

    登录页读取守卫保存的原目标,登录成功后返回原页面:

    <!– src/views/Login.vue –>
    <script setup>
    import { useRoute, useRouter } from 'vue-router'

    const route = useRoute()
    const router = useRouter()

    async function login() {
    localStorage.setItem('isLogin', '1')

    const redirect = typeof route.query.redirect === 'string'
    ? route.query.redirect
    : '/find/recommend'

    await router.push(redirect)
    }
    </script>

    <template>
    <section>
    <h2>登录</h2>
    <button @click="login">模拟登录成功</button>
    </section>
    </template>

    最后补上 404 页面:

    <!– src/views/NotFound.vue –>
    <template>
    <section>
    <h2>404</h2>
    <p>页面不存在或已经被移动。</p>
    <RouterLink to="/">返回首页</RouterLink>
    </section>
    </template>

    8.4 从启动到导航完成的执行链

    完整项目运行后,一次访问 /my 的过程如下:

  • main.js 创建 Vue 应用,通过 .use(router) 安装路由,再挂载到页面。
  • 浏览器请求 /my,路由器在 routes 中命中名称为 My 的记录。
  • 导航确认前执行 beforeEach(),读取 to.meta.requiresAuth。
  • 未登录时,守卫返回登录路由对象,并把 /my 保存到 redirect 查询参数。
  • 路由器取消原导航,创建到 /login?redirect=/my 的新导航。
  • Login.vue 被渲染到 App.vue 的一级 <RouterView>。
  • 用户登录后,router.push(redirect) 再次发起到 /my 的导航。
  • 守卫确认登录状态,本次返回 undefined,导航放行,My.vue 最终显示。
  • 最后可以按下面的清单验证功能:

    测试动作预期地址或结果涉及知识点
    首次访问 / 自动进入 /find/recommend 根路径重定向
    点击排行榜 进入 /find/ranking,父页面结构保留 嵌套路由与二级出口
    点击朋友 地址包含 tab、page 声明式查询参数
    点击“查看朋友 10086” 进入 /friend/10086 编程式动态参数
    未登录访问 /my 进入登录页并记录原地址 全局前置守卫
    完成模拟登录 返回 /my useRouter() 与 router.push()
    访问任意未知路径 显示 404 页面 捕获所有路由
    直接刷新二级路径 生产环境仍返回应用 History 模式服务器回退

    总结

    Vue Router 的核心并不复杂:它维护 URL 与组件之间的映射,在地址变化时解析路由表、执行导航守卫,并把匹配组件放入对应层级的 <RouterView>。基础使用可以记成“安装、导入、创建、注册,加上配置规则和提供出口”的 4+2 流程;项目变大后,应把路由抽离到独立模块,并通过页面组件与复用组件的目录约定保持结构清晰。

    导航层面,<RouterLink> 负责模板中的声明式导航,useRouter() 与 router.push() 负责 JavaScript 主动导航,useRoute() 负责读取当前地址信息。查询参数适合筛选与多个可选条件,动态参数适合表达资源身份。重定向解决默认入口,捕获路由提供 404 兜底,嵌套路由用 children 和多层 <RouterView> 表达页面层级,全局前置守卫则把访问权限集中在导航确认之前。真正理解这些角色的边界后,路由配置就不再是零散语法,而是一条可以被定位、验证和维护的完整导航链路。

    赞(0)
    未经允许不得转载:171主机测评 » Vue Router 4 从入门到实战:路由、导航传参、嵌套与守卫完整指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址