主题
需求与路由守卫(Lesson11 1/3)
本页对应 Lesson11,共 6 段视频,约 1 小时 56 分。
学习目标
- 能独立搭建包含一级路由、二级路由、404 路由的 Vue Router 配置,并区分有导航布局与无导航页面的路由层级。
- 能实现“未登录访问个人中心自动跳转登录页、登录成功后返回个人中心”的需求,并使用编程式导航与
localStorage完成状态保存和退出登录。 - 能使用
beforeRouteEnter在进入路由前完成权限校验,通过next()放行或next('/login')跳转,并在回调中获取组件实例。 - 能使用
beforeRouteUpdate响应同一组件内路由参数变化,根据最新to参数重新请求并渲染详情数据。 - 能使用
beforeRouteLeave在离开路由前执行确认提示或数据保护,并正确调用next()控制是否离开。 - 能配置路由独享守卫
beforeEnter,说明其执行时机、to/from/next用法以及与组件内守卫的区别。
知识点
需求拆解:先分清“有导航”和“无导航”
为什么需要:做路由前先明确页面清单和导航差异,否则会把所有页面都塞进同一个出口,导致登录页、404 也出现导航。
怎么想:把“有导航”的页面放进同一个父级容器,把“无导航”的页面放在同级;请求地址与页面一一对应。
怎么做:本节需求如下:
请求地址:/ ---------------------------------------->首页(有导航)
请求地址:/newsList ---------------------------------------->新闻列表页(有导航)
请求地址:/goodsList ---------------------------------------->商品列表页(有导航)
请求地址:/my---------------------------------------->我的个人中心(有导航)
请求地址:/a;lskdjf;lakdsjf; ---------------------------------------->404页面(无导航)
请求地址:/login------------------------------------------------------->登陆界面(无导航)
易错点:404 的 path:"*" 必须放在路由数组最后;不要把所有页面都写成一级路由,否则导航会一直出现。
路由规划:用二级路由承载导航
为什么需要:首页、新闻、商品、个人中心都有导航,而登录、404 没有导航。用二级路由可以让“导航容器”只包住需要导航的页面。
怎么想:先让 / 对应 Index,再把 Home、GoodsList、NewsList、My 作为 Index 的 children;Login 和 NotFound 与 Index 同级。
怎么做:src/router/index.js 配置如下:
js
import Vue from "vue";
import VueRouter from "vue-router";
import Login from "@/views/Login";
import NotFound from "@/views/NotFound";
import Home from "@/views/Home";
import Index from "@/views/Index";
import GoodsList from "@/views/GoodsList";
import NewsList from "@/views/NewsList";
import My from "@/views/My";
Vue.use(VueRouter);
const routes = [
{
path:"/",
component:Index,
children:[
{
path:"/",
component:Home
},{
path:"/goodsList",
component: GoodsList
},{
path:"/newsList",
component: NewsList
},{
path:"/my",
component: My
}
]
},
{
path:"/login",
name:"login",
component:Login
},{
path:"*",
component: NotFound
}
];
export default new VueRouter({
mode:"history",
routes
})易错点:mode:"history" 本地开发可用,部署时要服务端回退到 index.html;组件导入注意大小写;子路由的 router-view 必须写在 Index 里。
导航容器 Index.vue:导航和子路由出口
为什么需要:有导航的页面需要共用同一套导航,点击后只切换下方内容。
怎么想:在 Index 中写 nav 和 router-view,router-link 指向各子路由。
怎么做:
vue
<template>
<div>
<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>
<script>
export default {
name: "Index"
}
</script>
<style lang="less" scoped>
nav{
a{
color:skyblue;
padding:5px;
}
a.router-link-active{
color:red;
}
}
</style>易错点:首页要加 exact,否则 /newsList 等也会让首页处于激活态;router-link-active 是模糊匹配,精确匹配用 exact。
登录页 Login.vue:回车登录并写入本地存储
为什么需要:个人中心需要判断“是否登录”,登录成功后要保存用户标识。
怎么想:用固定数组模拟用户,回车时取输入值,校验非空和是否存在;成功写入 localStorage,再编程式跳转 /my。
怎么做:
vue
<template>
<div>
<h3>登陆界面</h3>
<input @keyup.enter="loginHandler" type="text" placeholder="请输入账号,回车登陆">
</div>
</template>
<script>
export default {
name: "Login",
data(){
return {
userList:["zhangsan","lisi","wangwu","zhaoliu","yanqi","shenba"]
}
},
methods:{
loginHandler(e){
const userName = e.target.value.trim();
if(userName.length === 0){
alert("请输入用户名!");
return;
}
if(this.userList.includes(userName)){
localStorage.setItem("userName",userName);
this.$router.push({
path:"/my"
})
}else{
alert("用户名错误!")
}
}
}
}
</script>
<style scoped>
</style>易错点:输入值要 trim();push 的路径建议写 /my;固定账号只是演示,不是真实登录。
个人中心 My.vue:登录校验与退出
为什么需要:未登录不能进入个人中心;退出后要清空登录状态并回到登录页。
怎么想:进入前读 localStorage,没有就跳 /login;页面显示用户名;退出时 clear() 再跳转。
怎么做:
vue
<template>
<div>
<h3>欢迎<span :style="{color:'red'}">{{userName}}</span>的到来</h3>
<button @click="outLogin">退出登陆</button>
</div>
</template>
<script>
export default {
name: "My",
data(){
const userName = localStorage.getItem("userName");
return {
userName
}
},
methods:{
outLogin(){
localStorage.clear();
this.$router.push({
path:"login"
});
}
},
beforeCreate() {
if(!localStorage.getItem("userName")){
// 编程式导航
this.$router.push("/login");
}
},
created(){
console.log("created",this.a);
},
mounted(){
console.log("mounted")
}
}
</script>
<style scoped>
</style>易错点:beforeCreate 里跳转后,后面的 created、mounted 仍可能继续执行,转写稿也强调这不是最佳方案;mounted 中校验太晚;模板里不能直接写 localStorage,要在 data 中先取;this.a 未定义只是调试残留;退出跳转建议写 /login。
根组件 App.vue:一级路由出口
为什么需要:App.vue 是一级路由出口,Login、Index、NotFound 都在这里切换。
怎么想:只放 router-view,再加统一容器样式。
怎么做:
vue
<template>
<div class="container">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "App"
}
</script>
<style scoped>
.container{
text-align: center;
}
</style>易错点:不要把导航写进 App.vue,否则登录页和 404 也会有导航;一级出口和二级出口不要混淆。
转写稿里的调试推导:后续可用路由守卫优化
为什么需要:beforeCreate 校验虽然能跳转,但不能真正“拦住”组件后续流程。转写稿中通过打印 beforeCreate、created、mounted 观察到执行顺序,并尝试用路由守卫做权限拦截。
怎么想:进入路由前判断 localStorage.getItem("userName"),有值 next() 放行,无值 next("/login") 跳转;路由参数变化时用 beforeRouteUpdate,离开页面时用 beforeRouteLeave。
怎么做:转写稿演示了 beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave 以及路由独享守卫 beforeEnter;其中 beforeRouteEnter 中 this 不是组件实例,可用 next(vm => {}) 拿到实例;beforeRouteUpdate 中取新参数应看 to.query,不要用旧实例上的 this.$route。
易错点:next 不调用就不放行;beforeEnter 是写在路由配置里的独享守卫,不是组件内守卫;本节代码仍保留 beforeCreate 写法,学习时要知道它的局限。
路由守卫是什么
路由守卫又称路由拦截,职责是对路由进行权限设置。转写稿里反复强调:不是“页面写出来后随便点”,而是要在进入、复用、离开路由时决定“能不能进、要不要跳、是否放行”。本节以登录后进入个人中心为例:未登录点个人中心应跳登录页,登录成功再进入。
为什么需要:只在组件 mounted 或 beforeCreate 里判断,组件已经开始创建甚至挂载,后面的请求、渲染可能已经发生,权限判断不够早。
怎么想:把判断放到“路由进入之前”,用 next() 控制放行,用 next('/login') 或 next({ path: '/login' }) 跳转。
怎么做:组件内守卫常用三个:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave;路由配置里还有独享守卫 beforeEnter。
易错点:beforeRouteEnter 里不能访问 this,因为组件实例还没创建;需要实例时用 next(vm => { ... }),它在 beforeMount 与 mounted 之间执行。
beforeRouteEnter:进入前做登录拦截
转写稿中的调试过程很关键:先在 mounted 判断不合适,因为组件已挂载;在 beforeCreate、created 里判断也不理想,因为即使跳走了,后续钩子仍可能继续执行。最后把登录判断放进 beforeRouteEnter,在进入个人中心前拦截。
js
export default {
name: 'My',
beforeRouteEnter(to, from, next) {
const userName = localStorage.getItem('userName')
if (userName) {
next()
} else {
next({ path: '/login' })
}
},
mounted() {
// 只有放行后才会走到这里
}
}为什么需要:未登录不能进入个人中心,避免先渲染再跳转。
怎么想:to 是即将进入的路由,from 是来源路由,next 决定是否放行。
怎么做:有登录标记 next();没有则 next('/login')。
易错点:不要写成 this.$router,beforeRouteEnter 中 this 是 undefined;要操作实例用 next(vm => { vm.xxx = ... })。转写稿还验证了:不调用 next() 或 next(false) 都会拦下。
beforeRouteUpdate:同一组件参数变化
新闻列表点标题一、标题二,地址从 /my?id=1 变成 /my?id=2,组件被复用,mounted 不会再次执行。转写稿调试发现:此时 this.$route.query.id 仍是旧值,比较和取参要用 to。
js
beforeRouteUpdate(to, from, next) {
if (to.query.id !== from.query.id) {
this.getInfoById(to.query.id)
}
next()
}为什么需要:复用组件时参数变了,但组件不重新创建,需要重新请求详情。
怎么想:监听“路由参数变化”,不是监听组件挂载。
怎么做:比较 to.query.id 与 from.query.id,不同就重新取数据,最后 next() 放行。
易错点:用 this.$route.query.id 可能拿到旧路由;next 不支持 next(vm => {}) 这种拿实例的写法。
beforeRouteLeave:离开前确认
转写稿用“您确定要离开个人中心吗”演示离开守卫:点取消留在当前页,点确定才走。
js
beforeRouteLeave(to, from, next) {
const ok = window.confirm('您确定要离开个人中心吗?')
ok ? next() : next(false)
}为什么需要:防止用户误离开,丢失未提交内容。
怎么想:离开前弹确认,拒绝就不放行。
怎么做:next() 放行,next(false) 取消。
易错点:to 是准备去的路由,from 是当前要离开的路由;不要把它和进入守卫混淆。
beforeEnter:路由独享守卫
独享守卫写在路由配置对象里,不写在组件里,只对当前这条路由生效。转写稿验证:它在组件内部守卫之前执行,且不支持 next(vm => {})。
js
{
path: '/my',
component: My,
beforeEnter(to, from, next) {
const userName = localStorage.getItem('userName')
userName ? next() : next('/login')
}
}为什么需要:某些路由单独做权限,不必污染组件代码。
怎么想:权限属于这条路由,就写在路由配置里。
怎么做:在 beforeEnter 中判断并 next() 或跳转。
易错点:独享守卫拿不到组件实例;若要统一做登录判断,也可用全局守卫 router.beforeEach,但本节重点是组件内守卫与独享守卫的时机差异。
路由表:先规划“谁有导航、谁是详情页”
为什么需要:综合案例里同时有首页、关于页、正在热映、即将上映和电影详情。若把所有页面都写成一级路由,详情页和列表页会平铺在 App.vue 的同一个出口里,层级和导航关系会乱。
怎么想:先把“有二级导航”的页面归到 Home 下,再把“即将上映”的详情页作为 comingSoon 的子路由。/film 默认重定向到 /film/comingSoon。
怎么做:在 src/router/index.js 中配置嵌套路由,并使用 history 模式。
易错点:子路由 path 写绝对路径时要和父级匹配;redirect 要指向真实存在的地址;component 不要误写成 components。
js
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "@/views/Home";
import About from "@/views/About";
import NowPlaying from "@/views/NowPlaying";
import ComingSoon from "@/views/ComingSoon";
import Details from "@/views/Details";
Vue.use(VueRouter);
const routes = [
{
path:"/film",
component:Home,
children:[
{
path:"/film",
redirect:"/film/comingSoon"
},
{
path:"/film/nowPlaying",
component:NowPlaying
},
{
path:"/film/comingSoon",
component:ComingSoon,
children:[
{
path:":id",
component:Details
}
]
}
]
},{
path:"/About",
component:About
},{
path:"/",
redirect:"/film/comingSoon"
}
];
const router = new VueRouter({
routes,
mode:"history"
});
export default router;App 骨架:一级出口与导航
为什么需要:App.vue 是根组件,一级路由必须在这里留 <router-view>,否则页面组件没有渲染位置。
怎么想:左侧放 About 和“电影”的导航,右侧放一级路由出口;电影内部的二级导航由 Home.vue 自己负责。
怎么做:用 router-link 的 active-class="active" 控制选中样式。
易错点:to="/about" 要和路由表大小写、路径一致;router-view 不要漏写。
vue
<template>
<div>
<div class="row">
<div class="col-xs-offset-2 col-xs-8">
<div class="page-header">
<h2>Vue Router Demo</h2>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-2 col-xs-offset-2">
<div class="list-group">
<router-link active-class="active" class="list-group-item" to="/about">About</router-link>
<router-link active-class="active" class="list-group-item" to="/film" >电影</router-link>
</div>
</div>
<div class="col-xs-6">
<div class="panel">
<div class="panel-body">
<router-view></router-view>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "App"
}
</script>
<style scoped>
</style>Home 二级导航:二级路由出口不能忘
为什么需要:/film 下有“正在热映”和“即将上映”,它们共用 Home 的头部和导航,只切换下方内容。
怎么想:把这两个页面作为 Home 的子路由,Home 内部再放一个 <router-view>。转写稿里调试时出现过“二级路由配了但页面不显示”,原因就是一级组件里没有写二级出口。
怎么做:Home.vue 中用两个 router-link 指向 /film/nowPlaying、/film/comingSoon,下方放 <router-view>。
易错点:子路由组件必须渲染在父组件的 <router-view> 中;active-class 要写在 router-link 上。
vue
<template>
<div>
<h2>卖座电影内容</h2>
<div>
<ul class="nav nav-tabs">
<li><router-link to="/film/nowPlaying" active-class="active" class="list-group-item">正在热映</router-link></li>
<li><router-link to="/film/comingSoon" active-class="active" class="list-group-item">即将上映</router-link></li>
</ul>
<div>
<router-view></router-view>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Home"
}
</script>
<style scoped>
</style>Details 详情页:动态参数取 id 并请求接口
为什么需要:详情页地址形如 /film/comingSoon/1,需要从路由参数里取 id,再请求电影详情。
怎么想:mounted 中通过 this.$route.params.id 获取参数,调用 getFilmById;请求期间用 isLoading 显示“正在加载中”。
怎么做:用 axios.get 拼 filmId,按接口要求带 X-Client-Info、X-Host 请求头。
易错点:this.$route.params 解构时注意 id 名称与路由 :id 一致;请求成功后要关闭加载状态。
vue
<template>
<div>
<h3 v-if="isLoading">正在加载中.....</h3>
<template v-else>
<h3>{{ film.name }}</h3>
<p>{{ film.synopsis }}</p>
</template>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "Details",
data() {
return {
film: {},
isLoading:false
}
},
methods: {
getFilmById(id) {
this.isLoading = true;
axios.get("https://m.maizuo.com/gateway?filmId=" + id + "&k=5447241", {
headers: {
"X-Client-Info": '{"a":"3000","ch":"1002","v":"5.2.1","e":"16905280922820767016288257","bc":"310100"}',
"X-Host": "mall.film-ticket.film.info"
}
}).then(value => {
this.film = value.data.data.film;
this.isLoading = false;
})
}
},
mounted() {
const {id} = this.$route.params;
this.getFilmById(id);
},
beforeRouteUpdate(to, from, next) {
console.log("beforeRouteUpdate")
this.getFilmById(to.params.id);
next();
}
}
</script>
<style scoped>
</style>动态参数切换:为什么需要 beforeRouteUpdate
为什么需要:从详情 /1 点到详情 /2 时,组件被复用,mounted 不会再次执行,页面可能还停在旧电影。
怎么想:转写稿调试时发现“地址变了、请求没发”,就要用组件内守卫 beforeRouteUpdate。它在当前路由复用、参数更新时触发。
怎么做:在 beforeRouteUpdate(to, from, next) 中用 to.params.id 重新请求,然后 next() 放行。
易错点:不要用 this.$route.params.id,此时它可能仍是旧路由;要传 to.params.id。若只想在 id 真正变化时请求,可比较 to.params.id !== from.params.id,避免重复请求。
课堂画面






