一、路由

路由的本质就是一种对应关系,根据不同的URL请求,返回对应不同的资源。那么url地址和真实的资源之间就有一种对应的关系,就是路由。路由分为:后端路由和前端路由。

1.1 路由分为两大类:

前端路由:Hash地址与组件之间的对应关系。
  • SPA与前端路由之间的关系:SPA指的是一个web网站只有唯一的一个HTML页面,所有组件的展示与切换都在这唯一一个页面内完成。此时不同组件之间的切换,要依赖于前端路由来完成。
后端路由:请求方式、请求地址与function处理函数之间的对应关系。

前端路由的工作方式:

  • 1、用户点击了页面上的路由链接
  • 2、导致了URL地址栏中的Hash值发生了变化
  • 3、前端路由监听到了Hash地址的变化
  • 4、前端路由把当前Hash地址对应的组件渲染到浏览器中。

image.png

二、vue-router 的基本使用

vue-route是vue.js官方给出路由解决方案,只能结合vue项目使用,能够轻松管理SPA项目中的组件切换。

注意版本:vue-router 3.x只能结合vue2使用,vue-router 4.x只能结合vue3使用!

  • 1、安装 vue-router
npm i vue-router@next -S
  • 2、定义路由组件:在项目中定义所需的组件,将来要用vue-router控制它们的展示与切换。
  • 3、声明路由链接和占位符:使用<router-link>标签声明路由链接,使用<router-view>标签声明路由占位符。
<!-- App.vue -->
<template>
  <div>
    vue-router 的基本使用
    <!-- 声明路由链接 -->
    <router-link to="/home">首页</router-link>&nbsp;
    <router-link to="/movie">电影</router-link>&nbsp;
    <router-link to="/about">关于</router-link>
    <hr />
    <!-- 路由的占位符 -->
    <router-view></router-view>
  </div>
</template>
 
<script>
export default {
  name: 'MyApp',
}
</script>
  • 4、创建路由模块:在项目中创建router.js路由模块,在其中按如下步骤创建并得到路由对象:
    • 从vue-router中按需导入两个方法
    • 导入需要使用路由控制的组件
    • 创建路由实例对象
    • 向外共享路由实例对象
    • 在main.js中导入并挂载路由模块
// router.js
// 1、从vue-router按需导入两个方法
// createRouter方法用于创建路由实例对象
// createWebHashHistory方法用于指定路由的工作模式(hash模式)
import { createRouter, createWebHashHistory } from 'vue-router'
// 2、导入需要使用路由控制的组件
import Home from './MyHome.vue'
import Movie from './MyMovie.vue'
import About from './MyAbout.vue'
 
// 3、创建路由对象
const router = createRouter({
  // 通过history属性,指定路由的工作模式
  history: createWebHashHistory(),
  // 自定义路由高亮的 class 类名,会覆盖掉默认的router-link-active类名
  linkActiveClass: 'active-router',
  // 通过routes数组,声明路由的匹配规则。
  // path是hash地址,component是要展示的组件,redirct是要重定向到的新地址
  routes: [
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home },
    { path: '/movie', component: Movie },
    { path: '/about', component: About },
  ],
})
 
// 4、导出路由对象
export default router
// main.js
import { createApp } from 'vue'
import App from './components/start/App.vue'
// 5、在main.js中导入并挂载路由模块
import router from './components/start/router'
import './assets/css/bootstrap.css'
import './index.css'
 
const app = createApp(App)
// 挂载路由模块
app.use(router)
app.mount('#app')
  • 5、路由重定向 路由重定向:用户在访问地址A的时候,强制用户跳转到地址C,从而展示特定的组件页面。

  • 通过路由规则的redirect属性,指定一个新的路由地址。

  • 6、路由高亮 将激活的路由链接进行高亮显示:

    • ① 使用默认的高亮class类:被激活的路由链接,默认会应用一个叫做router-link-active的类名,开发者可以使用此类名选择器,在index.css文件中为激活的路由链接设置高亮样式。
    • ② 自定义路由高亮的class类:在创建路由的实例对象时,开发者可以基于linkActiveClass属性,自定义路由链接被激活时所应用的类名。
