Vue Router 进阶:动态路由、参数传递与嵌套路由完全指南
本文以 Vue Router 的核心进阶机制为主线,深入剖析声明式与编程式导航的底层差异、路由组件专属钩子的执行时序、激活样式的三种控制策略、query 与 params 参数持久化原理、动态路由段(
:param)的匹配机制,以及嵌套路由(children)的渲染流水线。所有示例均基于 Vue CLI + Vue Router 3.x,可在标准 Vue 2 工程中直接运行。
目录
- 零、导读与学习价值
- 一、路由导航:声明式与编程式的选择
- 二、路由组件的专属钩子函数
- 三、路由激活状态与样式控制
- [四、路由参数传递:query vs params](#四、路由参数传递:query vs params "#%E5%9B%9B%E8%B7%AF%E7%94%B1%E5%8F%82%E6%95%B0%E4%BC%A0%E9%80%92query-vs-params")
- 五、动态路由与路径参数
- 六、嵌套路由(二级/三级路由)
- 七、综合实战:仿影院选片路由架构
- 总结
零、导读与学习价值
0.1 示例覆盖清单
| 章节 | 示例内容 |
|---|---|
| 第一章 | 声明式三种写法、编程式 push/replace/go |
| 第二章 | 路由切换生命周期执行顺序完整演示 |
| 第三章 | .router-link-active、active-class、linkActiveClass 三种方案 |
| 第四章 | query 传参接收完整示例、params 传参及刷新丢失问题 |
| 第五章 | 动态路由段 :id、路径参数持久化、GitHub 用户详情页 |
| 第六章 | 新闻列表嵌套路由、默认子路由的两种方案 |
| 第七章 | 仿影院选片"有导航"与"无导航"两套路由架构对比 |
0.2 核心名词速查
| 名词 | 含义 |
|---|---|
router-link |
Vue Router 内置声明式导航组件,渲染为 <a> 标签 |
router-view |
路由出口,渲染当前匹配到的路由组件 |
$route |
当前激活路由的信息对象(只读),含 path/query/params/name/meta |
$router |
路由器实例(VueRouter),含 push/replace/go/back 等方法 |
query |
URL 查询字符串参数,?key=value 形式,刷新不丢失 |
params |
路由路径参数,有两种模式:纯内存模式(刷新丢失)与动态路由段模式(刷新保留) |
| 动态路由段 | 路由配置中以 : 开头的路径片段,如 /film/:filmId |
| 嵌套路由 | 路由配置中通过 children 数组定义的子路由,形成父子渲染层级 |
linkActiveClass |
VueRouter 实例级别的激活样式配置,作用于所有 router-link |
0.3 为什么要学本篇
前端路由是单页应用(SPA)的核心机制。掌握参数传递与嵌套路由,意味着能够独立设计如"电商商品列表 → 商品详情"、"资讯首页 → 分类 → 文章详情"这类多层跳转结构。面试中,"query 与 params 区别"、"路由钩子执行顺序"是高频考题,本篇逐一拆解。
一、路由导航:声明式与编程式的选择
名词解释
声明式导航 :使用 <router-link> 组件在模板中定义跳转行为,Vue Router 在渲染时将其转换为带有点击监听的 <a> 标签。路由跳转发生在用户点击时。
编程式导航 :在 JavaScript 方法中调用 this.$router.push()、this.$router.replace() 等方法实现路由跳转,常用于需要在跳转前执行逻辑(如表单校验、权限检查)的场景。
概念与底层原理
<router-link> 在编译时等价于以下逻辑:
js
// 内部简化实现
<a @click.prevent="$router.push(to)">{{ slot }}</a>
【代码注释】这段是 router-link 编译后的"心智模型":它本质就是一个 <a> 标签,加了 @click.prevent 拦截默认跳转、转而调用 router.push(to)。.prevent 至关重要------它阻止浏览器对 <a href> 的整页刷新,从而让 SPA 在不重新加载页面的前提下完成路由切换。真实源码还会处理鼠标中键/Ctrl 点击(保留新开标签页的原生行为)、计算激活类名等,但核心就是"拦截点击 + 调用 push"。市面应用 :理解这层等价关系后,就明白为什么给 router-link 绑 @click 有时不生效------需要用 @click.native 才能监听原生点击事件。
$router.push() 与 $router.replace() 的差异在于历史栈操作:
push:向 history 栈中追加一条记录,用户可以点击"后退"返回replace:替换当前栈顶记录,用户无法"后退"到本页
示例一:声明式导航的三种写法
vue
<!-- src/App.vue -->
<template>
<div id="app">
<!-- 写法一:字符串路径 -->
<nav>
<router-link to="/">首页</router-link> |
<router-link to="/newsList">新闻列表</router-link> |
<router-link to="/goodsList">商品列表</router-link> |
<router-link to="/my">个人中心</router-link>
</nav>
<!-- 写法二:对象形式,通过 path 跳转 -->
<nav>
<router-link :to="{ path: '/' }">首页</router-link> |
<router-link :to="{ path: '/newsList' }">新闻列表</router-link> |
<router-link :to="{ path: '/goodsList' }">商品列表</router-link> |
<router-link :to="{ path: '/my' }">个人中心</router-link>
</nav>
<!-- 写法三:对象形式,通过命名路由(name)跳转 -->
<nav>
<router-link :to="{ name: 'index' }">首页</router-link> |
<router-link :to="{ name: 'newsList' }">新闻列表</router-link> |
<router-link :to="{ name: 'goodsList' }">商品列表</router-link> |
<router-link :to="{ name: 'my' }">个人中心</router-link>
</nav>
<router-view></router-view>
</div>
</template>
【代码注释】写法三通过 name 跳转的好处是:当路径改变时,只需修改路由配置,无需更新所有导航链接。这是大型项目的推荐写法。
示例二:编程式导航
vue
<!-- src/views/Login.vue -->
<template>
<div>
<h3>登录</h3>
<input v-model="username" placeholder="用户名" />
<input v-model="password" type="password" placeholder="密码" />
<button @click="handleLogin">登录</button>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
username: "",
password: ""
};
},
methods: {
handleLogin() {
if (!this.username || !this.password) {
alert("请填写账号和密码");
return;
}
// 校验通过后跳转,使用 replace 避免用户通过后退回到登录页
this.$router.replace({ name: "index" });
// 也可以用 push,允许后退
// this.$router.push("/");
// 带参数跳转
// this.$router.push({ name: "goodsList", query: { category: "1" } });
},
goBack() {
// 历史回退一步,等同于浏览器后退按钮
this.$router.go(-1);
// 或 this.$router.back()
}
}
};
</script>
【代码注释】$router.go(n) 接受正整数(前进)或负整数(后退),go(-1) 等同于 back(),go(1) 等同于 forward()。

