likes
comments
collection
share

学习vue3系列-路由编

作者站长头像
站长
· 阅读数 16

学习vue3系列-路由编

目标

  1. vue-router基础使用
  2. vue-router数据结构
  3. meta数据结构定义
  4. 路由守卫
  5. 路由鉴权过程
  6. 路由permisson逻辑实现

vue-router基础使用

vue项目的开发是离不开vue-router的。vue作为一个mvvm框架,它的所有的页面跳转都是通过vue-router来完成的,所以对此进行学习是一个非常有必要。

router.vuejs.org/zh/introduc…

npm i vue-router --save

在src下新建router/index.ts目录

import { createRouter, createWebHashHistory } from "vue-router";
import Login from "@/views/login/index.vue";
import Dashboard from "@/views/dashboard/index.vue";
export const constantRoutes = [
  {
    path: "/login",
    component: Login,
  },
  {
    path: "/dashboard",
    component: Dashboard,
  },
];

const router = createRouter({
  history: createWebHashHistory("hash"),
  routes: constantRoutes,
});
export default router;

定义路由的方式大部分是与vue2一致的,唯一不同之处就是createRouter方法的时候,需要手动调用createWebHashHistory('hash')。代表的意思为页面跳转是hash的模式,也就是带有#号的形式。

在main.ts中引入router

import { createApp } from "vue";
import App from "./App.vue";
import router from "@/router";
const app = createApp(App);
app.use(router);
app.mount("#app");

在app.vue中引入RouterView组件即可,在浏览器上输入login和home便可出现对应的内容

<script setup lang="ts"></script>

<template>
  <RouterView />
</template>

<style scoped></style>

学习vue3系列-路由编

meta数据结构定义

对于router的使用,我们可以在业务代码中,通过useRouter()这个hook获取router的所有信息,以login.vue为例子

<template>login</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
const router = useRouter();
console.log(router, "r");
console.log(router.options.routes[0].meta.);
</script>
<style scoped></style>

但是在以上的代码中,发现通过meta字段无法直接点出你所需要的属性,并且ts也会给出对应的保存。meta字段是用来存储当前路由额外的属性。解决这个问题,需要通过TS定义meta的字段类型。

在src/types目录,使用declare来对RouteMeta进行额外定义

import "vue-router";

declare module "vue-router" {
  interface RouteMeta {
    /**
     * 设置该路由在侧边栏和面包屑中展示的名字
     */
    title?: string;
    /**
     * 设置该路由的图标,记得将 svg 导入 @/icons/svg
     */
    svgIcon?: string;
    /**
     * 设置该路由的图标,直接使用 Element Plus 的 Icon(与 svgIcon 同时设置时,svgIcon 将优先生效)
     */
    elIcon?: string;
    /**
     * 默认 false,设置 true 的时候该路由不会在侧边栏出现
     */
    hidden?: boolean;
    /**
     * 设置该路由进入的权限,支持多个权限叠加
     */
    roles?: string[];
    /**
     * 默认 true,如果设置为 false,则不会在面包屑中显示
     */
    breadcrumb?: boolean;
    /**
     * 默认 false,如果设置为 true,它则会固定在 tags-view 中
     */
    affix?: boolean;
    /**
     * 当一个路由下面的 children 声明的路由大于 1 个时,自动会变成嵌套的模式,
     * 只有一个时,会将那个子路由当做根路由显示在侧边栏,
     * 若想不管路由下面的 children 声明的个数都显示你的根路由,
     * 可以设置 alwaysShow: true,这样就会忽略之前定义的规则,一直显示根路由
     */
    alwaysShow?: boolean;
    /**
     * 示例: activeMenu: "/xxx/xxx"
     * 当设置了该属性进入路由时,则会高亮 activeMenu 属性对应的侧边栏。
     * 该属性适合使用在有 hidden: true 属性的路由上
     */
    activeMenu?: string;
    /**
     * 是否缓存该路由页面
     * 默认为 false,为 true 时代表需要缓存,此时该路由和该页面都需要设置一致的 Name
     */
    keepAlive?: boolean;
  }
}

这时候我们在回到login.vue中,通过.的形式,就会出现对应meta的属性

学习vue3系列-路由编

路由守卫

在 Vue.js 中,Vue Router 提供了一系列的路由守卫(navigation guard),用于在路由切换过程中进行拦截和控制,以满足不同的业务需求。