/* index.css */
.router-link-active {
  background-color: red;
  color: white;
  font-weight: bold;
}
 
.active-router {
  background-color: blue;
  color: white;
  font-weight: bold;
}

三、vue-router 的高级用法

2.1 嵌套路由

嵌套路由:通过路由实现组件的嵌套展示。

  • 声明子路由链接和子路由占位符
  • 在父路由规则中,通过children属性嵌套声明子路由规则
<!-- MyAbout.vue -->
<template>
  <div>
    <h3>MyAbout 组件</h3>
    <!-- 声明子路由链接 -->
    <router-link to="/about/tab1">Tab1</router-link>&nbsp;
    <router-link to="/about/tab2">Tab2</router-link>
    <hr>
    <!-- 声明子路由占位符 -->
    <router-view></router-view>
  </div>
</template>
 
<script>
export default {
  name: 'MyAbout',
}
</script>
// router.js
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from './MyHome.vue'
import Movie from './MyMovie.vue'
import About from './MyAbout.vue'
import Tab1 from './tabs/MyTab1.vue'
import Tab2 from './tabs/MyTab2.vue'
 
const router = createRouter({
  history: createWebHashHistory(),
  linkActiveClass: 'active-router',
  routes: [
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home },
    { path: '/movie', component: Movie },
    {
      path: '/about',
      component: About,
      // 嵌套路由的重定向
      redirect: '/about/tab1',
      // 通过 children 属性嵌套声明子级路由规则
      // 注意:子路由的path不要以/开头
      children: [
        { path: 'tab1', component: Tab1 },
        { path: 'tab2', component: Tab2 },
      ],
    },
  ],
})
 
export default router

3.2 动态路由匹配

动态路由:把Hash地址中可变的部分定义为参数项,从而提高路由规则的复用性。

  • 在vue-router中使用英文的冒号(:)来定义路由的参数项。
    • ① 通过动态路由匹配的方式渲染出来的组件中,可以使用$route.params对象访问到动态匹配的参数值。【记住路由规则里的参数名称,因为必须用 $route.params.参数名称 来获取参数值】
    • ② 为了简化路由参数的获取方式,vue-rouetr允许在路由规则中开启props传参。
// router.js 关键部分代码
// 动态路由匹配
// 声明props:true选项,即可在MyMovie组件中,以props形式接收到路由规则匹配到的参数项
{ path: '/movie/:mid', component: Movie, props: true },
<!--App.vue 关键部分代码-->    
<router-link to="/movie/1">电影1</router-link>&nbsp;
<router-link to="/movie/2">电影2</router-link>&nbsp;
<router-link to="/movie/3">电影3</router-link>&nbsp;
<!-- MyMovie.vue -->
<template>
  <div>
    <!-- 开启props传参后,可以直接使用props接收路由参数 -->
    <h3>MyMovie 组件 --- {{ $route.params.mid }} --- {{ mid }}</h3>
    <button type="button" class="btn btn-danger" @click="goBack">后退</button>
  </div>
</template>
 
<script>
export default {
  name: 'MyMovie',
  props: ['mid'], // 使用props接收路由规则中匹配到的参数项
  methods: {
    goBack() {
      this.$router.go(-1) // 后退到之前的组件按钮
    },
  },
}
</script>

三、编程式导航

3.1 编程式导航 vs 声明式导航

  • 编程式导航:通过调用API实现导航的方式。
    • 在普通网页中调用location.href跳转到新页面的方式属于编程式导航。
  • 声明式导航:通过点击链接实现导航的方式。
    • 在普通网页中点击<a>链接、vue项目中点击<router-link>都属于声明式导航。

3.2 vue-router中的编程式导航API

  • 跳转到指定Hash地址,从而展示对应组件:this.$router.push('hash地址')
  • 实现导航历史的前进后退:this.$router.go(整数值)