【代码注释】该图揭示一个常被忽视的事实:声明式(router-link)和编程式(router.push/replace)只是触发入口不同 ,蓝/绿/紫三个上游节点最终都汇流到橙色的「History API」节点。router-link 内部本质就是监听点击、调用 router.push;二者随后共用同一条流水线------更新地址栏(history.pushState 或 hashchange)→ 用 path-to-regexp 做路由匹配 → 在 router-view 出口渲染目标组件。理解这一点能消除"两套导航是否行为一致"的疑虑:它们在 history 栈、守卫触发、激活样式上的表现完全相同。市面应用 :表单提交成功后用 router.push 跳转、面包屑用 router-link 渲染,两者混用而行为统一,正是因为共享这条底层链路。
【实战要点】:在需要传递复杂对象或在业务逻辑执行后才跳转的场景,必须使用编程式导航。声明式导航适合纯展示性的顶部/侧边导航栏。
【本章小结】:声明式导航语法简洁适合模板,编程式导航灵活适合逻辑控制,两者底层均调用相同的 History API。
【面试考点】 :push 与 replace 的区别?前者向历史栈追加记录,后者替换当前记录,区别体现在用户能否通过浏览器后退键回到上一页。
二、路由组件的专属钩子函数
名词解释
路由切换生命周期:当用户从路由 A 切换到路由 B 时,Vue Router 会触发一系列特定的生命周期钩子,其执行顺序与组件的挂载/销毁流程高度交织。
概念与底层原理
Vue Router 的路由切换过程遵循严格的时序:Vue Router 首先挂载目标组件(B 组件),在 B 组件的 mounted 钩子执行之前,才销毁原来的组件(A 组件)。这一设计保证了两个组件之间的交接不会出现"空白期"。
完整的执行顺序如下:
css
A 路由 → B 路由 的切换时序:
1. B->beforeCreate (B 组件开始创建)
2. B->created
3. B->beforeMount (B 组件即将挂载)
4. A->beforeDestroy (A 组件开始销毁)
5. A->destroyed (A 组件销毁完成)
6. B->mounted (B 组件挂载完成)
这说明在 B 组件完全进入视图之前,A 组件已经销毁完毕,从而避免两个路由组件同时占据 DOM。
【代码注释】这份序号清单是路由切换钩子时序的"标准答案",背下来即可应对面试。要抓住的反直觉点是第 3 步与第 4 步的相对位置:B 都已经走到 beforeMount(第 3 步)了,A 才开始 beforeDestroy(第 4 步)。原因是 Vue Router 走的是 transition 式的"先进后出"------先把新组件准备到即将落地的状态,确认无误再拆旧组件,最后让新组件 mounted。市面应用 :当你发现"离开页面时旧组件的定时器还触发了一次",多半就是把清理逻辑误写在了比 beforeDestroy 更晚的位置,对照这张时序表就能定位问题。
示例:观察路由切换的钩子顺序
vue
<!-- src/views/Home.vue -->
<template>
<div>
<h3>首页界面</h3>
</div>
</template>
<script>
export default {
name: "Home",
beforeCreate() {
console.log("1-Home->beforeCreate");
},
created() {
console.log("2-Home->created");
},
beforeMount() {
console.log("3-Home->beforeMount");
},
mounted() {
console.log("4-Home->mounted");
// 此时可以安全访问 DOM 并发起数据请求
},
beforeDestroy() {
// 路由离开时的清理工作:清除定时器、取消未完成的请求
console.log("8-Home->beforeDestroy");
},
destroyed() {
console.log("9-Home->destroyed");
}
};
</script>
【代码注释】Home 组件把六个关键钩子都打了带序号的日志,序号刻意按"实际执行顺序"编排(1-4 是初次挂载,8-9 是被切走时的销毁)。mounted 里注释强调"此时才能安全访问 DOM 并发起请求",beforeDestroy 里注释强调"路由离开的清理时机"。把序号埋进日志是观察时序最直观的手段------切换路由后看控制台数字是否连续递增,就能验证理论时序。市面应用:排查"切走页面后仍报错"的问题时,临时在各钩子打日志是最快的定位方式。
vue
<!-- src/views/NewsList.vue -->
<script>
export default {
name: "NewsList",
beforeCreate() {
console.log("5-NewsList->beforeCreate");
},
created() {
console.log("6-NewsList->created");
},
beforeMount() {
console.log("7-NewsList->beforeMount");
},
mounted() {
// 注意:顺序是 10,说明 Home 的 destroyed(9) 先于 NewsList 的 mounted(10)
console.log("10-NewsList->mounted");
},
beforeDestroy() {
console.log("NewsList->beforeDestroy");
},
destroyed() {
console.log("NewsList->destroyed");
}
};
</script>
【代码注释】控制台输出顺序为 1→2→3→4(Home 完整挂载)。切换到 NewsList 后:5→6→7→8→9→10。关键点:NewsList 的 beforeMount(7) 先于 Home 的 beforeDestroy(8),但 NewsList 的 mounted(10) 在 Home 的 destroyed(9) 之后。

