Skip to content

路由进阶(三) ​

本页对应 路由进阶,共 7 段视频,约 1 小时 11 分。

学习目标 ​

  • 能够配置通配符路由并接入 404 组件,通过手动修改任意不存在地址验证页面展示 404 提示。
  • 能够使用 router-link 的精确匹配类名与模糊匹配类名实现一级/二级导航高亮,并处理查询参数场景下的高亮冲突。
  • 能够使用 this.$router.push、replace、back、forward、go 完成指定地址跳转、前进后退及历史记录替换,并说明 push 与 replace 的差异。
  • 能够分别使用字符串、name、path 三种方式编写编程式导航,并在 query 与 params 传参场景中正确选择传参字段。
  • 能够根据面经项目页面结构规划一级路由与二级嵌套路由,明确各级 router-view 的展示位置及超链接所在页面。

知识点 ​

使用场景:什么时候需要二级路由 ​

为什么需要:当页面跳转时并不是整页替换,而只是页面中的某一块内容切换,就需要嵌套路由。比如面经项目中,/home 下面是固定的底部导航,上面内容区会在“面经、收藏、喜欢、我的”之间切换;这时底部导航不动,只切换内容区。

怎么想:先判断“谁包含谁”。如果四个子页面都渲染在 Layout 这个一级路由组件里面,那么它们就应该配置成 /home 的 children。转写稿里也强调:一级路由在 App 中展示,二级路由要看它是谁的孩子,就要在对应父组件的出口中展示。

怎么做:在一级路由下增加 children,并在父组件中放二级路由出口。

易错点:不要因为“二级”就随便挂到某个一级路由下;要看组件实际渲染位置。父组件没有 <router-view>,子组件不会显示。

语法:children 与二级出口 ​

为什么需要:Vue Router 需要知道“访问哪个地址时渲染哪个组件”,二级路由就是把子地址和子组件建立对应关系。

怎么想:一级路由的 path 通常写 /home,二级路由的 path 写在 children 里,不再加 /。因为二级路径是相对于父路径解析的。

怎么做:在一级路由下配置 children:

js
const router = new VueRouter({
  routes: [
    {
      path: '/home',
      component: Layout,
      children:[
        //children中的配置项 跟一级路由中的配置项一模一样 
        {path:'xxxx',component:xxxx.vue},
        {path:'xxxx',component:xxxx.vue},
      ]
    }
  ]
})

同时,在父组件 Layout.vue 中配置二级路由出口:

vue
<template>
  <div class="h5-wrapper">
    <div class="content">
      <router-view></router-view>
    </div>
  ....
  </div>
</template>

易错点:笔记特别提醒,一级路由 path 需要加 /,二级路由 path 不需要加 /;配置了嵌套路由,就一定要配置对应出口,否则组件不会渲染。

代码实现:面经项目的二级路由 ​

为什么需要:把语法落到项目里,才能理解“默认打开哪个子页面”和“底部导航如何切换内容区”。

怎么想:/home 对应 Layout,Layout 内部有内容区和底部导航。内容区用 <router-view> 承载 Article、Collect、Like、User 四个子组件;访问 / 时重定向到 /home/article,让用户一进来就看到面经列表。

怎么做:路由配置如下:

js
...
import Article from '@/views/Article.vue'
import Collect from '@/views/Collect.vue'
import Like from '@/views/Like.vue'
import User from '@/views/User.vue'
...

const router = new VueRouter({
  routes: [
    {
      path: '/',
      redirect: '/home/article'
    },
    {
      path: '/home',
      component: Layout,
      children:[
        {
          path:'article',
          component:Article
        },
        {
          path:'collect',
          component:Collect
        },
        {
          path:'like',
          component:Like
        },
        {
          path:'user',
          component:User
        }
      ]
    },
    ....
  ]
})

Layout.vue 中提供二级出口和导航:

vue
<template>
  <div class="h5-wrapper">
    <div class="content">
      <!-- 内容部分 -->
      <router-view></router-view>
    </div>
    <nav class="tabbar">
      <a href="#/home/article">面经</a>
      <a href="#/home/collect">收藏</a>
      <a href="#/home/like">喜欢</a>
      <a href="#/home/user">我的</a>
    </nav>
  </div>
</template>

易错点:redirect 要指向真实存在的二级地址;href 在 hash 模式下要写完整 #/home/article;如果内容区不写 <router-view>,点击导航只会改地址,页面内容不会切换。转写稿的作业梳理也说明:登录、注册、详情是单独一级页面,主页才做嵌套,别把不需要嵌套的页面硬塞进 children。

调试思路与易错点小结 ​

怎么排查:先看地址是否变成 /home/article 这类二级路径;再看 Layout 里有没有二级 <router-view>;最后检查 children 是否配置在正确的父路由下。转写稿总结里反复强调“规则配好、出口放好”,嵌套路由就这两件核心事。

易错点:二级 path 多写 /;父级出口和子级出口混淆;默认重定向地址写错;导航链接与路由规则不一致;忘记在父组件中放出口。二级路由可以继续嵌套三级、四级,但每一层都要有对应出口,且路径层级要与组件包含关系一致。

