Skip to content

需求与路由守卫(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

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例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 不支持这种拿实例的写法。

常见错误 ​

  1. 现象:访问 /login 或 404 页面时仍然显示导航。
    原因:登录页、404 与有导航页面都作为一级路由,导航写在 App.vue 或公共一级组件中。
    修法:新建 Index.vue 承载导航和 <router-view>,把 Home/NewsList/GoodsList/My 作为 Index 的子路由;Login/404 与 Index 同级。

  2. 现象:子路由地址已变化,但页面内容不显示。
    原因:父路由组件 Index.vue 中只写了导航,没有写子路由出口 <router-view>。
    修法:在 Index.vue 导航下方补充 <router-view></router-view>。

  3. 现象:首页导航一直高亮,进入 /newsList 后首页仍激活。
    原因:<router-link to="/"> 未加 exact,根路径会匹配所有子路径;或反过来给所有链接滥用 exact 导致子路由不高亮。
    修法:只给需要根路径精确匹配的首页链接加 exact,其他链接按需单独处理,不要全局滥用。

  4. 现象:未登录仍能进入 /my,且 My.vue 的 created/mounted 或接口请求照常执行。
    原因:在 beforeCreate 中 push("/login") 不能阻止组件后续钩子执行;组件内生命周期不是路由守卫。
    修法:改用 beforeRouteEnter、路由独享 beforeEnter 或全局守卫;未登录时 next("/login"),不要执行放行 next()。

  5. 现象:beforeRouteEnter 中写 this.userName 报错或拿不到实例。
    原因:该守卫执行时组件实例尚未创建。
    修法:使用 to/from/next;需要实例时用 next(vm => { ... }),不要在守卫顶层直接访问 this。

  6. 现象:同一详情组件从 /details/1 切到 /details/2,页面仍显示旧数据。
    原因:组件被复用,mounted 不会重新执行;在 beforeRouteUpdate 中取 this.$route.params.id 可能仍是旧值。
    修法:在 beforeRouteUpdate(to, from, next) 中用 to.params.id 或 to.query.id 重新请求,比较 to/from 参数避免重复请求,最后 next()。

  7. 现象:点击其他导航无法离开当前页。
    原因:beforeRouteLeave 中忘记调用 next(),或错误调用 next(false)。
    修法:确认离开用 next(),取消离开用 next(false)。

  8. 现象:beforeEnter 中写 next(vm => { ... }) 不执行。
    原因:路由独享守卫不绑定组件实例,不支持通过 next 回调获取 vm。
    修法:独享守卫中用 next() 或 next("/login");需要实例逻辑放到组件内守卫中。

练习 ​

  1. 按需求设计路由结构:/、/newsList、/goodsList、/my 有导航;/login、404 无导航。写出路由表层级,并标明每个 <router-view> 应放在哪个组件中。

  2. 实现“未登录访问 /my 跳转 /login,登录成功后回到 /my”。要求:未登录时 My.vue 的 mounted 和接口请求不能执行。请用 beforeRouteEnter 或 beforeEnter 写出关键代码,并说明 next 的调用方式。

  3. 商品列表点击商品进入详情页。要求从 /goods/1 切换到 /goods/2 时详情内容更新且不刷新整页。请用 beforeRouteUpdate 完成重新请求,并说明为什么不能只依赖 mounted 和 this.$route.params.id。

  4. 给“离开个人中心”增加确认提示:用户确认后离开,取消则留在当前页。写出 beforeRouteLeave 中确认、取消两种分支的 next 调用。

  5. 修复导航高亮问题:首页链接始终高亮、新闻子路由链接不高亮。给出 exact 的使用方案,并说明为什么不能给所有链接都加 exact。

原视频对照 ​

#视频时长
1搭建路由23:51
2完成需求20:11
3beforeRouteEnter22:35
4beforeRouteUpdate34:05
5beforeRouteLeave06:26
6路由独享守卫09:40

小结 ​

  1. 先按“是否有导航”分层路由,再写页面,比后期用 v-if 硬切导航更稳定。
  2. 权限判断应放在路由守卫中,不要放在 created/mounted;组件生命周期不能阻止组件继续创建。
  3. beforeRouteEnter 中没有 this,需要实例时用 next(vm => { ... })。
  4. 同一组件仅参数变化不会触发 mounted,应使用 beforeRouteUpdate,并从 to 中取新参数。
  5. 离开守卫必须显式调用 next();取消离开用 next(false)。
  6. 路由独享 beforeEnter 适合单路由权限控制,但不支持 next(vm => ...)。
  7. exact 只给需要精确匹配的链接使用,尤其是根路径 /,不要全局滥用。
  8. 编程式导航优先用绝对路径或命名路由;history 模式部署时还需要服务端 fallback 配置。