<!-- MyHome.vue -->
<template>
  <div>
    <h3>MyHome 组件</h3>
    <button type="button" class="btn btn-primary" @click="goToMovie(3)">导航到Movie页面</button>
  </div>
</template>
 
<script>
export default {
  name: 'MyHome',
  methods: {
    goToMovie(id) {
      this.$router.push('/movie/' + id) // 跳转到指定Hash地址
    },
  },
}
</script>

四、命名路由

命名路由:通过name属性为路由规则定义名称。

  • 注意:命名路由的name值不能重复,必须保证唯一性!
  • 使用命名路由实现声明式导航:为<router-link>标签动态绑定to属性的值,并通过name属性指定要跳转到的路由规则。期间还可以使用params属性指定跳转期间要携带的路由参数。
  • 使用命名路由实现编程式导航:调用push函数期间制定一个配置对象,name是要跳转到的路由规则,params是携带的路由参数。
// router.js 关键部分代码
// 命名路由
{ name: 'mov', path: '/movie/:mid', component: Movie, props: true },
<!--MyHome.vue-->
<template>
  <div>
    <h3>MyHome 组件</h3>
    <router-link :to="{ name: 'mov', params: { mid: 2 } }">go to movie</router-link>
    <button type="button" class="btn btn-primary" @click="goToMovie(1)">go to movie</button>
  </div>
</template>
 
<script>
export default {
  name: 'MyHome',
  methods: {
    goToMovie(id) {
      this.$router.push({
        name: 'mov',
        params: {
          mid: id,
        },
      })
    },
  },
}
</script>

五、导航守卫

导航守卫可以控制路由的访问权限。

5.1 声明全局导航守卫

全局导航守卫会拦截每个路由规则,从而对每个路由进行访问权限的控制。

  • 在守卫方法中如果不声明next形参,则默认允许用户访问每一个路由!
  • 在守卫方法中如果声明了next形参,则必须调用next()函数,否则不允许用户访问任何一个路由!

5.2 next函数的三种调用方式:

  • 直接放行:next()
  • 强制其停留在当前页面:next(false)
  • 强制其跳转到登录页面:next('/login')
// router.js 部分关键代码
 
// 声明全局的导航守卫
// 三个可选参数:to目标路由对象; from当前导航正要离开的路由对象; next是一个函数,表示放行
router.beforeEach((to, from, next) => {
  const tokenStr = localStorage.getItem('token') // 1、读取token
 
  if (to.path === '/main' && !tokenStr) {  // 2、想访问“后台主页”,且token值不存在
    // next(false) // 3.1 不允许跳转
    next('/login') // 3.2 强制跳转到“登录页面”
  } else {
    next()  // 3.3 直接放行,允许访问后台主页
  }
})

六、后台管理案例

知识点:命名路由、路由重定向、导航守卫、嵌套路由、动态路由匹配、编程式导航

  • main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import './assets/css/bootstrap.css'
// 1、导入路由模块
import router from './router'
 
const app=createApp(App)
 
//2、挂载路由对象
app.use(router)
 
app.mount('#app')
  • router.js
//1、按需导入对应的函数
import { createRouter, createWebHashHistory } from "vue-router";
 
import Login from './components/MyLogin.vue'
import Home from './components/MyHome.vue'
 
import Users from './components/menus/MyUsers.vue'
import Rights from './components/menus/MyRights.vue'
import Goods from './components/menus/MyGoods.vue'
import Orders from './components/menus/MyOrders.vue'
import Settings from './components/menus/MySettings.vue'
import UserDetail from './components/user/MyUserDetail.vue'
 
//2、创建路由对象
const router=createRouter({
    history:createWebHashHistory(),
    routes:[
        // 路由重定向
        {path:'/',redirect:'/login'},
        {path:'/login',component:Login},
        {
            path:'/home',
            component:Home,
            // 用户访问/home时,重定向到/home/users
            redirect:'/home/users',
            // 子路由规则
            children:[
                {path:'users',component:Users},
                {path:'rights',component:Rights},
                {path:'goods',component:Goods},
                {path:'orders',component:Orders},
                {path:'settings',component:Settings},
                // 用户详情页的动态路由规则
                {path:'users/:id',component:UserDetail,props:true},
            ]
        },
    ],
})
 