为什么需要:二级导航里有“面经、收藏、喜欢、我的”四个入口,如果继续用普通 <a> 标签,浏览器只知道跳转地址,Vue Router 不会自动告诉你“当前哪个导航被选中”。要实现“点谁谁高亮”,就得自己写判断逻辑,麻烦且容易漏。<router-link> 是 Vue Router 提供的组件,它会渲染成 <a>,同时根据当前路由自动加高亮类名。

怎么想:笔记里的思路很明确:先把 <a> 替换成 <router-link>,用 to 属性配置目标路径;再结合高亮类名写样式。笔记推荐用模糊匹配类 router-link-active,因为二级导航的四个地址分别是 /home/article、/home/collect、/home/like、/home/user,路径区分明显,模糊匹配已经够用。

怎么做:在 Layout.vue 的 nav.tabbar 中,把原来的 a 标签换成 router-link,to 直接写路径,不要加 #。然后在样式里给 a.router-link-active 设置颜色。

易错点:to 不要写成 href,也不要加 #;类名不要手写错,router-link-active 是组件自动加的,不需要在模板上手动加 class;样式选择器建议带上 a,写成 a.router-link-active,避免和项目里其他同名类冲突。

代码实现:Layout.vue 的二级导航 ​

为什么需要:只改思路不落地,高亮不会生效。必须把模板结构和样式同时改掉。

怎么想:四个导航项对应四个二级路由地址,to 的值要和路由规则保持一致。样式写在 Layout.vue 的 <style> 中,直接针对当前激活链接。

怎么做:保留笔记中的代码结构:

vue
....
    <nav class="tabbar">
      <router-link to="/home/article">面经</router-link>
      <router-link to="/home/collect">收藏</router-link>
      <router-link to="/home/like">喜欢</router-link>
      <router-link to="/home/user">我的</router-link>
    </nav>

<style>
   a.router-link-active {
      color: orange;
    }
</style>

易错点:如果页面里已经有全局 a 的颜色样式,a.router-link-active 可能因为选择器权重不够而看不到橙色,需要检查样式优先级;另外,to 的路径要和路由配置完全对应,少写或多写斜线都会导致匹配不到。

高亮类名与调试过程 ​

为什么需要:要知道高亮为什么生效,必须理解 router-link 自动加了什么类。转写稿里通过右键检查元素验证:router-link 最终会解析成 <a>,当前访问的链接会自动多出两个类名,其他未选中的链接没有。切换导航后,类名会跟着切换,这就是“排他效果”。

怎么想:Vue Router 默认提供模糊匹配类和精确匹配类。模糊匹配类 router-link-active 的特点是:只要当前地址包含该链接的路径,就会加上;精确匹配类只在地址完全一致时加。二级导航四个地址不同,用模糊匹配即可。转写稿还补充了一个调试场景:如果多个链接的路径相同、只有查询参数不同,模糊匹配会让同一路径下的链接都高亮,这时需要结合精确匹配类单独处理。

怎么做:先在浏览器里检查当前导航的 <a>,确认它有 router-link-active;再切换到“收藏”等导航,观察类名是否转移。若出现“父级或同路径链接也高亮”,不要急着改样式,先判断是模糊匹配范围过大,还是地址只差参数。必要时用精确匹配类控制,或者按转写稿思路先恢复默认样式,再给精确匹配项单独加高亮。

易错点:不要把 router-link-active 当成普通 class 手动写在模板上;不要以为 router-link 不会渲染成 a;调试时先看元素类名,再看样式是否被覆盖,不要一上来就写 !important。本节二级导航路径区分明显,按笔记用 a.router-link-active 就能实现高亮。

本节目标与实现链路 ​

首页文章列表不能写死在模板里,而要从接口拿到数据后动态渲染。为什么需要这样做?因为文章数量、作者、点赞数、浏览数都会变化,写死就无法维护。怎么想?把任务拆成四步:安装请求库、确认接口文档、在 created 中请求并存入 data、模板中用 v-for 渲染。怎么做?先跑通“请求—存数据—渲染”这条链路,再补样式。易错点:不要一上来就写复杂模板,先用 console.log 确认接口返回结构,再决定解构层级。

安装 axios ​

为什么需要?项目里要发 HTTP 请求,使用 axios 比手写原生请求更统一。怎么想?先看项目是否已有依赖,没有就安装。怎么做?在项目根目录执行:

bash
yarn add axios
# 或
npm i axios

易错点:不要混用包管理器导致锁文件混乱;安装后若终端报找不到模块,先确认命令是在项目根目录执行,并重启开发服务。

确认接口文档 ​

为什么需要?请求方式、请求地址、请求参数决定了代码怎么写,不能靠猜。怎么想?先读文档,再写请求。怎么做?本节接口为:

vue
请求地址: https://mock.boxuegu.com/mock/3083/articles
请求方式: get

易错点:地址抄错、GET 写成 POST、参数漏传,都会导致请求失败。转写稿里乱改地址后页面出现 404,并强调“地址不对要先核对地址”;迁移到本节就是:请求地址或接口路径写错时,先看 Network 面板里的请求是否 404/200,不要急着改模板。