【代码注释】该图把"A→B 路由切换"拆成 6 个严格有序的步骤:蓝色 1-3 是新组件 B 的创建链(beforeCreate→created→beforeMount),红色 4-5 是旧组件 A 的销毁(beforeDestroy→destroyed),绿色 6 才是 B 的 mounted。关键结论用黄色框标注------新组件 beforeMount 早于旧组件 beforeDestroy,但新组件 mounted 一定晚于旧组件 destroyed 。这种"交织"不是巧合,而是 Vue Router 刻意为之:先把新组件创建到 beforeMount(虚拟 DOM 已生成但未落地),再销毁旧组件腾出位置,最后让新组件真正 mounted,从而保证视图切换没有"两个组件同时占位"或"中间空白"的窗口期。理解时序的实战价值是知道资源清理必须放在红色的 beforeDestroy ------此时旧组件仍持有 DOM 与数据引用;若拖到 destroyed 才清理,已经过晚。市面应用 :列表页跳详情页时在 beforeDestroy 里 clearInterval 停掉轮询、cancelToken 取消未完成的 axios 请求,避免回调写入已销毁实例引发报错。
【实战要点】 :在 beforeDestroy 中执行资源清理是最佳实践,包括:clearInterval / clearTimeout 清除定时器、取消 axios 请求、移除全局事件监听。如果在 destroyed 中清理,已经过晚。
【本章小结】 :路由切换时,新组件的 beforeMount 先于旧组件的 beforeDestroy 执行,但新组件的 mounted 在旧组件完全销毁(destroyed)之后才执行。
【面试考点】:路由从 A 切换到 B,两个组件的生命周期钩子执行顺序是什么?答:B 的 beforeCreate → B 的 created → B 的 beforeMount → A 的 beforeDestroy → A 的 destroyed → B 的 mounted。
三、路由激活状态与样式控制
名词解释
路由激活状态 :当浏览器当前 URL 与某个 <router-link> 的 to 属性匹配时,Vue Router 会自动为该 <a> 标签添加特定 CSS 类名,以标识"当前所在位置"。
精确匹配(exact) :URL 与路由配置地址完全一致,如 URL 为 /newsList,路由配置为 /newsList。
非精确匹配 :URL 是路由配置地址的前缀,如 URL 为 /newsList/sport,路由配置 /newsList 仍然算匹配。
概念与底层原理
Vue Router 会为激活的 <router-link> 自动添加两个内置类名:
| 类名 | 触发条件 |
|---|---|
router-link-active |
非精确匹配(包含精确匹配) |
router-link-exact-active |
精确匹配 |
当 URL 为 /newsList/sport 时:
- 指向
/newsList的链接:添加router-link-active(非精确匹配),不添加router-link-exact-active - 指向
/newsList/sport的链接:同时添加router-link-active和router-link-exact-active
注意:/(根路径)比较特殊,所有路径都以 / 开头,如果不加限制,指向 / 的链接在任何页面都会处于"激活"状态。
三种控制方案
方案一:使用内置类名配合 CSS
vue
<!-- src/App.vue -->
<template>
<div id="app">
<!-- exact 属性:要求精确匹配才触发 router-link-active -->
<nav>
<router-link exact to="/">首页</router-link> |
<router-link to="/newsList">新闻列表</router-link> |
<router-link to="/goodsList">商品列表</router-link> |
<router-link to="/my">个人中心</router-link>
</nav>
<router-view></router-view>
</div>
</template>
<style lang="less" scoped>
#app {
a {
padding: 5px;
color: skyblue;
}
// 任何匹配(含非精确)即高亮
a.router-link-active {
color: red;
font-weight: bold;
}
// 精确匹配才高亮(更严格)
// a.router-link-exact-active {
// color: red;
// }
}
</style>
【代码注释】/ 根路径的 <router-link> 必须加 exact 属性,否则在所有路由下它都会被认为是"激活"的,导致首页链接始终高亮。
方案二:自定义激活类名(推荐)
vue
<template>
<nav>
<!-- active-class:匹配(含非精确)时使用 active 样式 -->
<!-- exact active-class:精确匹配才使用 active 样式 -->
<router-link exact active-class="active" to="/">首页</router-link> |
<!-- exact-active-class:仅精确匹配时才使用 active 样式 -->
<router-link exact-active-class="active" to="/newsList">新闻列表</router-link> |
<router-link active-class="active" to="/goodsList">商品列表</router-link> |
<router-link active-class="active" to="/my">个人中心</router-link>
</nav>
</template>
<style lang="less" scoped>
a.active {
color: red;
font-weight: bold;
}
</style>
【代码注释】方案二把激活类名收敛成自定义的 .active,摆脱了又长又容易拼错的内置类名 router-link-active。两个属性的分工要记牢:active-class 控制"非精确匹配"时的高亮类,exact-active-class 控制"精确匹配"时的高亮类;根路径 / 必须配 exact 否则在所有页面都高亮。这种"逐链接覆盖"的写法适合个别链接需要特殊高亮规则的场景。市面应用 :侧边栏大部分菜单走全局配置,唯独"首页"链接因根路径特性需要单独加 exact,正是方案二的典型落地。
方案三:在路由配置中全局设置(最简洁)
js
// src/router/index.js
export default new VueRouter({
routes,
mode: "history",
// 全局设置:所有 router-link 的非精确激活类名都改为 active
linkActiveClass: "active",
// 全局设置:所有 router-link 的精确激活类名都改为 active
// linkExactActiveClass: "active"
});
【代码注释】方案三是工程化项目的最佳实践。在 VueRouter 实例中统一配置后,所有 <router-link> 无需单独设置 active-class,样式库中只需定义 .active 即可。
【实战要点】:三种方案的优先级:方案二(单个 router-link 属性)> 方案三(全局路由配置)> 方案一(默认内置类名)。实际项目中,通常选择方案三全局设置,再对特殊链接使用方案二覆盖。
【本章小结】 :理解精确匹配与非精确匹配的区别,是正确控制导航高亮的前提。根路径 / 必须使用精确匹配限制,否则会产生全局高亮的问题。
【面试考点】 :router-link-active 与 router-link-exact-active 的区别?前者在路径前缀匹配时也生效,后者要求 URL 与 to 完全一致才生效。
四、路由参数传递:query vs params
名词解释
query 参数 :通过 URL 查询字符串传递数据,格式为 ?key=value&key2=value2。数据编码在 URL 中,页面刷新后参数保留。
params 参数(纯内存模式) :通过 $router.push({ name, params }) 传递,数据存储在内存中。页面刷新后数据丢失,URL 中不显示参数。
params 参数(动态路由段模式) :在路由配置中使用 :paramName 定义路径段,参数编码在 URL 路径中,刷新后数据保留。
概念与底层原理
$route 对象是 Vue Router 注入到每个组件的只读对象,完整结构如下:
js
this.$route = {
path: "/one", // 当前路由路径
fullPath: "/one?id=1", // 完整路径,含查询字符串
name: "one", // 路由名称
query: { id: "1" }, // query 参数(始终是字符串)
params: { a: "100" }, // params 参数(动态路由段)
hash: "", // URL 中的 hash 部分
meta: {}, // 路由元信息
matched: [...] // 匹配到的路由记录数组
}
【代码注释】这是 $route 的"全字段地图",每个字段对应一类信息源:path/fullPath 来自地址栏、query 来自查询字符串、params 来自动态路由段、meta 来自路由配置、matched 来自逐级匹配链。最值得记的两点:① query 的值永远是字符串 ({ id: "1" } 而非 { id: 1 }),数字运算前必须 Number() 转换;② matched 是个数组,嵌套路由时自顶向下记录每一级路由记录,导航守卫做逐级权限校验靠的就是它。$route 是只读 的,要改路由必须走 $router 的方法。市面应用 :埋点系统常读 $route.fullPath 上报当前页、权限中间件常读 $route.meta.roles 做拦截。
示例一:query 传参(推荐用于列表筛选等场景)
vue
<!-- 传参方:src/App.vue -->
<template>
<nav>
<!-- 写法一:直接拼接字符串 -->
<router-link to="/one?id=1&type=2">one-1</router-link> |
<!-- 写法二:对象形式 path + query -->
<router-link :to="{ path: '/one', query: { id: 21, type: 22 } }">
one-2
</router-link> |
<!-- 写法三:命名路由 + query(推荐) -->
<router-link :to="{ name: 'one', query: { id: 31, type: 32, category: 'tech' } }">
one-3
</router-link>
</nav>
</template>
【代码注释】这是 query 传参的三种写法对比:写法一直接把 ?id=1&type=2 拼进字符串,最直观但路径硬编码、易出错;写法二用对象 path + query,由 Vue Router 负责 URL 编码(中文、特殊字符自动转义);写法三用 name + query,路径改了也不用动导航代码,是大型项目首选。三者最终都会在地址栏生成可见、可分享、刷新不丢的查询字符串。市面应用:商品列表的筛选条件(分类、价格区间、排序)几乎都用 query 传参,因为用户需要把"筛选后的链接"复制给同事或收藏。
vue
<!-- 接收方:src/views/One.vue -->
<template>
<div>
<h3>query 接收参数</h3>
<p>id: {{ $route.query.id }}</p>
<p>type: {{ $route.query.type }}</p>
<p>category: {{ $route.query.category }}</p>
</div>
</template>
<script>
export default {
name: "One",
mounted() {
// 初次进入时读取参数
console.log("mounted", this.$route.query);
// 输出:{ id: "21", type: "22", category: "tech" }
// 注意:query 中的值始终是字符串类型,即使传入数字
},
// 当 query 参数改变但路由路径不变时,组件不会重新挂载
// 需要通过 updated 钩子或 watch 来响应参数变化
updated() {
console.log("updated", this.$route.query);
},
watch: {
"$route.query": {
handler(newQuery) {
console.log("query 变化", newQuery);
// 在此重新发起数据请求
},
immediate: true // 进入组件时立即执行一次
}
}
};
</script>
【代码注释】当 query 参数从 ?id=1 变为 ?id=2 时,URL 路径部分(/one)未变,Vue Router 不会销毁并重建组件,只会触发 updated 钩子。因此监听参数变化需要 watch 或 updated,不能依赖 mounted。
示例二:params 传参(纯内存模式,刷新丢失)
vue
<!-- 传参方 -->
<router-link :to="{
name: 'two',
params: {
a: 1,
b: 2,
userInfo: { name: '张三', age: 30 }
}
}">
跳转到 Two(可传对象)
</router-link>
【代码注释】注意这里必须用 name 而非 path 来跳转------这是 params 内存模式的硬性约束:Vue Router 只有通过路由名才能定位到目标路由并注入内存中的 params,写成 path 会导致 params 被静默丢弃。它能携带 userInfo: { name, age } 这样的嵌套对象,正是 query 做不到的(query 会把对象 [object Object] 化)。代价是这些数据只活在内存里,刷新即归零。市面应用:多步骤表单(如下单时把上一步勾选的商品对象数组带到下一步)常用内存 params,因为这些临时数据不需要、也不应该出现在 URL 上。
vue
<!-- 接收方:src/views/Two.vue -->
<template>
<div>
<h3>params 接收参数(刷新会丢失)</h3>
<p>a: {{ $route.params.a }}</p>
<p>b: {{ $route.params.b }}</p>
<p>userName: {{ $route.params.userInfo?.name }}</p>
</div>
</template>
<script>
export default {
name: "Two",
mounted() {
// 直接在浏览器刷新此页,params 将为空对象 {}
console.log(this.$route.params);
}
};
</script>
【代码注释】params 纯内存模式的优势是可以传递任意 JavaScript 对象(包括嵌套对象、数组),而 query 传对象时会被序列化为字符串。但代价是刷新后数据丢失,仅适合"点击后立即使用、不需要分享 URL"的场景。