Vue Router 的路由守卫主要分为三类

  1. 全局路由守卫
  2. 路由独享守卫
  3. 组件内的守卫`

除了全局路由守卫之外,Vue Router 还提供了路由独享守卫和组件内的守卫。其中,路由独享守卫是针对某个具体的路由实例进行拦截和控制,而组件内的守卫则是针对某个具体的组件进行拦截和控制。

全局路由守卫

全局前置守卫 beforeEach

全局前置守卫是 Vue Router 中最常用的路由守卫之一。它会在路由切换之前进行拦截和控制,以满足不同的业务需求。全局前置守卫的使用方法如下:

router.ts

import router from "@/router";
router.beforeEach((to, from, next) => {
  console.log(to, from, next);
  next();
});

前置路由守卫常见场景

  1. 控制用户是否登录,跳转对应页面
  2. 第三方单点登录

在全局前置守卫中,当我们使用了beforeEach这个方法后,是需要手动在函数体内调用next方法,我们以用户是否登录为例子,如果没有登录,则跳转到login页面,登录了之后,则正常跳转路由

router.beforeEach((to, from, next) => {
  if (to.path === '/login') {
    next()
  } else {
    if (isLogin()) {
      next()
    } else {
      next('/login')
    }
  }
})

第三方单点登录,我们往往会采用一个空白路由,对跳转过来的路由进行解析,然后再模拟登录一个过程即可。

<template>authorzition</template>
<script setup lang="ts">
import { toRaw } from "vue";
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
console.log(toRaw(route), "this.$route");
const { token } = route.query;
// console.log(query, "query");
if (token) {
  setToken(token);
  router.push("/dashboard");
}
</script>
<style scoped></style>

全局后置守卫 afterEach

全局后置守卫 afterEach 会在每次路由跳转结束后被调用,无论是正常跳转还是取消跳转。它接收三个参数: 我们可以通过 afterEach 钩子做一些全局的收尾工作,例如埋点、页面滚动等操作。

router.afterEach((to, from) => {
  // 埋点
  trackPage(to.path)
  
  // 页面滚动到顶部
  window.scrollTo(0, 0)
})

需要注意的是,afterEach 钩子不像其他路由钩子一样,不能通过调用 next 函数来控制路由的跳转行为,因为此时路由跳转已经结束了。如果在 afterEach 钩子中调用 next 函数,则会抛出一个错误。

路由独享守卫 beforeEnter

路由独享守卫 beforeEnter 路由独享守卫是针对某个具体的路由实例进行拦截和控制。它和全局前置守卫的用法类似,只不过它是针对某个具体的路由实例而言的。

const router = new VueRouter({
  routes: [
    {
      path: '/login',
      component: login,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})

路由鉴权过程

获取用户登录信息

用户登录成功之后,我们是需要存储到全局中,可以借助cookies,localstorage,sessionstorage方式存储到本地。在这里就需要用到vue中全局管理工具pinia插件

const handleLogin = () => {
  loginFormRef.value?.validate((valid: boolean, fields) => {
    if (valid) {
      loading.value = true
      useUserStore()
        .login(loginFormData)
        .then(() => {
          router.push({ path: "/" })
        })
        .catch(() => {
          createCode()
          loginFormData.password = ""
        })
        .finally(() => {
          loading.value = false
        })
    } else {
      console.error("表单校验不通过", fields)
    }
  })
}

store/module/user.ts

const login = async ({ username, password, code }: LoginRequestData) => {
    const { data } = await loginApi({ username, password, code })
    setToken(data.token)
    token.value = data.token
  }
/** 统一处理 Cookie */

import CacheKey from "@/constants/cache-key"
import Cookies from "js-cookie"

export const getToken = () => {
  return Cookies.get(CacheKey.TOKEN)
}
export const setToken = (token: string) => {
  Cookies.set(CacheKey.TOKEN, token)
}
export const removeToken = () => {
  Cookies.remove(CacheKey.TOKEN)
}

在utils/request中,每次请求,都携带token

function createRequest(service: AxiosInstance) {
  return function <T>(config: AxiosRequestConfig): Promise<T> {
    const token = getToken()
    const defaultConfig = {
      headers: {
        // 携带 Token
        Authorization: token ? `Bearer ${token}` : undefined,
        "Content-Type": "application/json"
      },
      timeout: 5000,
      data: {}
    }
    // 将默认配置 defaultConfig 和传入的自定义配置 config 进行合并成为 mergeConfig
    const mergeConfig = merge(defaultConfig, config)
    return service(mergeConfig)
  }
}

在本地cookies中,存储了当前的cookies,在我们每次发起ajax请求的时候,都需要添加到header中去,由后端的同事进行处理,判断当前用户是否登录过期等信息。

ps:之所有采取cookies存储的方式,是为了下次打开页面或者刷新页面的时候能记住用户的登录状态,不用再去登录页面重新登录了。当前用storage的api也可以实现该效果,只不过是每次刷新都要重新去本地重新获取token。

动态路由生成思路

在前后端分离的项目中,操作路由鉴权的方式,往往会分为以下两种,

  1. 前端定义一份路由表,在路由表上定义role字段,通过后端getUserInfo接口获取当前登录人的角色,然后再遍历路由表找出符合角色的路由,然后再通过addRoutes动态挂载路由
  2. 目前我司项目是通过后端getMenu接口,返回当前用户的所有的菜单,前端根据菜单遍历生成路由表,然后再通过addRoutes动态挂载路由。这样做的弊端就是需要和后端的数据结构强耦合起来,而且我们的组件往往是会通过path字段,来生成对应的前端路由表(通过required引入组件方式)

但其实这两种的本质是一样的,就是生成或者找出当前用户的路由表,需要注意的是,这种只是起到了页面控制级别的方式。而页面跳转的过程会遵循以下的规则

  1. 在router.beforeEach中判断,当前路由是否是存在前端白名单中(一般用于第三方登录鉴权方式)
  2. 如果存在,则直接跳转页面,不存在则会进入以下过程
  3. 当前用户是否已经登录(采用本地存储token的形式,判断用户是否登录),如果已经登录,但是跳转的路由是登录页面,则引导到首页路由(如果是选择第二种路由鉴权方式,需要先生成路由,然后再跳转到第一个路由)
  4. 如果跳转的不是登录页面,则调用getMenu接口,获取所有的菜单生成对应的路由表,然后在进行跳转

所有的过程可以通过下图配合理解。

学习vue3系列-路由编

路由permisson逻辑实现

基于以上的理论基础,可以在router/permission文件中编写以下代码(是基于路由鉴权第一种方式)

import router from "@/router"
import { useUserStoreHook } from "@/store/modules/user"
import { usePermissionStoreHook } from "@/store/modules/permission"
import { ElMessage } from "element-plus"
import { getToken } from "@/utils/cache/cookies"
import asyncRouteSettings from "@/config/async-route"
import isWhiteList from "@/config/white-list"
import NProgress from "nprogress"
import "nprogress/nprogress.css"

NProgress.configure({ showSpinner: false })

router.beforeEach(async (to, _from, next) => {
  NProgress.start()
  const userStore = useUserStoreHook()
  const permissionStore = usePermissionStoreHook()
  // 判断该用户是否登录
  if (getToken()) {
    if (to.path === "/login") {
      // 如果已经登录,并准备进入 Login 页面,则重定向到主页
      next({ path: "/" })
      NProgress.done()
    } else {
      // 检查用户是否已获得其权限角色
      if (userStore.roles.length === 0) {
        try {
          if (asyncRouteSettings.open) {
            // 注意:角色必须是一个数组! 例如: ['admin'] 或 ['developer', 'editor']
            await userStore.getInfo()
            const roles = userStore.roles
            // 根据角色生成可访问的 Routes(可访问路由 = 常驻路由 + 有访问权限的动态路由)
            permissionStore.setRoutes(roles)
          } else {
            // 没有开启动态路由功能,则启用默认角色
            userStore.setRoles(asyncRouteSettings.defaultRoles)
            permissionStore.setRoutes(asyncRouteSettings.defaultRoles)
          }
          // 将'有访问权限的动态路由' 添加到 Router 中
          permissionStore.dynamicRoutes.forEach((route) => {
            router.addRoute(route)
          })
          // 确保添加路由已完成
          // 设置 replace: true, 因此导航将不会留下历史记录
          next({ ...to, replace: true })
        } catch (err: any) {
          // 过程中发生任何错误,都直接重置 Token,并重定向到登录页面
          userStore.resetToken()
          ElMessage.error(err.message || "路由守卫过程发生错误")
          next("/login")
          NProgress.done()
        }
      } else {
        next()
      }
    }
  } else {
    // 如果没有 Token
    if (isWhiteList(to)) {
      // 如果在免登录的白名单中,则直接进入
      next()
    } else {
      // 其他没有访问权限的页面将被重定向到登录页面
      next("/login")
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  NProgress.done()
})