代码示例
示例1:嵌套路由 + 登录 + 个人中心(15-11)
js
// src/router/index.js
import Vue from "vue";
import VueRouter from "vue-router";
import Login from "@/views/Login";
import NotFound from "@/views/NotFound";
import Home from "@/views/Home";
import Index from "@/views/Index";
import GoodsList from "@/views/GoodsList";
import NewsList from "@/views/NewsList";
import My from "@/views/My";
Vue.use(VueRouter);
const routes = [
{
path: "/",
component: Index,
children: [
{
path: "/",
component: Home
},
{
path: "/goodsList",
component: GoodsList
},
{
path: "/newsList",
component: NewsList
},
{
path: "/my",
component: My
}
]
},
{
path: "/login",
name: "login",
component: Login
},
{
path: "*",
component: NotFound
}
];
export default new VueRouter({
mode: "history",
routes
});vue
<!-- src/App.vue -->
<template>
<div class="container">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "App"
}
</script>
<style scoped>
.container {
text-align: center;
}
</style>vue
<!-- src/views/Index.vue -->
<template>
<div>
<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>
<script>
export default {
name: "Index"
}
</script>
<style lang="less" scoped>
nav {
a {
color: skyblue;
padding: 5px;
}
a.router-link-active {
color: red;
}
}
</style>vue
<!-- src/views/Login.vue -->
<template>
<div>
<h3>登陆界面</h3>
<input @keyup.enter="loginHandler" type="text" placeholder="请输入账号,回车登陆">
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
userList: ["zhangsan", "lisi", "wangwu", "zhaoliu", "yanqi", "shenba"]
}
},
methods: {
loginHandler(e) {
const userName = e.target.value.trim();
if (userName.length === 0) {
alert("请输入用户名!");
return;
}
if (this.userList.includes(userName)) {
localStorage.setItem("userName", userName);
this.$router.push({
path: "/my"
})
} else {
alert("用户名错误!")
}
}
}
}
</script>
<style scoped>
</style>vue
<!-- src/views/My.vue -->
<template>
<div>
<h3>欢迎<span :style="{ color: 'red' }">{{ userName }}</span>的到来</h3>
<button @click="outLogin">退出登陆</button>
</div>
</template>
<script>
export default {
name: "My",
data() {
const userName = localStorage.getItem("userName");
return {
userName
}
},
methods: {
outLogin() {
localStorage.clear();
this.$router.push({
path: "login"
});
}
},
beforeCreate() {
if (!localStorage.getItem("userName")) {
// 编程式导航
this.$router.push("/login");
}
},
created() {
console.log("created", this.a);
},
mounted() {
console.log("mounted")
}
}
</script>
<style scoped>
</style>要点解释与易错点:
Index.vue作为有导航的一级路由组件,子路由必须在该组件内写<router-view></router-view>,否则首页、新闻、商品、个人中心不会渲染。- 首页链接要加
exact,否则/会匹配所有子路由,导致首页高亮错误。 Login.vue登录成功后先localStorage.setItem("userName", userName),再this.$router.push({ path: "/my" }),否则My.vue的登录判断会失败。My.vue在beforeCreate中直接读localStorage,因为此时data还未初始化;若在mounted中判断,组件已挂载,后面的钩子仍会执行,不是最佳拦截方案。NotFound、Home、GoodsList、NewsList需按笔记同名视图文件创建,保证import路径存在。
示例2:综合案例 + beforeRouteUpdate 处理详情复用(15-13)
js
// src/router/index.js
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "@/views/Home";
import About from "@/views/About";
import NowPlaying from "@/views/NowPlaying";
import ComingSoon from "@/views/ComingSoon";
import Details from "@/views/Details";
Vue.use(VueRouter);
const routes = [
{
path: "/film",
component: Home,
children: [
{
path: "/film",
redirect: "/film/comingSoon"
},
{
path: "/film/nowPlaying",
component: NowPlaying
},
{
path: "/film/comingSoon",
component: ComingSoon,
children: [
{
path: ":id",
component: Details
}
]
}
]
},
{
path: "/About",
component: About
},
{
path: "/",
redirect: "/film/comingSoon"
}
];
const router = new VueRouter({
routes,
mode: "history"
});
export default router;vue
<!-- src/App.vue -->
<template>
<div>
<div class="row">
<div class="col-xs-offset-2 col-xs-8">
<div class="page-header">
<h2>Vue Router Demo</h2>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-2 col-xs-offset-2">
<div class="list-group">
<router-link active-class="active" class="list-group-item" to="/about">About</router-link>
<router-link active-class="active" class="list-group-item" to="/film">电影</router-link>
</div>
</div>
<div class="col-xs-6">
<div class="panel">
<div class="panel-body">
<router-view></router-view>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "App"
}
</script>
<style scoped>
</style>vue
<!-- src/views/Home.vue -->
<template>
<div>
<h2>卖座电影内容</h2>
<div>
<ul class="nav nav-tabs">
<li>
<router-link to="/film/nowPlaying" active-class="active" class="list-group-item">正在热映</router-link>
</li>
<li>
<router-link to="/film/comingSoon" active-class="active" class="list-group-item">即将上映</router-link>
</li>
</ul>
<div>
<router-view></router-view>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Home"
}
</script>
<style scoped>
</style>vue
<!-- src/views/Details.vue -->
<template>
<div>
<h3 v-if="isLoading">正在加载中.....</h3>
<template v-else>
<h3>{{ film.name }}</h3>
<p>{{ film.synopsis }}</p>
</template>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "Details",
data() {
return {
film: {},
isLoading: false
}
},
methods: {
getFilmById(id) {
this.isLoading = true;
axios.get("https://m.maizuo.com/gateway?filmId=" + id + "&k=5447241", {
headers: {
"X-Client-Info": '{"a":"3000","ch":"1002","v":"5.2.1","e":"16905280922820767016288257","bc":"310100"}',
"X-Host": "mall.film-ticket.film.info"
}
}).then(value => {
this.film = value.data.data.film;
this.isLoading = false;
})
}
},
mounted() {
const { id } = this.$route.params;
this.getFilmById(id);
},
beforeRouteUpdate(to, from, next) {
console.log("beforeRouteUpdate")
this.getFilmById(to.params.id);
next();
}
}
</script>
<style scoped>
</style>要点解释与易错点:
/film默认重定向到/film/comingSoon,所以Home.vue内要有<router-view>渲染二级路由。Details.vue是复用组件:从/film/comingSoon/1切到/film/comingSoon/2时,mounted不会再次执行,必须用beforeRouteUpdate重新请求。beforeRouteUpdate中要用to.params.id获取最新参数,不能用this.$route.params.id,因为守卫执行时当前实例上的路由信息还未更新。beforeRouteUpdate必须调用next()放行,否则路由会停在旧状态。- 路由独享守卫和组件内守卫的执行顺序:路由独享守卫在组件内守卫之前执行;
beforeRouteEnter的next(vm => {})可拿到组件实例,而beforeRouteUpdate/beforeRouteLeave的next不支持这种拿实例的写法。
常见错误
现象:访问
/login或 404 页面时仍然显示导航。
原因:登录页、404 与有导航页面都作为一级路由,导航写在App.vue或公共一级组件中。
修法:新建Index.vue承载导航和<router-view>,把Home/NewsList/GoodsList/My作为Index的子路由;Login/404与Index同级。现象:子路由地址已变化,但页面内容不显示。
原因:父路由组件Index.vue中只写了导航,没有写子路由出口<router-view>。
修法:在Index.vue导航下方补充<router-view></router-view>。现象:首页导航一直高亮,进入
/newsList后首页仍激活。
原因:<router-link to="/">未加exact,根路径会匹配所有子路径;或反过来给所有链接滥用exact导致子路由不高亮。
修法:只给需要根路径精确匹配的首页链接加exact,其他链接按需单独处理,不要全局滥用。现象:未登录仍能进入
/my,且My.vue的created/mounted或接口请求照常执行。
原因:在beforeCreate中push("/login")不能阻止组件后续钩子执行;组件内生命周期不是路由守卫。
修法:改用beforeRouteEnter、路由独享beforeEnter或全局守卫;未登录时next("/login"),不要执行放行next()。现象:
beforeRouteEnter中写this.userName报错或拿不到实例。
原因:该守卫执行时组件实例尚未创建。
修法:使用to/from/next;需要实例时用next(vm => { ... }),不要在守卫顶层直接访问this。现象:同一详情组件从
/details/1切到/details/2,页面仍显示旧数据。
原因:组件被复用,mounted不会重新执行;在beforeRouteUpdate中取this.$route.params.id可能仍是旧值。
修法:在beforeRouteUpdate(to, from, next)中用to.params.id或to.query.id重新请求,比较to/from参数避免重复请求,最后next()。现象:点击其他导航无法离开当前页。
原因:beforeRouteLeave中忘记调用next(),或错误调用next(false)。
修法:确认离开用next(),取消离开用next(false)。现象:
beforeEnter中写next(vm => { ... })不执行。
原因:路由独享守卫不绑定组件实例,不支持通过next回调获取vm。
修法:独享守卫中用next()或next("/login");需要实例逻辑放到组件内守卫中。
练习
按需求设计路由结构:
/、/newsList、/goodsList、/my有导航;/login、404 无导航。写出路由表层级,并标明每个<router-view>应放在哪个组件中。实现“未登录访问
/my跳转/login,登录成功后回到/my”。要求:未登录时My.vue的mounted和接口请求不能执行。请用beforeRouteEnter或beforeEnter写出关键代码,并说明next的调用方式。商品列表点击商品进入详情页。要求从
/goods/1切换到/goods/2时详情内容更新且不刷新整页。请用beforeRouteUpdate完成重新请求,并说明为什么不能只依赖mounted和this.$route.params.id。给“离开个人中心”增加确认提示:用户确认后离开,取消则留在当前页。写出
beforeRouteLeave中确认、取消两种分支的next调用。修复导航高亮问题:首页链接始终高亮、新闻子路由链接不高亮。给出
exact的使用方案,并说明为什么不能给所有链接都加exact。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 搭建路由 | 23:51 |
| 2 | 完成需求 | 20:11 |
| 3 | beforeRouteEnter | 22:35 |
| 4 | beforeRouteUpdate | 34:05 |
| 5 | beforeRouteLeave | 06:26 |
| 6 | 路由独享守卫 | 09:40 |
小结
- 先按“是否有导航”分层路由,再写页面,比后期用
v-if硬切导航更稳定。 - 权限判断应放在路由守卫中,不要放在
created/mounted;组件生命周期不能阻止组件继续创建。 beforeRouteEnter中没有this,需要实例时用next(vm => { ... })。- 同一组件仅参数变化不会触发
mounted,应使用beforeRouteUpdate,并从to中取新参数。 - 离开守卫必须显式调用
next();取消离开用next(false)。 - 路由独享
beforeEnter适合单路由权限控制,但不支持next(vm => ...)。 exact只给需要精确匹配的链接使用,尤其是根路径/,不要全局滥用。- 编程式导航优先用绝对路径或命名路由;
history模式部署时还需要服务端 fallback 配置。