第二节:深度剖析菜单权限、按钮权限、keepAlive缓存的设计
一. 剖析菜单权限
(一). 登录成功后,调用initBackEndControlRoutes方法,初始化路由
1. 初始化用户信息(含基础信息、菜单权限、按钮权限),存放vuex中, store.dispatch('userInfos/setUserInfos');
2. 获取路由全部菜单
(1). 请求接口,获取所有的路由信息 (注意:这里不是特定权限哦)
(2). 将路由信息存放到vuex中的requestOldRoutes里
(3). 将获取的路由数据转换一下,即把 "component": "home/index" 转换成 component: () => import('/@/views/home/index.vue'),
最终赋值给 dynamicRoutes[0].children = await backEndComponent(res.data); 后面的setAddRoute方法中将使用赋值后的dynamicRouters
注意:此时dynamicRoute并没有注册到vue-router中
3. 将用户具有权限的菜单对应路由注册到vue-router中,详见 setAddRoute 方法
(1). 获取当前用户所具有权限标识的数组 setFilterRouteEnd()
A. 将多级路由嵌套处理成一维数组 formatFlatteningRoutes(dynamicRoutes) 【难点!】
B. 再一维数组处理成多级嵌套数组,返回 dynamicRoutes 的格式,但这里只处理两级即可。 详见:formatTwoStageRoutes 解释
【主要目的是处理keep-alive缓存,将所有需要缓存的菜单name存放到vuex中的keepAliveNames数组中】
C. 获取当前用户权限标识的路由数组 setFilterRoute(filterRouteEnd[0].children)
【已经改造成meta.roles数组中只存一个值,且和path值相同的情况了】
D. 将pathMatch(未匹配上统一跳转404)的路由和C中获取的路由合并,统一返回
(2). 遍历数组进行路由的注册
根据routeName值判断路由中是否已经存在,不存在则通过 router.addRoute(route) 进行添加
4. 准备左侧菜单所需要的数据 + tagView需要的数据 详见setFilterMenuAndCacheTagsViewRoutes方法
(1).获取用户所具有权限的左侧(或横向)菜单数据,赋值给vuex中的routesList
A. 通过setFilterHasRolesMenu方法递归遍历dynamicRoutes[0].children的数据, 利用hasRoles方法判断是否具有这项菜单权限,最后返回具有权限的菜单数据
【重点:这里是在dynamicRoutes[0].children上处理,如果没有权限,删掉其中的那部分,但是格式还是形如 dynamicRoutes[0].children的格式】
B. 赋值给vuex中的routesList,即菜单所需要的数据
(2).将用户具有权限的菜单数据对应的路由向vuex中的tagsViewRoutes存放,供tagView使用 详见setCacheTagsViewRoutes方法
A. 获取用户具有权限的菜单数据 详见setFilterHasRolesMenu
B. 将路由多级嵌套处理成一维数组 详见formatFlatteningRoutes
C. 一维数组处理成多级嵌套数组 详见 formatTwoStageRoutes
D. 将具有权限的菜单存放到Vuex中的tagsViewRoutes
注:此处的步骤和上面步骤3中的路由注册有点重复,考虑精简一下
(二) 相关菜单组件的显示
封装了两个菜单组件,水平方向和竖直方向的,分别是:
1. layout/navMenu/horizontal.vue 水平方向的菜单
调用位置:layout/navBars/breadcrumb/index.vue, 传递的参数 store.state.routesList.routesList
2. layout/navMenu/vertical.vue 竖直方向的菜单
调用位置:layout/component/asize.vue,传递参数 store.state.routesList.routesList
总结:最终传递的数据都是vuex中的store.state.routesList.routesList
二. 剖析按钮权限
1. 思考
(1). 项目中总共有几类情况用到按钮权限?
【页面的按钮是一类,表格的查询权限是一类】
(2). 按钮权限的定义规则和存放问题?
【形如:/system/user/search,最后一部分为按钮的含义;用户所具有的按钮权限以数组的形式存放在VueX中的authBtnList中】
(3). 页面上如何判断是否有该按钮权限呢?
本质:就是判断Vuex中的authBtnList中是否包含这个按钮名字,包含则显示,反之则销毁。这里封装三种形式判断有无:
a. 函数形式:单个权限和多个(满足1个 or 全部满足)权限验证,详见/utils/authFunction
查看代码import { store } from '/@/store/index';
import { judementSameArr } from '/@/utils/arrayOperation';
/**
* 单个权限验证
* @param value 权限值
* @returns 有权限,返回 `true`,反之则反
*/
export function auth(value) {
return store.state.userInfos.userInfos.authBtnList.some((v) => v === value);
}
/**
* 多个权限验证,满足一个则为 true
* @param value 权限值
* @returns 有权限,返回 `true`,反之则反
*/
export function auths(value) {
let flag = false;
store.state.userInfos.userInfos.authBtnList.map((val) => {
value.map((v) => {
if (val === v) flag = true;
});
});
return flag;
}
/**
* 多个权限验证,全部满足则为 true
* @param value 权限值
* @returns 有权限,返回 `true`,反之则反
*/
export function authAll(value) {
return judementSameArr(value, store.state.userInfos.userInfos.authBtnList);
}
/**
* 判断两数组是否相同(忽略顺序)
* @param news 新数据
* @param old 源数据
* @returns 两数组相同返回 `true`,反之则反
*/
export function judementSameArr(news, old) {
let count = 0;
const leng = old.length;
// for in也可以遍历数组,通常用它来遍历对象
for (let i in old) {
for (let j in news) {
if (old[i] === news[j]) count++;
}
}
return count === leng ? true : false;
}
b. 指令形式:单个权限和多个权限验证,定义详见/utils/directive/authDirective,在main.js进行全局指令的注册
查看代码 import { store } from '/@/store/index';
import { judementSameArr } from '/@/utils/arrayOperation';
/**
* 用户权限指令
* @directive 单个权限验证(v-auth="xxx")
* @directive 多个权限验证,满足一个则显示(v-auths="[xxx,xxx]")
* @directive 多个权限验证,全部满足则显示(v-auth-all="[xxx,xxx]")
*/
export function authDirective(app) {
// 单个权限验证(v-auth="xxx")
app.directive('auth', {
mounted(el, binding) {
if (!store.state.userInfos.userInfos.authBtnList.some(v => v === binding.value)) el.parentNode.removeChild(el);
},
});
// 多个权限验证,满足一个则显示(v-auths="[xxx,xxx]")
app.directive('auths', {
mounted(el, binding) {
let flag = false;
store.state.userInfos.userInfos.authBtnList.map(val => {
binding.value.map(v => {
if (val === v) flag = true;
});
});
if (!flag) el.parentNode.removeChild(el);
},
});
// 多个权限验证,全部满足则显示(v-auth-all="[xxx,xxx]")
app.directive('auth-all', {
mounted(el, binding) {
const flag = judementSameArr(binding.value, store.state.userInfos.userInfos.authBtnList);
if (!flag) el.parentNode.removeChild(el);
},
});
}
c. 组件形式:详见components/auth文件夹下的三个组件
auth.vue
<template> <slot v-if="getUserAuthBtnList" /> </template> <script> import { computed } from 'vue'; import { useStore } from 'vuex'; export default { name: 'auth', props: { value: { type: String, default: () => '', }, }, setup(props) { const store = useStore(); // 获取 vuex 中的用户权限 const getUserAuthBtnList = computed(() => { return store.state.userInfos.userInfos.authBtnList.some(v => v === props.value); }); return { getUserAuthBtnList, }; }, }; </script>
authAll.vue
<template> <slot v-if="getUserAuthBtnList" /> </template> <script> import { computed } from 'vue'; import { useStore } from 'vuex'; import { judementSameArr } from '/@/utils/arrayOperation'; export default { name: 'authAll', props: { value: { type: Array, default: () => [], }, }, setup(props) { const store = useStore(); // 获取 vuex 中的用户权限 const getUserAuthBtnList = computed(() => { return judementSameArr(props.value, store.state.userInfos.userInfos.authBtnList); }); return { getUserAuthBtnList, }; }, }; </script>
auths.vue
<template> <slot v-if="getUserAuthBtnList" /> </template> <script> import { computed } from 'vue'; import { useStore } from 'vuex'; export default { name: 'auths', props: { value: { type: Array, default: () => [], }, }, setup(props) { const store = useStore(); // 获取 vuex 中的用户权限 const getUserAuthBtnList = computed(() => { let flag = false; store.state.userInfos.userInfos.authBtnList.map(val => { props.value.map(v => { if (val === v) flag = true; }); }); return flag; }); return { getUserAuthBtnList, }; }, }; </script>
用法:
<Auth :value="authList.add"> <el-button size="mini" type="success" @click="onOpenAddDialog" v-auth="authList.add"> 新增 </el-button> </Auth>
2. 实操
A. 登录的时候从接口中获取按钮权限,存放到Vuex中的 userInfos.authBtnList
B. 页面上先把该页面用到的所有按钮权限名称定义出来,如下
const authList = reactive({
search: '/system/user/search',
add: '/system/user/add',
edit: '/system/user/edit',
delOne: '/system/user/delOne',
delMany: '/system/user/delMany',
excel: '/system/user/excel',
arrange: '/system/user/arrange',
});
C. 使用 v-auth指令的模式绑定按钮
<template #handler="myInfo">
<el-button size="small" type="text" @click="onOpenEditDialog(myInfo.row1)" v-auth="authList.edit">修改</el-button>
<el-button size="small" type="text" @click="deleteObjs(myInfo.row1.id)" v-auth="authList.delOne">删除</el-button>
</template>
D. 使用 auth方法处理表格是否有查询权限
import { auth } from '/@/utils/authFunction';
/**
* 初始化表格数据
*/
const initTableData = async () => {
if (!auth(authList.search)) {
ElMessage.error('您没有查询权限');
return;
}
//将string类型的对象去空格
let myTrimData = TrimObjStrPros(formData.value);
const { status, data } = await myAxios({
url: proxy.$url.GetUserInforByConditionUrl,
data: { ...tableData.param, ...myTrimData },
});
if (status == 'ok') {
tableData.tableRows = data.tableRows;
tableData.total = data.total;
}
};
三. keepAlive缓存设计
1. 思考
(1). 菜单路由中的参数 meta对象中的 isKeepAlive 作用是什么?
【答:当这个菜单对应的isKeepAlive=true时,表示该菜单需要缓存,会存放到vuex中的keepAliveNames数组,详见router/index.js/formatTwoStageRoutes方法】
(2). 菜单存在上下级问题,如果上级isKeepAlive为false,下级的isKeepAlive设为true,有效吗?
【答:有效! 但不建议这么设置,建议下级只要有1个isKeepAlive设为true,就将他的父级isKeepAlive设为true 详见router/index.js/formatTwoStageRoutes方法
分析该方法,这里只分两级,第一级是指 / 这种路径,这个路径的isKeepAlive必须设置为true,而对于菜单的上下级,比如/system/role 和 /system 是同一级的。
该方法的判断条件是:if (newArr[0].meta.isKeepAlive && v.meta.isKeepAlive) 说明他这个第一级是指 / 的这种路径 】
(3). Vuex中的keepAliveNames数组,作用是什么?
【答:形如:["home","system","systemUser","systemRole"],存放的是需要缓存菜单的名称,最后放在parent.vue页面中 <keep-alive> 组件的includes属性中】
(4). Vuex中的 tagsViewRoutes 数组,作用是什么?
【答:这里存放的是具有权限菜单的路由数据,与isKeepAlive无关 】
(5). 点击左侧的菜单,是如何与右侧的tageView相关联的?
【答:以vertical.vue竖向菜单为例,el-menu添加的router属性,表示启用vue-router模式,启用该模式会在激活导航时以index的值作为path进行路由跳转】
(6). <keep-alive>组件所在的页面 parent.vue 中的 v-slot="{ Component }" 怎么理解?
<router-view v-slot="{ Component }">
<transition :name="setTransitionName" mode="out-in">
<keep-alive :include="state.keepAliveNameList">
<component :is="Component" :key="state.refreshRouterViewKey" class="w100" />
</keep-alive>
</transition>
</router-view>
【参考官网:https://router.vuejs.org/zh/api/#router-view-的-v-slot】
【答:通过路由跳转,比如跳转路径为 /system/user, router-view组件中Component将恢自动解析为 /system/user 路径对应的组件,从而赋值给 :is="Component" 】
(7). 每个菜单对应的Vue页面为什么有个name值?这个值为什么需要和路由菜单中name值相同才能实现页面缓存呢?
【参考官网:https://v3.cn.vuejs.org/api/built-in-components.html#keep-alive 】
【答:
A. 观察parent.vue中的<keep-alive>组件,include里存放的是Vuex中keepAliveNames数组,而该数组存放的就是需要缓存的菜单路由的name值
B. <component>组件匹配的时候,首先检查组件自身的 name 选项,所以要定义菜单页面的name值 】
(8). 设置中的开启tagView缓存的作用是什么?
【答:可以缓存多个tagView,刷新页面不消失】
(9). 设置中的tagView公用的作用是什么?
【】
2. 分析流程
肯定是使用keepAlive,使用方法:每个页面都必须加一个name属性,这个name属性必须和路由菜单中的name值相同,才能实现keepAlive缓存。
原理:需要全局搜索一下 keep-alive,找到相应文件
<keep-alive> → parent.vue → main.vue → [classic.vue] [columns.vue] [defaults.vue] [transverse.vue]
(1). <keep-alive> 组件在 parent.vue 中使用 (layout\routerView\parent.vue),keep-alive中存放<component>动态组件
【:include里传入的值是vuex中的store.state.keepAliveNames.keepAliveNames】
(2). parent.vue组件在 main.vue 中使用 (\layout\component\main.vue)
(3). main.vue组件在 /layout/main下的四个组件中调用,分别是:[classic.vue] [columns.vue] [defaults.vue] [transverse.vue]
现象: 开启缓存后,页面关闭,下次打开仍然缓存,实际我不想这样
3. tagsView组件分析
【组件详见:/@/layout/navBars/tagsView/tagsView.vue'】
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。