在 created 中发请求并存入 data ​

为什么需要?页面渲染依赖数据,数据要先进入响应式系统。怎么想?data 里先准备空数组,created 阶段组件实例已创建、data 可用,适合尽早发请求。怎么做?按笔记代码:

vue
data() {
  return {
    articelList: [],
  }
},
async created() {
  const { data: { result: { rows } } } = await axios.get('https://mock.boxuegu.com/mock/3083/articles')
  this.articelList = rows
},

易错点:第一,解构层级必须和接口返回一致,笔记按 data.result.rows 取列表;第二,this.articelList 不要漏 this;第三,笔记中变量名是 articelList,实际项目建议改为 articleList,但本节代码按笔记保留;第四,请求可能失败,实际项目应加 try/catch 或错误提示。

模板动态渲染 ​

为什么需要?拿到数组后要循环生成文章项,否则只请求不展示没有意义。怎么想?外层容器放 v-for,每项用 :key 标记唯一值,字段用插值或 :src 绑定。怎么做?完整模板如下,图片 alt 改为中文:

vue
<template>
  <div class="article-page">
    <div class="article-item" v-for="item in articelList" :key="item.id">
      <div class="head">
        <img :src="item.creatorAvatar" alt="作者头像" />
        <div class="con">
          <p class="title">{{ item.stem }}</p>
          <p class="other">{{ item.creatorName }} | {{ item.createdAt }}</p>
        </div>
      </div>
      <div class="body">
        {{ item.content }}
      </div>
      <div class="foot">点赞 {{ item.likeCount }} | 浏览 {{ item.views }}</div>
    </div>
  </div>
</template>

易错点:v-for 的 item 只在当前循环内有效;:key 用稳定唯一值,不要用索引凑合;字段名要和接口一致,如 creatorAvatar、stem、likeCount;图片地址要用 :src,不能写成普通 src。

调试与检查清单 ​

为什么需要?请求渲染出错时,问题可能在地址、返回结构、字段名或模板。怎么想?按“先请求、再数据、后模板”的顺序排查。怎么做?先看 Network 中请求是否成功,再看 rows 是否有值,最后审查元素看模板是否渲染。转写稿后半段主要讲路由的 404、链接高亮和编程式导航,本节不混入这些内容,只借用其排查思路:地址先核对,页面异常先审查元素和请求。易错点:不要用“页面空白”直接判断模板错,很多时候是请求 404 或解构层级不对;也不要忘记空数组时页面本来就没有文章项。

场景:为什么要把文章 id 带到详情页 ​

为什么需要: 面经列表页展示的是多条文章,用户点击哪一条,详情页就要请求哪一条的数据。详情页本身不知道用户点的是哪篇,所以必须在跳转时把当前文章的 id 带过去。

怎么想: 先把“参数放在哪里”想清楚。查询参数传参的特点是把参数拼在地址栏问号后面,例如 /detail?id=1;它的好处是不用先改路由规则,详情页通过 $route.query.id 读取即可。转写稿里也提到:如果是查询参数,编程式导航里对应的是 query,参数会被拼到 URL 后面,形成问号格式。

怎么做: 列表页点击时调用 this.$router.push(),把完整路径和查询字符串传进去;详情页在 created 里读取 this.$route.query.id。

易错点: 不要把 $route 和 $router 混用:$router.push 负责跳转,$route.query 负责读取当前路由参数。

列表页跳转:用 $router.push 拼接查询字符串 ​

为什么需要: 列表项是 v-for 渲染出来的,每一项都有自己的 item.id,点击事件里必须把“当前项”的 id 拼到跳转地址中。

怎么想: 笔记里给出的写法是:

js
this.$router.push('/detail?参数1=参数值&参数2=参数值')

对应到文章列表,就是把参数名写成 id,参数值写成 item.id。因为地址里用了模板字符串,所以要用反引号,并在 ${} 中取 item.id。

怎么做: 在 Article.vue 的列表项上绑定点击事件:

vue
<template>
  <div class="article-page">
    <div class="article-item" 
      v-for="item in articelList" :key="item.id" 
      @click="$router.push(`/detail?id=${item.id}`)">
     ...
    </div>
  </div>
</template>

易错点: 一是不要漏掉 ?,否则 id 不会成为查询参数;二是多个参数之间要用 & 连接;三是模板里使用 $router.push 时注意反引号和 ${} 的写法,不要写成普通单引号导致变量不生效。

详情页接收:用 $route.query.id 读取参数 ​

为什么需要: 跳转只完成了“把 id 放到地址栏”,详情页还要把 id 取出来,后续才能根据 id 发请求获取文章详情。

怎么想: 查询参数挂在 $route.query 上,参数名是 id,所以读取方式就是 this.$route.query.id。转写稿中查询参数场景也是用 query 来接收,和动态路由的 params 要区分开。

怎么做: 在 ArticleDetail.vue 的 created 中打印验证:

vue
  created(){
    console.log(this.$route.query.id)
  }