// 全局路由导航守卫
router.beforeEach((to,from,next)=>{
    // 若用户访问的是登录页面,直接放行
    if(to.path==='/login') return next()
    // 获取Token值
    const token=localStorage.getItem('token')
    if(!token){  // 若Token值不存在,强致跳转登录页面
        return next('/login')
    }
    next()  // 存在token值,直接放行
})
 
//3、向外共享路由实例对象
export default router
  • index.css
:root {
  font-size: 13px;
}
 
html,
body,
#app {
  height: 100%;
}
  • App.vue
<template>
  <!-- 路由占位符  -->
  <router-view></router-view>
</template>
 
<script>
export default {
  name: 'MyApp',
}
</script>
  • /components/MyLogin.vue
<template>
<div class="login-container">
    <div class="login-box">
        <!-- 头像区域 -->
        <div class="text-center avatar-box">
            <img src="../assets/logo.png" class="img-thumbnail avatar" alt="" />
        </div>
        <!-- 表单区域 -->
        <div class="form-login p-4">
            <!-- 登录名称,文本框使用v-model双向数据绑定 -->
            <div class="form-group form-inline">
                <label for="username">登录名称</label>
                <input
                    type="text"
                    class="form-control ml-2"
                    id="username"
                    placeholder="请输入登录名称"
                    autocomplete="off"
                    v-model.trim="username"
                />
            </div>
            <!-- 登录密码,文本框使用v-model双向数据绑定 -->
            <div class="form-group form-inline">
                <label for="password">登录密码</label>
                <input
                    type="password"
                    class="form-control ml-2"
                    id="password"
                    placeholder="请输入登录密码"
                    v-model.trim="password"
                />
            </div>
             <!-- 登录和重置按钮 -->
            <div class="form-group form-inline d-flex justify-content-end">
                <button type="button" class="btn btn-secondary mr-2">重置</button>
                <button type="button" class="btn btn-primary" @click="onLoginClick">登录</button>
            </div>
        </div>
    </div>
</div>
</template>
 
<script>
export default{
    name: 'MyLogin',
    data(){
        return{
            username:'',
            password:'',
        }
    },
    methods:{
        onLoginClick(){
            //判断用户名和密码是否正确
            if (this.username === 'admin' && this.password === '123456') {
                //登录成功,跳转到后台主页
                this.$router.push('/home')
                //模拟存储Token操作
                localStorage.setItem('token', 'Bearer xxx')
            } else {
                //登录成功,清除Token
                localStorage.removeItem('token')
            }
        },
    }
}
</script>
 
<style lang="less" scoped>
.login-container {
  background-color: #35495e;
  height: 100%;
  .login-box {
    width: 400px;
    height: 250px;
    background-color: #fff;
    border-radius: 3px;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    box-shadow: 0 0 6px rgba(255, 255, 255, 0.5);
    .form-login {
      position: absolute;
      bottom: 0;
      left: 0;
      width: 100%;
      box-sizing: border-box;
    }
  }
}
 
.form-control {
  flex: 1;
}
 
.avatar-box {
  position: absolute;
  width: 100%;
  top: -65px;
  left: 0;
  .avatar {
    width: 120px;
    height: 120px;
    border-radius: 50% !important;
    box-shadow: 0 0 6px #efefef;
  }
}
</style>
  • /components/MyHome.vue
<template>
  <div class="home-container">
      <!-- 头部区域 -->
      <my-header></my-header>
 
      <!-- 主体区域 -->
      <div class="home-main-box">
          <!-- 左侧边栏区域 -->
          <my-aside></my-aside>
          <!-- 右侧内容主体区域 -->
          <div class="home-main-body">
              <!-- ***子路由占位符*** -->
              <router-view></router-view>
          </div>
      </div>
  </div>
</template>
 
