主题
路由进阶(三)
本页对应 路由进阶,共 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 标签换成 router-link
为什么需要:二级导航里有“面经、收藏、喜欢、我的”四个入口,如果继续用普通 <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()"><</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()"><</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 中请求并赋值,模板负责动态渲染。掌握这条链路后,详情页就不再是静态页面,而是“路由参数 → 接口请求 → 数据渲染”的完整闭环。
课件图示








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