学习vue3系列-路由编
目标
- vue-router基础使用
- vue-router数据结构
- meta数据结构定义
- 路由守卫
- 路由鉴权过程
- 路由permisson逻辑实现
vue-router基础使用
vue项目的开发是离不开vue-router的。vue作为一个mvvm框架,它的所有的页面跳转都是通过vue-router来完成的,所以对此进行学习是一个非常有必要。
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>
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的属性
路由守卫
在 Vue.js 中,Vue Router 提供了一系列的路由守卫(navigation guard),用于在路由切换过程中进行拦截和控制,以满足不同的业务需求。
Vue Router 的路由守卫主要分为三类
- 全局路由守卫
- 路由独享守卫
- 组件内的守卫`
除了全局路由守卫之外,Vue Router 还提供了路由独享守卫和组件内的守卫。其中,路由独享守卫是针对某个具体的路由实例进行拦截和控制,而组件内的守卫则是针对某个具体的组件进行拦截和控制。
全局路由守卫
全局前置守卫 beforeEach
全局前置守卫是 Vue Router 中最常用的路由守卫之一。它会在路由切换之前进行拦截和控制,以满足不同的业务需求。全局前置守卫的使用方法如下:
router.ts
import router from "@/router";
router.beforeEach((to, from, next) => {
console.log(to, from, next);
next();
});
前置路由守卫常见场景
- 控制用户是否登录,跳转对应页面
- 第三方单点登录
在全局前置守卫中,当我们使用了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。
动态路由生成思路
在前后端分离的项目中,操作路由鉴权的方式,往往会分为以下两种,
- 前端定义一份路由表,在路由表上定义role字段,通过后端getUserInfo接口获取当前登录人的角色,然后再遍历路由表找出符合角色的路由,然后再通过addRoutes动态挂载路由
- 目前我司项目是通过后端getMenu接口,返回当前用户的所有的菜单,前端根据菜单遍历生成路由表,然后再通过addRoutes动态挂载路由。这样做的弊端就是需要和后端的数据结构强耦合起来,而且我们的组件往往是会通过path字段,来生成对应的前端路由表(通过required引入组件方式)
但其实这两种的本质是一样的,就是生成或者找出当前用户的路由表,需要注意的是,这种只是起到了页面控制级别的方式。而页面跳转的过程会遵循以下的规则
- 在router.beforeEach中判断,当前路由是否是存在前端白名单中(一般用于第三方登录鉴权方式)
- 如果存在,则直接跳转页面,不存在则会进入以下过程
- 当前用户是否已经登录(采用本地存储token的形式,判断用户是否登录),如果已经登录,但是跳转的路由是登录页面,则引导到首页路由(如果是选择第二种路由鉴权方式,需要先生成路由,然后再跳转到第一个路由)
- 如果跳转的不是登录页面,则调用getMenu接口,获取所有的菜单生成对应的路由表,然后在进行跳转
所有的过程可以通过下图配合理解。
路由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()
})
转载自:https://juejin.cn/post/7253291597043712058