VueRouter进阶-动态路由与嵌套路由

Vue Router 进阶:动态路由、参数传递与嵌套路由完全指南

本文以 Vue Router 的核心进阶机制为主线,深入剖析声明式与编程式导航的底层差异、路由组件专属钩子的执行时序、激活样式的三种控制策略、query 与 params 参数持久化原理、动态路由段(:param)的匹配机制,以及嵌套路由(children)的渲染流水线。所有示例均基于 Vue CLI + Vue Router 3.x,可在标准 Vue 2 工程中直接运行。


目录

  1. 零、导读与学习价值
  2. 一、路由导航:声明式与编程式的选择
  3. 二、路由组件的专属钩子函数
  4. 三、路由激活状态与样式控制
  5. [四、路由参数传递: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")
  6. 五、动态路由与路径参数
  7. 六、嵌套路由(二级/三级路由)
  8. 七、综合实战:仿影院选片路由架构
  9. 总结

零、导读与学习价值

0.1 示例覆盖清单

章节 示例内容
第一章 声明式三种写法、编程式 push/replace/go
第二章 路由切换生命周期执行顺序完整演示
第三章 .router-link-activeactive-classlinkActiveClass 三种方案
第四章 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。

【面试考点】pushreplace 的区别?前者向历史栈追加记录,后者替换当前记录,区别体现在用户能否通过浏览器后退键回到上一页。


二、路由组件的专属钩子函数

名词解释

路由切换生命周期:当用户从路由 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 的创建链(beforeCreatecreatedbeforeMount),红色 4-5 是旧组件 A 的销毁(beforeDestroydestroyed),绿色 6 才是 B 的 mounted。关键结论用黄色框标注------新组件 beforeMount 早于旧组件 beforeDestroy,但新组件 mounted 一定晚于旧组件 destroyed 。这种"交织"不是巧合,而是 Vue Router 刻意为之:先把新组件创建到 beforeMount(虚拟 DOM 已生成但未落地),再销毁旧组件腾出位置,最后让新组件真正 mounted,从而保证视图切换没有"两个组件同时占位"或"中间空白"的窗口期。理解时序的实战价值是知道资源清理必须放在红色的 beforeDestroy ------此时旧组件仍持有 DOM 与数据引用;若拖到 destroyed 才清理,已经过晚。市面应用 :列表页跳详情页时在 beforeDestroyclearInterval 停掉轮询、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-activerouter-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-activerouter-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 钩子。因此监听参数变化需要 watchupdated,不能依赖 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。

【实战要点】

  1. query 的类型陷阱$route.query.id 永远是字符串,即使传入 { id: 1 },接收到的也是 "1"。需要用 Number($route.query.id) 转换类型。
  2. 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),让 mountedwatch/守卫共用,避免重复代码。市面应用 :商品详情页"看了又看"推荐位点击切换商品、用户主页在不同用户间跳转,都依赖 watch $routebeforeRouteUpdate 来刷新数据,否则会出现"地址变了内容没变"的经典 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 时:

  1. 路由器先匹配一级路由,找到 path: "/newsList" 对应的 NewsList 组件
  2. 路由器继续用剩余路径 /sport 匹配 NewsList 路由的 children,找到 path: "sport" 对应的 Sport 组件
  3. 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"
});

【代码注释】这份路由表演示了嵌套路由的几个关键点:① /newsListchildren 数组挂了四个子路由,子路由的 pathtiyu/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,浏览器地址栏也会相应更新。

【实战要点】

  1. 子路由的 path 不要以 / 开头(除非使用绝对路径),否则会被当作根路径处理,无法正确匹配。
  2. 每一级路由组件都需要包含 <router-view> 作为子路由的渲染出口,缺少它子路由内容将无法显示。
  3. 使用 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 壳、DetailsLogin)。注意这里没有任何导航条,因为导航条已经下沉到 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 这样的"开关"来手动控制显隐,架构更清晰。

【实战要点】

  1. 设计路由层级时,先问"这个页面有没有公共导航/布局",有则放入对应壳组件的 children,没有则作为独立一级路由。
  2. 壳组件(如 Index.vue)只负责布局框架,不处理业务逻辑,符合单一职责原则。
  3. 从详情页返回列表页使用 $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 $routebeforeRouteUpdate 重新请求
路由懒加载怎么做 component: () => import('xx.vue'),Webpack 按代码分割点打成独立 chunk,按需加载;webpackChunkName 可合并 chunk
嵌套路由如何设置默认子路由 在 children 中添加 { path: "/", redirect: "子路由名" }
如何实现部分页面无导航栏 无导航页面作为独立一级路由,有导航页面作为壳组件的子路由
$route vs $router $route 只读当前路由信息,$router 是路由器实例可调用方法

学习建议

  1. 动手验证钩子顺序:新建一个 Vue CLI 项目,在两个路由组件中分别添加所有生命周期钩子并打印日志,切换路由观察控制台输出,这是理解路由切换时序最有效的方式。

  2. 刻意对比 query 与 params:在同一个项目中分别实现 query 传参和 params 传参,然后在接收页手动刷新浏览器,观察哪种方式的数据丢失,直观感受两者的差异。

  3. 从架构角度理解嵌套路由:在设计路由时,先画出页面结构图,标注哪些页面有公共布局,再决定嵌套层级。不要为了使用嵌套路由而使用,而是因为页面布局的天然层级关系才使用。

  4. 在项目中统一使用命名路由 :养成给每个路由配置 name 属性的习惯,传参和跳转都使用 { name: "xxx", params/query: {} } 的对象形式,避免路径硬编码带来的维护问题。

相关推荐
梯度不陡1 小时前
Signal #17:Agent 开始进入组织系统
前端·javascript
何智超1 小时前
AI 微前端性能优化之旅(上):复盘
前端·vibecoding
许我半盏清茶1 小时前
前端路由:理解 hash 路由和 history 路由原理
前端·react.js
胡萝卜术1 小时前
从暴力到Z字形消元:力扣240「搜索二维矩阵II」的降维打击之路
前端·javascript·面试
比老马还六1 小时前
Bipes-Blockly项目二次开发/Coze智能体(十)
前端·嵌入式
1 小时前
Vue 3 组件封装与使用:保姆级教程
前端
星辰1 小时前
深入浅出 Android AOA 协议:通信流程与设备切换附着机制解析
前端
恋猫de小郭2 小时前
Amper 正式转正 Kotlin Toolchain ,Gradle 未来何去何从
android·前端·flutter
敲代码的彭于晏2 小时前
Bean 生命周期完全图解:前端同学也能看懂的 Spring 核心机制
java·前端·后端