【代码注释】该决策树把"用 query 还是 params"落到两个可操作的判断:先问黄色「是否需要刷新保留」,否则走红色 params 内存模式(可传对象、URL 不留痕、刷新即丢);若需保留,再问黄色「数据类型」------简单键值(筛选条件、分页页码)走绿色 query,路径标识符(详情 id)走紫色 params + 动态路由段。三条出口的本质差异在于参数的存储位置 :query 存在 URL 查询字符串、动态段存在 URL 路径、内存模式存在 JS 运行时内存。刷新会重新解析 URL 但清空内存,这正是"内存模式刷新丢失"的根因。市面应用 :商品列表的 ?category=1&page=2 用 query(可分享可收藏)、商品详情 /goods/1001 用动态段、跨页传一个临时勾选的对象数组用内存模式 params。
【实战要点】:
- query 的类型陷阱 :
$route.query.id永远是字符串,即使传入{ id: 1 },接收到的也是"1"。需要用Number($route.query.id)转换类型。 - params 刷新丢失问题:如果业务要求参数在刷新后保留,必须使用动态路由段(见第五章)或将参数存储在 localStorage/Vuex 中。
【本章小结】:query 把参数编码在 URL 查询字符串中,适合筛选、分页等可分享的状态;params 内存模式把参数存在内存中,适合页面间临时传递复杂数据。
【面试考点】:query 与 params 的核心区别?query 体现在 URL 中刷新保留,params 内存模式不体现在 URL 中刷新丢失;query 值只能是字符串,params 可以传对象。
五、动态路由与路径参数
名词解释
动态路由段 :路由配置路径中以 : 开头的片段,如 path: "/film/:filmId"。访问 /film/123 时,$route.params.filmId 的值为 "123"。
路由参数持久化:当参数编码在 URL 路径中(动态路由段),页面刷新时浏览器重新解析 URL,参数得以保留。
概念与底层原理
动态路由段基于路径匹配算法实现。Vue Router(底层使用 path-to-regexp 库)将 /film/:filmId 编译为正则表达式,当 URL 与该正则匹配时,提取捕获组并填入 $route.params。
支持的路径参数语法:
js
// 单个参数
{ path: "/user/:id" } // 匹配 /user/123,$route.params = { id: "123" }
// 多个参数
{ path: "/user/:id/post/:postId" } // 匹配 /user/1/post/2,$route.params = { id: "1", postId: "2" }
// 带后缀(SEO 友好)
{ path: "/film/:filmId.html" } // 匹配 /film/123.html,$route.params = { filmId: "123" }
// 带前缀
{ path: "/three/:a/:b.html" } // 匹配 /three/100/200.html
【代码注释】这组展示动态路由段的四种语法形态,底层都由 path-to-regexp 把 :param 编译成带捕获组的正则:/user/:id 编译为 ^\/user\/([^\/]+)$,URL 命中后捕获组的值填进 $route.params。值得注意的是"带后缀 .html"这种写法------:filmId.html 仍能正确把 123 提取为 filmId,因为 .html 是字面量、:filmId 是捕获组,正则会精确区分。市面应用 :早期 SPA 为了 SEO 和"看起来像静态页",常给详情页 URL 加 .html 后缀(如 /film/123.html),这正是动态段带后缀语法的现实来源。
示例一:动态路由参数传递(基础版)
js
// src/router/index.js
const routes = [
{
// 路径参数 :a 和 :b 会出现在 URL 中,刷新后不丢失
// 访问 /three/100/200.html 时,params = { a: "100", b: "200" }
path: "/three/:a/:b.html",
name: "three",
component: Three
}
];
【代码注释】这份路由配置定义了一个含两个动态段 :a、:b 且带 .html 后缀的路径,并用 name: "three" 命名。name 是后续"命名路由 + params"跳转的前提------有了它,跳转代码就不必关心 .html 后缀和路径拼接细节。市面应用 :把路径模板的复杂性(多级动态段、后缀、前缀)全部封装在路由表里、对外只暴露 name,是大型项目维护路由的标准做法,路径改版时只动这一处。
vue
<!-- 传参方 -->
<template>
<nav>
<!-- 写法一:手动拼接路径 -->
<router-link to="/three/1/2.html">three-1</router-link> |
<!-- 写法二:对象 path 形式(需手动拼接参数) -->
<router-link :to="{ path: '/three/22/23.html' }">three-2</router-link> |
<!-- 写法三:命名路由 + params(最推荐,无需拼接字符串) -->
<router-link :to="{ name: 'three', params: { a: 100, b: 200 } }">
three-3
</router-link>
</nav>
</template>
【代码注释】三种写法的核心区别在"谁负责拼路径":写法一/写法二把 .html 后缀和分隔符手写进字符串,一旦路由模板改版(比如去掉 .html),所有导航都得逐个改;写法三用 name + params,由 Vue Router 依据路由表里的 path 模板自动填充,组件只关心参数名和值。配合动态路由段时,写法三的 params 会被编码进 URL 路径,因此刷新不丢失 ------这与第四章纯内存 params 形成关键对比。市面应用:所有需要"可收藏、可分享、刷新仍在"的详情页,都用动态段 + 命名路由的组合。
vue
<!-- 接收方:src/views/Three.vue -->
<template>
<div>
<h3>params + 动态路由接收参数(刷新不丢失)</h3>
<p>a: {{ $route.params.a }}</p>
<p>b: {{ $route.params.b }}</p>
</div>
</template>
<script>
export default {
name: "Three",
mounted() {
// 刷新页面后,URL 仍为 /three/100/200.html
// Vue Router 重新解析 URL,params 仍然有值
console.log(this.$route.params); // { a: "100", b: "200" }
}
};
</script>
【代码注释】写法三(命名路由 + params)是最安全的方式。路径模板的细节(如后缀 .html)封装在路由配置中,组件代码只需关心参数名和参数值,不会因为路径格式调整而需要批量修改。
示例二:动态路由实战 ------ GitHub 用户详情页
js
// src/router/index.js(详情路由配置)
{
path: "/details/:login.html",
name: "details",
component: Details
}
【代码注释】这里把 GitHub 用户名 login(如 torvalds)作为动态段 :login,详情页 URL 形如 /details/torvalds.html。用"业务唯一标识"(用户名、商品 id、订单号)做动态段是详情页路由的通用范式------它天然唯一、刷新可解析、便于后端 SEO 收录。市面应用 :GitHub 自身的 github.com/用户名、电商的 /item/商品id 都是这种"标识符进路径"的设计。
vue
<!-- src/views/Home.vue(列表页,点击跳转详情) -->
<template>
<div>
<div v-for="item in items" :key="item.id">
<!-- 使用 path 拼接(不推荐)-->
<!-- <router-link :to="'/details/' + item.login + '.html'">{{ item.login }}</router-link> -->
<!-- 使用命名路由(推荐) -->
<router-link :to="{ name: 'details', params: { login: item.login } }">
{{ item.login }}
</router-link>
</div>
</div>
</template>
<script>
export default {
name: "Home",
data() {
return { items: [] };
},
mounted() {
// 通过 GitHub Search API 获取热门用户列表
this.$github.get("/search/users?q=r&sort=stars").then(({ items }) => {
this.items = items;
});
}
};
</script>
【代码注释】列表页用 v-for 渲染用户,每项的跳转刻意用 { name: 'details', params: { login: item.login } } 而非字符串拼接(被注释掉的写法一作为对比)------好处是无需在模板里关心 .html 后缀,路由表改了模板也不用动。this.$github 是封装好的 axios 实例(在 main.js 里挂到 Vue 原型上),统一了 baseURL 和拦截器。市面应用:列表页"点条目进详情"是最高频的交互,统一用命名路由跳转能让上百个列表入口在路径改版时零成本迁移。
vue
<!-- src/views/Details.vue(详情页) -->
<template>
<div>
<img src="`${info.avatar_url}&s=100`" />
<h3>{{ info.login }}</h3>
<p>Followers: {{ info.followers }}</p>
</div>
</template>
<script>
export default {
name: "Details",
data() {
return { info: {} };
},
mounted() {
// 从路由参数获取用户名,发起详情请求
const login = this.$route.params.login;
this.$github.get(`/users/${login}`).then(info => {
this.info = info;
});
}
};
</script>
【代码注释】详情页中通过 this.$route.params.login 读取 URL 中的用户名参数,再请求对应用户的详细信息。由于参数在路径中,用户刷新页面、收藏链接后均可正常访问。
深入:动态路由复用组件的"陷阱"与 watch / beforeRouteUpdate
这是动态路由最容易踩、面试也最爱问的坑。Vue Router 官方文档 明确指出:"当使用路由参数时......原来的组件实例会被复用......组件的生命周期钩子不会再被调用。" 也就是说,从 /details/torvalds 跳到 /details/yyx990803------两个 URL 命中的是同一个 Details 组件,Vue Router 出于性能考虑复用同一个实例 ,不会销毁重建。后果是:你写在 mounted 里的请求只会执行一次 ,第二次切换 id 时 mounted 不触发,页面数据"卡"在上一个用户。
底层原因在于 Vue 的虚拟 DOM diff:两次渲染的 router-view 里都是同类型组件(Details),diff 判定为"可复用节点",于是只更新 props/$route 而不走卸载-挂载。$route 本身是响应式的,变了会触发重渲染,但不会重跑生命周期钩子。
官方给出两种应对方案:
vue
<!-- src/views/Details.vue(修正版:响应参数变化) -->
<script>
export default {
name: "Details",
data() {
return { info: {} };
},
methods: {
fetchUser(login) {
// 抽出请求逻辑,供 mounted 与 watch 复用
this.$github.get(`/users/${login}`).then(info => {
this.info = info;
});
}
},
mounted() {
// 首次进入:mounted 正常触发
this.fetchUser(this.$route.params.login);
},
// 方案一:watch 监听 $route,参数变化时重新请求
watch: {
"$route.params.login"(newLogin) {
this.fetchUser(newLogin);
}
},
// 方案二:路由组件专属守卫 beforeRouteUpdate(Vue Router 2.2+)
// 复用同一组件、仅参数变化时触发,可在此取消旧请求再发新请求
beforeRouteUpdate(to, from, next) {
this.fetchUser(to.params.login);
next();
}
};
</script>
【代码注释】这段把"复用陷阱"的两套标准解法都列出:① watch: { "$route.params.login" } 监听具体参数,变化即重新请求------写法直观,适合大多数场景;② beforeRouteUpdate(to, from, next) 是路由组件专属守卫 ,仅在"复用当前组件、只有参数变化"时触发,优势是能拿到 from/to 做对比、能在 next() 前取消上一次未完成的请求(避免竞态导致的数据错乱)。两者本质都是绕过"生命周期钩子不重跑"的限制,主动监听参数变化重新拉数据。实战中通常把请求逻辑抽成 methods(这里的 fetchUser),让 mounted 和 watch/守卫共用,避免重复代码。市面应用 :商品详情页"看了又看"推荐位点击切换商品、用户主页在不同用户间跳转,都依赖 watch $route 或 beforeRouteUpdate 来刷新数据,否则会出现"地址变了内容没变"的经典 Bug。
【实战要点】 :使用命名路由(name)+ params 的组合跳转时,Vue Router 内部会根据路由配置的 path 模板自动填充参数,生成最终 URL。这比手动字符串拼接更安全,也更易于维护。
【本章小结】:动态路由段将参数编码在 URL 路径中,结合命名路由使传参代码清晰简洁。路径参数刷新保留,适合详情页、用户主页等需要可分享、可收藏 URL 的场景。
【面试考点】 :params 传参与动态路由 :id 的关系?仅通过 { name, params } 传递的 params 存储在内存中,刷新丢失;配合路由配置中的 :id 动态段,params 才会编码进 URL,刷新后仍可通过重新解析 URL 获取。
【面试考点】 :动态路由从 /user/1 跳到 /user/2,为什么页面数据不更新?怎么解决?答:因为命中的是同一个组件,Vue Router 会复用组件实例、不重走生命周期钩子,mounted 里的请求只执行一次。解决:用 watch: { "$route.params.id" } 监听参数变化重新请求,或用路由组件专属守卫 beforeRouteUpdate(to, from, next)(后者还能拿到 from/to 做对比、取消旧请求)。
六、嵌套路由(二级/三级路由)
名词解释
嵌套路由 :在路由配置中通过 children 数组定义子路由,形成父子层级关系。父路由组件内部需要放置 <router-view> 作为子路由的渲染出口。
默认子路由 :当进入父路由时,希望自动渲染某一个子路由,有两种实现方案:将子路由路径设为与父路由相同,或使用 redirect 重定向。
概念与底层原理
嵌套路由的匹配过程是递归的。当 URL 为 /newsList/sport 时:
- 路由器先匹配一级路由,找到
path: "/newsList"对应的NewsList组件 - 路由器继续用剩余路径
/sport匹配NewsList路由的children,找到path: "sport"对应的Sport组件 NewsList组件内部的<router-view>渲染Sport组件
子路由 path 的写法规则 :子路由的 path 可以省略父路由的路径前缀。path: "sport" 等同于 path: "/newsList/sport",Vue Router 会自动拼接。
示例一:新闻列表嵌套路由(含默认子路由)
js
// src/router/index.js
const routes = [
{
path: "/",
component: Home
},
{
path: "/newsList",
alias: "/news",
component: NewsList,
// children 数组定义二级路由
children: [
// 方案:进入 /newsList 时,重定向到 /newsList/tiyu
{
path: "/", // 匹配 /newsList
redirect: "tiyu" // 重定向到 tiyu 子路由
},
{
path: "tiyu", // 完整路径:/newsList/tiyu
name: "tiyu",
component: TiYu
},
{
path: "yule", // 完整路径:/newsList/yule
name: "yule",
component: YuLe
},
{
path: "caijing", // 完整路径:/newsList/caijing
name: "caijing",
component: CaiJing
}
]
},
{
path: "/goodsList",
component: GoodsList
},
{
path: "/my",
component: My
},
{
path: "*",
component: NotFound
}
];
export default new VueRouter({
routes,
mode: "history",
linkActiveClass: "active"
});
【代码注释】这份路由表演示了嵌套路由的几个关键点:① /newsList 用 children 数组挂了四个子路由,子路由的 path(tiyu/yule/caijing)不以 / 开头 ,会自动拼成 /newsList/tiyu;② children 里第一项 { path: "/", redirect: "tiyu" } 实现"进入 /newsList 默认显示体育新闻";③ alias: "/news" 给 /newsList 起了别名,访问 /news 等效但地址栏不变(区别于 redirect);④ path: "*" 通配兜底所有未匹配路径到 NotFound,必须放在最后。市面应用 :资讯/电商类站点的"频道页 + 子分类"几乎都是这种 children 结构,* 通配则是 404 页的标准实现。
vue
<!-- src/views/NewsList.vue(父路由组件,内含子路由出口) -->
<template>
<div>
<!-- 子路由导航 -->
<nav>
<!-- 写法一:完整路径 -->
<!-- <router-link to="/newsList/tiyu">体育新闻</router-link> | -->
<!-- 写法二:命名路由(最推荐) -->
<router-link :to="{ name: 'tiyu' }">体育新闻</router-link> |
<router-link :to="{ name: 'yule' }">娱乐新闻</router-link> |
<router-link :to="{ name: 'caijing' }">财经新闻</router-link>
</nav>
<!-- 子路由渲染出口,必不可少 -->
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "NewsList"
// 父路由组件本身不需要额外逻辑
// 数据请求通常在子路由组件中进行
};
</script>
【代码注释】父路由组件 NewsList 扮演"布局壳"角色:上半部分是子路由导航(用命名路由 { name: 'tiyu' } 跳转,避免硬编码路径),下半部分是必不可少的 <router-view> ------它就是二级路由的渲染出口,子路由内容全靠它落地。父组件 script 几乎为空,正体现了"壳组件只管布局、业务逻辑下沉到子路由"的分层原则。市面应用 :后台系统的"二级菜单 Tab + 内容区"就是这种结构,Tab 切换只换 router-view 里的内容,外层壳保持不动、状态不丢。
vue
<!-- src/views/TiYu.vue(子路由组件) -->
<template>
<div>
<h3>体育新闻</h3>
<ul>
<li v-for="news in newsList" :key="news.id">{{ news.title }}</li>
</ul>
</div>
</template>
<script>
export default {
name: "TiYu",
data() {
return {
newsList: [
{ id: 1, title: "CBA 总决赛今晚开打" },
{ id: 2, title: "国足备战世界杯预选赛" }
]
};
}
};
</script>
【代码注释】嵌套路由的核心是两个 <router-view>:一个在 App.vue 中渲染一级路由(NewsList),另一个在 NewsList.vue 中渲染二级路由(TiYu/YuLe/CaiJing)。两层 <router-view> 对应两层路由匹配。