易错点: 查询参数不需要提前改造路由规则;如果你写成 this.$route.params.id,在查询参数场景下是拿不到的。动态路由传参才需要先改路由规则,再用 params 读取。

调试与验证:先看地址栏,再看控制台 ​

为什么需要: 传参失败时,最容易出错的是“地址栏里到底有没有 ?id=...”和“详情页读的是不是 query”。

怎么想: 先点列表项,观察地址栏是否变成 /detail?id=某篇文章的id;再看详情页控制台是否打印出同一个 id。转写稿中排查链接高亮时也强调过看地址、看参数,这里同样适用:地址不对,先查 push 的拼接;地址对但取不到,先查 $route.query 是否写错。

怎么做: 点击任意文章,确认地址栏出现 ?id=...,并在 created 的日志中核对 id 是否一致。

易错点: created 里能拿到路由参数,但不要把它当成“请求已经完成”;本节只验证传参,后续再根据 id 发请求。查询参数的值通常是字符串,比较时要注意类型。

先明确本节要解决什么问题 ​

面经项目里,文章列表页点击某一项后,要进入这篇文章的详情页。列表里有很多文章,不能给每一篇都写一条固定路由,所以需要用“动态路由传参”:把文章 id 放到地址里,详情页再根据 id 取数据。转写稿里反复提到的“动态路由传参”,就是本节的核心;同音字里的“动态录入传参”“拍入/派照”都应理解为动态路由和 params。

改造路由:让地址里能带 id ​

为什么需要:如果详情页路由写成固定的 /detail,所有文章都进同一个地址,详情页无法知道用户点的是哪一篇。把 id 放进地址,例如 /detail/1、/detail/2,详情页才能区分文章。

怎么想:路由规则里用 :id 占位,表示这一段是可变参数。访问 /detail/1 时,1 会被路由收集到 params.id 中。

怎么做:在 router/index.js 中把详情路由改成:

js
...
  {
      path: '/detail/:id',
      component: ArticleDetail
  }

易错点:path 里漏写 :id,详情页就取不到 params.id;:id 的名字要和后面取值一致,不能路由写 :id、取值写 params.articleId。

列表页动态传参:点击时把 id 拼进地址 ​

为什么需要:列表页知道当前点击的是哪一条数据,详情页不知道。所以要在点击事件里把 item.id 拼到跳转地址中。

怎么想:这里用 $router.push() 做编程式跳转。地址不是写死的,而是模板字符串 `/detail/${item.id}`。转写稿里也提到,动态路由传参时参数是拼在路径里的,和问号形式的查询参数不是一回事。

怎么做:在 Article.vue 中给文章项绑定点击事件:

vue
<div class="article-item" 
     v-for="item in articelList" :key="item.id" 
     @click="$router.push(`/detail/${item.id}`)">
       ....
 </div>

易错点:不要写成普通字符串 '/detail/item.id',那样 id 不会被解析;模板字符串要用反引号;v-for 的 :key 仍要保留,避免列表复用问题。

详情页获取参数:用 this.$route.params.id ​

为什么需要:详情页要根据 id 发请求或展示对应内容,所以必须先从当前路由中拿到参数。

怎么想:$router 是路由实例,用来跳转;$route 是当前路由信息,用来读取参数。取动态路由参数用 params,不是 query。

怎么做:在 ArticleDetail.vue 的 created 中打印验证:

vue
  created(){
    console.log(this.$route.params.id)
  }

调试时可以按转写稿的思路做:从列表点击某一项,观察地址是否变成 /detail/1 这类形式;进入详情页后看控制台是否打印出对应 id。若打印 undefined,先回头检查路由规则是否写了 :id,再检查跳转地址是否真的拼了 id。

易错点:把 $route 写成 $router,会拿不到 params;把 params 写成 query,动态路由参数会取不到;在模板里可写 $route.params.id,在 JS 里要写 this.$route.params.id。

额外优化:点击回退到上一页 ​

为什么需要:详情页通常需要一个返回按钮,用户看完详情后能回到列表页,而不是手动改地址。

怎么想:浏览器历史里有上一页时,可以直接调用路由的 back(),它等价于后退一步。转写稿里也提到 router.back() 是后退,go(-1) 也是后退一次。

怎么做:在详情页导航栏绑定点击事件:

vue
<template>
  <div class="article-detail-page">
    <nav class="nav"><span class="back" @click="$router.back()">&lt;</span> 面经详情</nav>
     ....
  </div>
</template>

易错点:如果用户是直接打开详情页、没有上一页历史,back() 可能没有可回退页面;实际项目里可结合业务决定是否兜底跳转到列表页。另外,回退按钮是 $router.back(),不要误写成 $route.back()。

本节目标:把面经详情页从“静态壳子”变成“接口驱动” ​

这一节要完成的是面经详情页渲染:进入详情页后,根据地址里的文章 id 请求接口,把标题、时间、浏览量、点赞数、作者头像/昵称和正文动态显示出来。转写稿里反复强调“地址乱改会 404”“动态路由参数要用 params 取”,这两点正好是详情页的前提:详情页不是写死内容,而是由路由参数决定请求哪一篇文章。