<script>
// 头部区域组件
import MyHeader from './subcomponents/MyHeader.vue'
// 左侧边栏组件
import MyAside from './subcomponents/MyAside.vue'
 
export default {
  name: 'MyHome',
  components: {
    MyHeader,
    MyAside,
  }
}
</script>
 
<style lang="less" scoped>
.home-container {
  height: 100%;
  display: flex;
  flex-direction: column;
 
  .home-main-box {
    height: 100%;
    display: flex;
    .home-main-body {
      padding: 15px;
      flex: 1;
    }
  }
}
</style>
  • /components/subcomponents/MyHeader.vue
<template>
  <div class="layout-header-container d-flex justify-content-between align-items-center p-3">
    <!-- 左侧 logo 和 标题区域 -->
    <div class="layout-header-left d-flex align-items-center user-select-none">
      <!-- logo -->
      <img class="layout-header-left-img" src="../../assets/logo.png" alt="">
      <!-- 标题 -->
      <h4 class="layout-header-left-title ml-3">后台管理系统</h4>
    </div>
 
    <!-- 右侧按钮区域 -->
    <div class="layout-header-right">
      <button type="button" class="btn btn-light" @click="onLogout">退出登录</button>
    </div>
  </div>
</template>
 
<script>
export default {
  name: 'MyHeader',
  methods: {
    // 退出按钮的点击事件处理函数
    onLogout() {
      //移除Token
      localStorage.removeItem('token')
      //强制跳转到登录页面
      this.$router.push('/login')
    }
  }
}
</script>
 
<style lang="less" scoped>
.layout-header-container {
  height: 60px;
  border-bottom: 1px solid #eaeaea;
}
 
.layout-header-left-img {
  height: 50px;
}
</style>
  • /components/subcomponents/MyAside.vue:略,只有样式,没有路由相关操作。

  • /components/menus/MyUsers.vue

<template>
  <!-- 标题 -->
  <h4 class="text-center">用户管理</h4>
 
  <!-- 用户列表 -->
  <table class="table table-bordered table-striped table-hover">
    <thead>
      <tr>
        <th>#</th>
        <th>姓名</th>
        <th>年龄</th>
        <th>头衔</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(item, i) in userlist" :key="item.id">
        <td>{{ i + 1 }}</td>
        <td>{{ item.name }}</td>
        <td>{{ item.age }}</td>
        <td>{{ item.position }}</td>
        <td>
          <router-link :to="'/home/users/' + item.id">详情</router-link>
        </td>
      </tr>
    </tbody>
  </table>
</template>
 
<script>
export default {
  name: 'MyUser',
  data() {
    return {
      // 用户列表数据
      userlist: [
        { id: 1, name: '嬴政', age: 18, position: '始皇帝' },
        { id: 2, name: '李斯', age: 35, position: '丞相' },
        { id: 3, name: '吕不韦', age: 50, position: '商人' },
        { id: 4, name: '赵姬', age: 48, position: '王太后' },
      ],
    }
  },
}
</script>
 
<style lang="less" scoped></style>
  • /components/menus/MyGoods.vue:略,只有样式,没有路由相关操作。

  • /components/menus/MyOrders.vue:略,只有样式,没有路由相关操作。

  • /components/menus/MyRights.vue:略,只有样式,没有路由相关操作。

  • /components/menus/MySettings.vue:略,只有样式,没有路由相关操作。

  • /components/user/MyUserDetail.vue

<template>
  <button type="button" class="btn btn-light btn-sm" @click="goBack">后退</button>
  <h4 class="text-center">用户详情 --- {{id}}</h4>
</template>
 
<script>
export default {
  name: 'MyUserDetail',
  props: ['id'],
  methods: {
    // 编程式导航实现后退功能
    goBack() {
      this.$router.go(-1)
    }
  }
}
</script>
 
<style lang="less" scoped>
</style>

参考: https://blog.csdn.net/SongD1114/article/details/124055153

https://blog.csdn.net/Gik99/article/details/129368630

https://blog.csdn.net/2301_77638787/article/details/132318329