【代码注释】该图把"两层 router-view 对应两层路由匹配"画成树。蓝色 App.vue 的一级 router-view 负责渲染一级路由(紫色 NewsList、黄色 GoodsList/My);紫色 NewsList 内部又放了二级 router-view,负责渲染绿色子路由(TiYu/YuLe/CaiJing)。访问 /newsList/tiyu 时,路由器递归匹配:先用 /newsList 命中 NewsList,再用剩余的 /tiyu 在它的 children 里命中 TiYu,于是 $route.matched 数组里会有两条记录(父+子),每条记录对应一个 router-view 出口。有几层嵌套就需要几个 router-view ------这是嵌套路由最易踩的坑:子路由不显示,十有八九是父组件忘了放 router-view。市面应用:后台管理系统的"侧边栏布局壳 + 内容区"、资讯站的"频道页 + 文章列表"都是这种两级出口结构。
【代码注释·原理补充】官方文档明确指出 嵌套路由 的匹配是按 URL 片段逐级进行的,$route.matched 自顶向下记录每一级被命中的路由记录,多级 router-view 与这个数组一一对应。这也是导航守卫里能用 to.matched 遍历整条路由链做逐级权限校验的底层依据。
默认子路由的两种方案对比
方案一:将子路由路径设置为与父路由相同
js
children: [
{
path: "/newsList", // 与父路由 path 完全相同
name: "tiyu",
component: TiYu
},
// ...
]
【代码注释】方案一让第一个子路由的 path 等于父路由 path(都是 /newsList),于是访问 /newsList 时父子同时命中、默认渲染 TiYu。缺点是地址栏停留在 /newsList(不会变成 /newsList/tiyu),且因为路径前缀关系,导航高亮会"误连带",所以模板里必须加 exact。市面应用:这种写法在老项目里仍常见,但新项目更倾向方案二(redirect),因为地址栏语义更明确。
模板中导航需要 exact:
html
<router-link exact to="/newsList">体育新闻</router-link>
【代码注释】配合方案一,exact 属性要求 URL 与 to 完全相等 才激活,避免"指向 /newsList 的链接在所有 /newsList/xxx 子页面都高亮"的问题。这正是第三章"根路径 / 必须加 exact"同一原理的延伸------只要存在"父路径是子路径前缀"的情况,就要用 exact 收紧激活判定。市面应用 :任何"父级 Tab + 子级 Tab"的导航,父级链接都需要 exact 才能正确反映当前位置。
方案二:使用 redirect 重定向(推荐)
js
children: [
{
path: "/", // 匹配 /newsList(与父路由相同)
redirect: "tiyu" // 重定向到子路由 tiyu
},
{
path: "tiyu",
name: "tiyu",
component: TiYu
}
]
【代码注释】方案二是推荐写法,语义更清晰,且不需要在 <router-link> 上添加 exact。进入 /newsList 时会自动重定向到 /newsList/tiyu,浏览器地址栏也会相应更新。
【实战要点】:
- 子路由的
path不要以/开头(除非使用绝对路径),否则会被当作根路径处理,无法正确匹配。 - 每一级路由组件都需要包含
<router-view>作为子路由的渲染出口,缺少它子路由内容将无法显示。 - 使用
redirect处理默认子路由比修改路径更直观,也更容易理解路由配置的意图。
【本章小结】 :嵌套路由通过 children 数组定义,父组件内放置 <router-view> 接收子路由渲染。子路由 path 通常省略父路由前缀,使用 redirect 可优雅解决默认显示问题。
【面试考点】 :嵌套路由如何实现默认显示第一个子路由?两种方案:将第一个子路由的 path 设置为与父路由完全相同(需在导航中使用 exact);或在 children 中添加 path: "/" 的重定向配置。
七、综合实战:仿影院选片路由架构
本章通过"仿影院选片"的完整案例,对比展示两种路由架构设计思路:有二级路由与无二级路由方案的取舍。
方案对比:有无二级路由