1. 先导入 axios:为什么需要 → 怎么想 → 怎么做 → 易错点 ​

为什么需要:详情数据在服务器上,页面本身只有结构,必须发请求拿数据。怎么想:先确认项目里有没有安装/引入 axios,再按接口文档发 GET 请求。怎么做:在组件 script 顶部导入:

js
import axios from 'axios'

易错点:只写 axios.get 却忘了导入,会报 axios is not defined;转写稿里提到“要用插件先装下来”,同理,请求库要先具备再使用。

2. 查看接口文档:先确认地址和参数 ​

本节笔记给出的接口文档是:

vue
 请求地址: https://mock.boxuegu.com/mock/3083/articles/:id
 请求方式: get

为什么需要:详情页的 id 不是随便写的,必须和接口地址中的 :id 对应。怎么想:接口里的 :id 是动态段,页面地址中也应通过动态路由参数拿到它。转写稿讲动态路由传参时明确说“规则里加冒号 id,组件里用 $route.params.id 取”,这里就是同一思路。怎么做:在组件中取 this.$route.params.id,再拼到接口地址后面。易错点:把动态参数写成 query 取法,或路由规则没有配置动态段,都会导致 id 拿不到;地址不对时就会像转写稿演示的那样走到 404 提示。

3. 在 created 中发送请求:为什么放这里 ​

为什么需要:详情页一进入就要展示数据,越早拿到越好。怎么想:created 时组件实例已创建,可以访问 this.$route,适合发初始请求。怎么做:保留笔记中的实现:

vue
 data() {
    return {
      articleDetail:{}
    }
  },
  async created() {
    const id = this.$route.params.id
    const {data:{result}} = await axios.get(
      `https://mock.boxuegu.com/mock/3083/articles/${id}`
    )
    this.articleDetail = result
  },

易错点:articleDetail 初始值要是空对象,否则模板访问 articleDetail.stem 容易报错;解构层级要按接口返回写,笔记用的是 data.result,不要多一层或少一层;created 用普通方法写法时 this 正常,若改成箭头函数要小心 this 丢失。

4. 页面动态渲染:模板只负责显示 ​

为什么需要:数据拿到后要替换静态文字。怎么想:模板中所有会变化的字段都用插值或 :src 绑定。怎么做:保留笔记模板,并把头像图片的 alt 改为中文:

vue
<template>
  <div class="article-detail-page">
    <nav class="nav">
      <span class="back" @click="$router.back()">&lt;</span> 面经详情
    </nav>
    <header class="header">
      <h1>{{articleDetail.stem}}</h1>
      <p>{{articleDetail.createAt}} | {{articleDetail.views}} 浏览量 | {{articleDetail.likeCount}} 点赞数</p>
      <p>
        <img
          :src="articleDetail.creatorAvatar"
          alt="创作者头像"
        />
        <span>{{articleDetail.creatorName}}</span>
      </p>
    </header>
    <main class="body">
      {{articleDetail.content}}
    </main>
  </div>
</template>

易错点:图片地址要用 :src,不能写死 src;返回按钮用 $router.back(),不是 $route.back();转写稿里“面筋”是“面经”的同音转写,“拍入”是 params,“宽主”是 query,详情页接口是 :id 动态段,所以这里用 params 而不是 query。

5. 调试顺序与小结 ​

按转写稿的排查思路:先看地址和路由参数是否匹配,再看接口文档地址是否拼对,最后看模板字段名是否和返回结果一致。详情页按作业梳理是一级路由中的独立页面,展示在顶层出口中;它通过 $route.params.id 决定请求哪篇面经,再在 created 中请求并赋值,模板负责动态渲染。掌握这条链路后,详情页就不再是静态页面,而是“路由参数 → 接口请求 → 数据渲染”的完整闭环。

课件图示 ​

课件第2页:目录 Contents u 路由进阶 ① 路由模块封装 ② 声明式导航 & 导航

课件第42页:案例效果: 分析:配路由 + 实现功能 1. 配路由 ① 首页 和 面经详情,两

课件第18页:1. 查询参数传参 2. 动态路由传参 声明式导航 - 跳转传参 目标:在跳转路