【代码注释】该图左红右绿对比两种路由架构。红色方案 A 把所有页面平铺为一级路由,导航条是否显示靠每个路由的 meta.isHide 字段在全局守卫里逐页判断------问题在于"布局关注点"散落进了业务路由配置,新增页面容易漏配。绿色方案 B 改用嵌套:把"有导航条"的页面(正在热映、即将上映)作为紫色 Index 壳组件的 children,把"无导航条"的页面(详情、登录)保留为蓝色独立一级路由。导航条只写在 Index 模板里,子路由天然继承、一级路由天然没有------用路由层级表达布局层级 ,不再需要 meta.isHide 开关。这正是"关注点分离"在路由设计上的落地。市面应用 :几乎所有后台系统的"登录页/404 无侧边栏,业务页有侧边栏"都用这种壳组件嵌套,而非满屏 meta 标志位。
方案 B(嵌套路由)的设计思路:
- 有导航栏的页面(正在热映、即将上映)作为
Index的子路由 - 无导航栏的页面(详情、登录)作为独立的一级路由
- 导航栏的显隐不再依靠
meta.isHide控制,而是天然地通过路由层级隔离
方案 B 完整代码
js
// src/router/index.js(嵌套路由版本)
import Vue from "vue";
import VueRouter from "vue-router";
import Details from "@/views/Details";
import Index from "@/views/Index";
import NowPlaying from "@/views/NowPlaying";
import ComingSoon from "@/views/ComingSoon";
import Login from "@/views/Login";
Vue.use(VueRouter);
const routes = [
// 一级路由:无导航的独立页面
{
path: "/login",
component: Login
},
{
path: "/film/:filmId",
component: Details
},
// 一级路由:有导航的 Index 壳组件,将正在热映/即将上映作为子路由
{
path: "/",
component: Index,
children: [
{
path: "/",
redirect: "/nowPlaying" // 进入根路径时自动跳转到正在热映
},
{
path: "/nowPlaying",
component: NowPlaying
},
{
path: "/comingSoon",
component: ComingSoon
}
]
}
];
export default new VueRouter({
routes,
mode: "history"
});
【代码注释】这是方案 B 的路由表,结构一眼可读出布局意图:/login 和 /film/:filmId 是无导航的一级路由 ,/(Index 壳)通过 children 挂了有导航的二级路由 (正在热映、即将上映),并用 redirect: "/nowPlaying" 设定默认子路由。导航条写在 Index 模板里,子路由继承、一级路由不沾边------布局靠层级而非 meta 开关。生产优化(路由懒加载) :实际项目会把 component: NowPlaying 改写成 component: () => import(/* webpackChunkName: "film" */ "@/views/NowPlaying")。据 Vue Router 路由懒加载 文档,动态 import() 是 Webpack 的代码分割点,会把该组件单独打成一个 chunk,进入对应路由时才按需加载;用相同的 webpackChunkName 还能把多个组件合并进同一个 chunk。市面应用:首屏只加载登录/首页相关代码、其余页面懒加载,是大型 SPA 控制首屏体积、提升加载速度的标准手段。
vue
<!-- src/App.vue(最简单,只有一个顶级出口) -->
<template>
<div class="container">
<router-view></router-view>
<!-- 一级路由出口:可能渲染 Index、Details 或 Login -->
</div>
</template>
<style lang="less">
.container {
text-align: center;
a { color: skyblue; margin: 5px; }
a.active { color: red; }
}
</style>
【代码注释】方案 B 的 App.vue 极简------只有一个顶级 router-view,它渲染的是一级路由 (可能是 Index 壳、Details 或 Login)。注意这里没有任何导航条,因为导航条已经下沉到 Index 壳组件里了。这正是嵌套路由"布局分层"的体现:最外层只管"一级路由切谁",导航这种局部布局交给对应的壳组件。市面应用 :几乎所有后台系统的 App.vue 都这么干净------一个顶级出口 + 全局样式,具体布局由各级壳组件负责。
vue
<!-- src/views/Index.vue(壳组件:包含导航条 + 子路由出口) -->
<template>
<div>
<!-- 导航条只在 Index 壳组件内,详情页不会出现 -->
<nav>
<router-link active-class="active" to="/nowPlaying">正在热映</router-link> |
<router-link active-class="active" to="/comingSoon">即将上映</router-link>
</nav>
<!-- 二级路由出口 -->
<router-view></router-view>
</div>
</template>
【代码注释】Index 壳组件把"导航条 + 二级 router-view"封装在一起:导航条只写在这里,因此只有它的子路由(正在热映、即将上映)才有导航条,而独立的一级路由(详情、登录)天然没有。这就是方案 B 相比"满屏 meta.isHide"的架构优势------显隐由路由层级决定,而非散落的标志位 。市面应用:电商 App 的"商品列表带底部 TabBar、商品详情/下单页不带 TabBar",正是用这种壳组件嵌套实现的。
vue
<!-- src/views/NowPlaying.vue(正在热映列表) -->
<template>
<div>
<div v-for="item in films" :key="item.filmId">
<!-- 点击海报跳转到详情页,使用路径拼接 -->
<router-link :to="'/film/' + item.filmId">
<img src="`${item.poster}?w=200`" />
<p>{{ item.name }}</p>
<p>主演:{{ item.actors | actorNames }}</p>
</router-link>
<hr />
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "NowPlaying",
data() {
return { films: [] };
},
filters: {
actorNames(actors) {
return actors.map(a => a.name).join(" / ");
}
},
mounted() {
// 请求正在热映的影片列表
axios.get("https://api.example.com/film/gateway", {
params: {
cityId: 310100,
pageNum: 1,
pageSize: 10,
type: 1,
k: Date.now()
},
headers: {
"X-Client-Info": '{"a":"3000","ch":"1002","v":"5.2.1","e":"1690528","bc":"310100"}',
"X-Host": "mall.film-ticket.film.list"
}
}).then(({ data }) => {
this.films = data.data.films;
});
}
};
</script>
【代码注释】列表组件做三件事:用 v-for 渲染影片、点海报用 router-link :to="'/film/' + item.filmId" 跳一级详情路由、在 mounted 里请求影片网关接口。:src 用模板字符串 ${item.poster}?w=200 给 CDN 图加宽度参数(按需缩放、省流量);item.actors | actorNames 用过滤器把演员数组拼成 张三 / 李四。请求头里的 X-Client-Info/X-Host 是该网关的鉴权约定,k: Date.now() 是防缓存时间戳。市面应用:电商/影院类列表页"图片 CDN 缩放 + 时间戳防缓存 + 网关自定义头"是真实接口对接的高频组合。
vue
<!-- src/views/Details.vue(影片详情页,无导航) -->
<template>
<div>
<button @click="$router.back()">返回</button>
<img src="`${film.poster}?w=400`" />
<h3>{{ film.name }}</h3>
<p>{{ film.synopsis }}</p>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "Details",
data() {
return { film: {} };
},
mounted() {
// 从路由参数获取 filmId,请求影片详情
const filmId = this.$route.params.filmId;
axios.get("https://api.example.com/film/gateway", {
params: {
filmId,
k: Date.now()
},
headers: {
"X-Client-Info": '{"a":"3000","ch":"1002","v":"5.2.1","e":"1690528","bc":"310100"}',
"X-Host": "mall.film-ticket.film.info"
}
}).then(({ data }) => {
this.film = data.data.film;
});
}
};
</script>
【代码注释】嵌套路由方案的核心优势体现在详情页(Details)与列表页(NowPlaying/ComingSoon)完全隔离。列表页作为 Index 的子路由,天然继承了导航条;详情页作为独立的一级路由,天然没有导航条。这种设计不需要 meta.isHide 这样的"开关"来手动控制显隐,架构更清晰。
【实战要点】:
- 设计路由层级时,先问"这个页面有没有公共导航/布局",有则放入对应壳组件的
children,没有则作为独立一级路由。 - 壳组件(如
Index.vue)只负责布局框架,不处理业务逻辑,符合单一职责原则。 - 从详情页返回列表页使用
$router.back()而非跳转到固定路径,可保留列表页的滚动位置。
【本章小结】 :嵌套路由不仅是技术实现,更是一种页面布局分层的架构思维。将"有导航"与"无导航"的页面用路由层级自然区分,比用 meta 字段手动控制更符合"关注点分离"原则。
【面试考点】:如何用路由设计解决"部分页面不显示导航栏"的问题?使用嵌套路由:将需要导航栏的页面作为壳组件的子路由,不需要导航栏的页面(如详情页、登录页)独立为一级路由,从而天然隔离布局。
总结
知识点回顾(思维导图)

【代码注释】该图以放射状把本篇五大模块串成一张知识网:蓝色中枢「Vue Router 进阶」向外辐射到紫色「路由导航」、黄色「路由钩子」、橙色「激活样式」、绿色「参数传递」、蓝色「嵌套路由」五个分支,每个分支再展开关键 API 与判据。复习时建议"由中心向外回放"------从中枢出发,逐个分支自问"这块解决什么问题、有哪几种方案、各自的取舍是什么",能在 5 分钟内自检整篇掌握度。重点关注绿色「参数传递」分支的三态(query / params 内存 / params 动态路由),这是面试出现频率最高、最容易混淆的知识点。
高频面试题速查
| 问题 | 要点 |
|---|---|
push vs replace 区别 |
push 追加历史记录,replace 替换当前记录,影响后退行为 |
| 路由切换钩子顺序 | B的beforeMount → A的beforeDestroy → A的destroyed → B的mounted |
router-link-active vs router-link-exact-active |
前者前缀匹配即生效,后者要求完全匹配 |
| query vs params 区别 | query 在URL中保留,params 内存模式刷新丢失,配合动态路由段则保留 |
| params 刷新丢失怎么解决 | 使用动态路由段(:id)将参数编码进路径 |
| 动态路由切换 id 数据不更新 | 组件被复用、钩子不重跑,用 watch $route 或 beforeRouteUpdate 重新请求 |
| 路由懒加载怎么做 | component: () => import('xx.vue'),Webpack 按代码分割点打成独立 chunk,按需加载;webpackChunkName 可合并 chunk |
| 嵌套路由如何设置默认子路由 | 在 children 中添加 { path: "/", redirect: "子路由名" } |
| 如何实现部分页面无导航栏 | 无导航页面作为独立一级路由,有导航页面作为壳组件的子路由 |
$route vs $router |
$route 只读当前路由信息,$router 是路由器实例可调用方法 |
学习建议
-
动手验证钩子顺序:新建一个 Vue CLI 项目,在两个路由组件中分别添加所有生命周期钩子并打印日志,切换路由观察控制台输出,这是理解路由切换时序最有效的方式。
-
刻意对比 query 与 params:在同一个项目中分别实现 query 传参和 params 传参,然后在接收页手动刷新浏览器,观察哪种方式的数据丢失,直观感受两者的差异。
-
从架构角度理解嵌套路由:在设计路由时,先画出页面结构图,标注哪些页面有公共布局,再决定嵌套层级。不要为了使用嵌套路由而使用,而是因为页面布局的天然层级关系才使用。
-
在项目中统一使用命名路由 :养成给每个路由配置
name属性的习惯,传参和跳转都使用{ name: "xxx", params/query: {} }的对象形式,避免路径硬编码带来的维护问题。