课件第22页:总结 小结 声明式导航跳转时, 有几种方式传值给路由页面? ① 查询参数传参 (

课件第48页:总结 总结 1. 项目案例实现的基本步骤分哪两大步? ① 配路由 ② 实现页面功

课件第35页:两种传参方式:查询参数 + 动态路由传参 两种跳转方式,对于两种传参方式都支持:

课件第36页:两种传参方式:查询参数 + 动态路由传参 两种跳转方式,对于两种传参方式都支持:

课件第38页:两种传参方式:查询参数 + 动态路由传参 两种跳转方式,对于两种传参方式都支持:

代码示例 ​

示例一:二级路由配置 + 二级导航高亮 + 404 兜底 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>面经 - 二级路由 & 高亮 & 404</title>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.js"></script>
  <style>
    .tabbar { display: flex; justify-content: space-around; border-top: 1px solid #eee; }
    .tabbar a { padding: 8px 12px; text-decoration: none; color: #333; }
    /* 推荐用模糊匹配类名做高亮 */
    a.router-link-active { color: orange; }
  </style>
</head>
<body>
  <div id="app">
    <!-- 一级路由出口 -->
    <router-view></router-view>
  </div>

  <script>
    // ---------- 组件 ----------
    const Article = { template: `<div class="article-page">面经列表</div>` }
    const Collect = { template: `<div class="collect-page">收藏</div>` }
    const Like    = { template: `<div class="like-page">喜欢</div>` }
    const User    = { template: `<div class="user-page">我的</div>` }

    // 404 组件(对应转写稿的 NotFound)
    const NotFound = {
      template: `<div class="not-found"><h2>404</h2><p>很抱歉,你找的页面找不到</p></div>`
    }

    // 一级路由对应的 Layout 组件,内部放二级路由出口
    const Layout = {
      template: `
        <div class="h5-wrapper">
          <div class="content">
            <!-- 二级路由出口:不写这个,子路由组件不会渲染 -->
            <router-view></router-view>
          </div>
          <nav class="tabbar">
            <router-link to="/home/article">面经</router-link>
            <router-link to="/home/collect">收藏</router-link>
            <router-link to="/home/like">喜欢</router-link>
            <router-link to="/home/user">我的</router-link>
          </nav>
        </div>`
    }

    // ---------- 路由规则 ----------
    const router = new VueRouter({
      routes: [
        { path: '/', redirect: '/home/article' },
        {
          path: '/home',            // 一级路由 path 要加 /
          component: Layout,
          children: [               // 二级路由 path 不加 /
            { path: 'article', component: Article },
            { path: 'collect', component: Collect },
            { path: 'like',    component: Like },
            { path: 'user',    component: User }
          ]
        },
        // 通配符规则:和上面所有规则都匹配不上时,才走这里
        { path: '*', component: NotFound }
      ]
    })

    new Vue({ router }).$mount('#app')
  </script>
</body>
</html>

关键行与易错点

  • 一级路由 path: '/home' 要带 /,children 里的 path: 'article' 不能带 /,最终地址是 /home/article。
  • Layout 里必须写 <router-view></router-view>,这是二级路由出口;只配置 children 而不写出口,子组件不会被渲染。
  • 二级路由配置到哪个一级路由下,取决于这些组件要渲染在哪个一级路由的出口里。
  • 超链接用 <router-link to="/home/article">,to 里不要加 #;渲染后会自动变成 <a> 并给当前匹配的链接加 router-link-active 类名,直接写样式即可高亮。
  • 404 规则 { path: '*', component: NotFound } 放在最顶层;它优先与其它规则匹配,只有其它规则都匹配不上时才命中,位置一般放最后当兜底。

示例二:列表请求渲染 + 传参跳转详情 + 详情渲染 + 返回 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>面经 - 列表与详情</title>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
  <div id="app">
    <router-view></router-view>
  </div>

  <script>
    // ---------- 面经列表页 ----------
    const Article = {
      data() {
        return { articelList: [] }
      },
      async created() {
        // 解构到 rows,一次取到列表数组
        const { data: { result: { rows } } } =
          await axios.get('https://mock.boxuegu.com/mock/3083/articles')
        this.articelList = rows
      },
      template: `
        <div class="article-page">
          <div class="article-item"
               v-for="item in articelList" :key="item.id"
               @click="$router.push('/detail/' + item.id)">
            <div class="head">
              <img :src="item.creatorAvatar" alt="" />
              <div class="con">
                <p class="title">{{ item.stem }}</p>
                <p class="other">{{ item.creatorName }} | {{ item.createdAt }}</p>
              </div>
            </div>
            <div class="body">{{ item.content }}</div>
            <div class="foot">点赞 {{ item.likeCount }} | 浏览 {{ item.views }}</div>
          </div>
        </div>`
    }

    // ---------- 详情页 ----------
    const ArticleDetail = {
      data() {
        return { articleDetail: {} }
      },
      async created() {
        const id = this.$route.params.id          // 动态路由传参取值
        const { data: { result } } = await axios.get(
          `https://mock.boxuegu.com/mock/3083/articles/${id}`
        )
        this.articleDetail = result
      },
      template: `
        <div class="article-detail-page">
          <nav class="nav">
            <span class="back" @click="$router.back()">&lt;</span> 面经详情
          </nav>
          <header class="header">
            <h1>{{ articleDetail.stem }}</h1>
            <p>{{ articleDetail.createAt }} | {{ articleDetail.views }} 浏览量 |
               {{ articleDetail.likeCount }} 点赞数</p>
            <p>
              <img :src="articleDetail.creatorAvatar" alt="" />
              <span>{{ articleDetail.creatorName }}</span>
            </p>
          </header>
          <main class="body">{{ articleDetail.content }}</main>
        </div>`
    }

    // ---------- 路由 ----------
    const router = new VueRouter({
      routes: [
        { path: '/', redirect: '/home/article' },
        { path: '/home/article', component: Article },
        // 动态路由:先改造路由加 :id,再传参
        { path: '/detail/:id', component: ArticleDetail }
      ]
    })

    new Vue({ router }).$mount('#app')
  </script>
</body>
</html>

关键行与易错点

  • 列表请求写在 created 中,用 async/await,把结果赋给 data 里的 articelList,模板用 v-for 动态渲染。
  • 传参有两种:查询参数 this.$router.push('/detail?id=' + item.id),详情页用 this.$route.query.id 取;动态路由 /detail/:id,详情页用 this.$route.params.id 取。push 用 $router,取值用 $route,多一个 r 就错。
  • 用动态路由传参,必须先在路由规则里写 path: '/detail/:id',否则跳过去匹配不上。
  • 详情请求要拼上 id:/articles/${id},id 从 this.$route.params.id 拿。
  • 返回上一页用 @click="$router.back()",别写成 $route.back()。

示例三:编程式导航三种写法(含传参) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>编程式导航</title>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.js"></script>
</head>
<body>
  <div id="app">
    <router-view></router-view>
  </div>

  <script>
    // 目标页:分别读取动态路由参数和查询参数
    const TopList = {
      template: `
        <div>
          <p>排行榜页</p>
          <p>params.id = {{ $route.params.id }}</p>
          <p>query.id = {{ $route.query.id }}</p>
        </div>`
    }

    // 关注页:用 JS 代码跳转
    const Follow = {
      template: `
        <div>
          <p>关注页</p>
          <button @click="goNewSong">新歌榜(字符串写法)</button>
          <button @click="goTopList">排行榜(name + params)</button>
          <button @click="goSinger">歌手(path + query)</button>
        </div>`,
      methods: {
        // 写法一:直接传完整字符串地址
        goNewSong() {
          this.$router.push('/toplist/1')
        },
        // 写法二:对象 + name,动态路由参数用 params
        goTopList() {
          this.$router.push({ name: 'toplist', params: { id: 2 } })
        },
        // 写法三:对象 + path,查询参数用 query
        goSinger() {
          this.$router.push({ path: '/toplist', query: { id: 3 } })
        }
      }
    }

    const router = new VueRouter({
      routes: [
        { path: '/', redirect: '/follow' },
        { path: '/follow', component: Follow },
        // 给规则加 name,才能用 name 跳转
        { path: '/toplist/:id', name: 'toplist', component: TopList }
      ]
    })

    new Vue({ router }).$mount('#app')
  </script>
</body>
</html>

关键行与易错点

  • 编程式导航就是“用 JS 代码实现页面跳转”,典型场景是登录/注册成功后再跳首页,这种没有超链接可点,只能用 this.$router.push(...)。
  • 三种写法:push('/完整地址')、push({ name: 'xxx', params: {...} })、push({ path: '/xxx', query: {...} })。
  • 用 name 跳转的前提是路由规则里写了 name;name 写法比 path 写法简短,实际开发更常用。
  • 易错点:push({ path: '/xxx', params: {...} }) 这种组合,动态路由参数拼不到 URL 后面,参数会丢;要传动态路由参数就用 name + params,要传查询参数就用 query。
  • 前进后退用 this.$router.go(1)(前进一次)、this.$router.go(-1)(后退一次);back() 等价于 go(-1)。
  • push 会新增一条历史记录(可前进后退),replace 只是替换当前地址、不新增历史记录。

常见错误 ​

1. 二级路由内容不渲染,页面只剩 tabbar

  • 现象:地址栏已变成 /home/article,Layout 骨架出来了,但内容区一片空白。
  • 原因:Layout.vue 里漏写 <router-view>,或者把出口写到了 App.vue 里(那里是一级出口,只负责渲染 Layout 本身)。
  • 修法:出口必须写在"内容要被替换"的那个组件内——二级路由的出口就在 Layout 的 .content 容器里,一级路由的出口才在 App.vue。

2. children 里的 path 前面多写了 /

  • 现象:点 tab 后地址跳到 /article,父级 Layout 整块消失或直接 404。
  • 原因:children 中的 path 一旦以 / 开头就被当成根路径,脱离父级,嵌套关系失效。
  • 修法:一级 path: '/home' 带斜杠,二级 path: 'article' 不带;写完自查一遍斜杠只出现在最外层。

3. redirect 指到了父级,根路径打开是空壳

  • 现象:访问 / 能看到底部导航,但上方内容区空白。
  • 原因:redirect: '/home' 只落到父级,父级本身没有默认子路由,出口自然没东西可渲染。
  • 修法:重定向要落到具体叶子,写 redirect: '/home/article'。

4. 高亮样式写了但不生效

  • 现象:点击后颜色没变化,或者只有部分项变色。
  • 原因:类名写错(写成 active、router-link-actived),或选择器权重不够被 a 的默认色覆盖,或用了 exact-active 而期望父级链路也亮。
  • 修法:类名照抄 router-link-active / router-link-exact-active;权重不够就提高选择器层级或加 !important;想让父级跟着亮就用模糊匹配那个。

5. 传参方式和取值方式对不上,详情页拿到 undefined

  • 现象:$route.query.id 或 $route.params.id 打印出来是 undefined,请求地址拼成 .../articles/undefined。
  • 原因:用 query 传却用 params 取(或反之);动态路由那条还漏了改造 path: '/detail/:id'。
  • 修法:问号传参 → $route.query;路径传参 → 先给规则加 :id,再用 $route.params。两者必须成对出现。

6. 模板字符串拼接写错,跳到字面量地址

  • 现象:点击后地址是 /detail/id=3 或 /detail/$%7Bitem.id%7D,详情页报错。
  • 原因:用了单/双引号而非反引号,或把 ${} 写在引号外面、漏写 $。
  • 修法:整体用反引号包裹,形如 @click="$router.push(`/detail/${item.id}`)";不确定就先在 methods 里拼好再调用。

7. this.$route.push 报 not a function

  • 现象:控制台 this.$route.push is not a function。
  • 原因:$route 是当前路由信息对象(只读,存 path/query/params),$router 才是路由实例(有 push/replace/go/back)。
  • 修法:跳转一律 this.$router.push(...),取值一律 this.$route.xxx。记法:带 r 的干活,不带 r 的看数据。

8. path 和 params 混着用,参数凭空消失

  • 现象:push({ path: '/detail', params: { id: 1 } }) 跳过去了,但 $route.params.id 是 undefined,地址栏也没有参数。
  • 原因:对象写法中 path 会忽略 params,这是框架行为,不是 bug。
  • 修法:要带 params 就用 name 跳转;坚持用 path 就把参数拼进路径字符串里。

9. 通配符 * 写在规则数组最前面

  • 现象:所有页面都变成 404,正常路由全部失效。
  • 原因:路由按定义顺序匹配,* 排第一会拦截一切。
  • 修法:{ path: '*', component: NotFound } 永远放数组最后一条当兜底。

10. 详情页在文章间来回切换,内容不更新

  • 现象:从 id=1 跳到 id=2,地址变了但页面还是第一篇的内容。
  • 原因:同一组件被复用,created 只执行一次,不会因参数变化重跑。
  • 修法:watch: { '$route.params.id': { immediate: true, handler() { this.getDetail() } } },或在 <router-view :key="$route.fullPath"> 上强制重建。

练习 ​

1. 兜底页接入 给面经项目加一个 NotFound.vue(内含 404 图),要求任意乱输地址(如 /abc、/home/xyz)都能命中它,且原有页面全部正常。完成后回答:把 * 规则临时移到数组第一条,观察现象并解释。

2. 高亮对比实验 把 Layout 的四个 tab 从 <a href="#/..."> 换成 <router-link to="...">,先只写 .router-link-active 样式,再换成 .router-link-exact-active 样式,分别在 /home/article 和 /home/collect 下截图对比。用一句话说明两者差异,并判断 tabbar 场景该用哪个。

3. 传参方式改造 把 Article → Detail 的传参从 query 改为动态路由。要求列出需要改动的所有位置(提示:不止一处),并说明详情页里取值的代码怎么跟着改;改完后再改回 query,确认两种方式都能跑通。

4. 编程式导航三连 在"关注"页用 JS 完成三个跳转:

  • 点"新歌榜"→ 跳到排行榜新歌榜(用字符串写法);
  • 点"张惠妹"→ 跳到歌手页并带上查询参数(用 name + query);
  • 再尝试用 path + params 跳同一目标,记录地址栏和 $route.params 的实际结果,说明为什么。

5. 找 bug 给定一段路由配置:根路径 redirect: '/home'、children 里 path: '/article'、* 规则排在第一、Layout.vue 中无 <router-view>。找出全部问题并修复,最后口述每条问题会导致什么现象。

原视频对照 ​

#视频时长
1404处理04:37
2链接高亮13:40
3链接高亮补充07:25
4使用JS进行路由跳转17:17
5使用JS进行路由跳转-传递query参数06:32
6总结14:03
7作业梳理07:43

小结 ​

  1. 嵌套路由是三件事的合集——children 配置、出口位置、路径斜杠规则,缺任何一件的表现都是"白屏",所以出问题先按这三项逐一排查,别急着怀疑框架。
  2. "地址对应哪个组件"和"组件在哪里渲染"是两个独立问题:前者由 routes 决定,后者由 <router-view> 的位置决定,出口放错层级是新手最高频的坑。
  3. 高亮不需要手写排他逻辑,框架用"精确匹配"和"模糊匹配"两个类名把"我是不是当前项"和"我是不是当前项的祖先"分开表达,选哪个取决于你想亮自己还是亮整条链路。
  4. 传参方式和取值方式是强绑定的成对关系,query 配 query、params 配 params,且动态路由必须先改规则加 :id,否则一定拿到 undefined。
  5. 编程式导航优先用 name:它短、可读、支持 params;path 写法的 params 会被静默丢弃,字符串写法又长又容易拼错。
  6. 数据请求放在 created 里没问题,但要意识到组件复用会让 created 只跑一次,参数变化场景必须靠 watch 或 key 补上。
  7. 兜底路由是体验底线,规则顺序决定成败——* 永远最后。
  8. 动手前先在纸上画出层级图(哪个组件里嵌哪个出口),比写完再 debug 省一半时间。