part02~开发通用前端UI框架之权限设计(路由,组件,指令)

Chynna ·
更新时间:2024-11-13
· 617 次阅读

设计左侧菜单和路由的关系

注意了:左侧菜单往往跟当前 登陆者的权限有关系,这一类数据可能是后端返回的,可能是多级的关系,那么就要用到递归的方式,所以我们用 Antd 中‘单文件递归菜单’

layouts->SiderMenu.vue 递归当前 Antd 版本-1.5.0-rc.5 版本 你会发现你复制过来的代码有点乱,还报错,还可能看不大懂对吗? 因为这个代码中包含了左侧菜单的样式,还有一个递归项:SubMenu 索性咱们将 SubMenu 抽离出来做一个递归组件,于是呢,你就再 layouts 下面新建 SubMenu.vue,代码应该是这样的 {{ props.menuInfo.title }} {{ item.title }} export default { props: ['menuInfo'], name: 'SubMenu', // must add isSubMenu: true isSubMenu: true } 于是乎呢,拆解之后你的 SiderMenu 应该是这个样子的
{{ item.title }}
// 这个就是你新建的递归组件 import SubMenu from './SubMenu' export default { components: { SubMenu }, data() { return { collapsed: false, list: [ { key: '1', title: 'Option 1' }, { key: '2', title: 'Navigation 2', children: [ { key: '2.1', title: 'Navigation 3', children: [{ key: '2.1.1', title: 'Option 2.1.1' }] } ] } ] } }, methods: { toggleCollapsed() { this.collapsed = !this.collapsed } } }

至此呢,你的左侧菜单就出来了,剩下的调整样式

是不是宽了? - 改啊,将 SliderMenu 宽度跟左侧布局的宽度调整一样

SliderMenu.vue
BasicLayout.vue

到这你的左侧菜单就出来了

但是你发现切换主题颜色时,左侧 menu 没有颜色变化,我们要修改成同步的

SliderMenu

props: { theme: { type: String, default: 'dark' // 默认黑色 } }, BasicLayout.vue 将我们需要的真实路由渲染到菜单上,实现菜单控制路由

router->index.js

注意:菜单应该是我登陆之后需要使用的一些功能页面的连接目录,我们希望通过点击菜单目录切换页面,所以有些路由我们不需要渲染到菜单列表中去

比如:登陆页面,渲染到菜单上没意义,我们不需要那么怎么办呢? 约定一:添加一个排除渲染的标签,比如叫:hideInMenu 属性 path: '/user', hideInMenu: true, // 添加一个不渲染的标识符 component: () => { return import(/* webpackChunkName: "user" */ '../layouts/UserLayout.vue') } 约定二:我们之渲染带 name 属性的路由 约定三:同级路由下的分步展示,比如分步表单,只是步骤的切换,而不是页面切换时,这种情况也不渲染 menu,比如:hideChildrenInMenu: true { path: '/form/step-form', name: 'stepform', hideChildrenInMenu: true, // 注意看这里,分布操作时我们需要处理,子代路由不渲染 meta: { title: '分布表单' }, component: () => import(/* webpackChunkName: "form" */ '../views/Forms/StepForm'), children: [ { 除此之外了,我们希望有菜单名称和 icon path: '/dashboard', name: 'dashboard', meta: { icon: 'dashboard', title: '仪表盘' }, // 给你需要渲染的menu自定义一个对象用来渲染名称和icon component: { render: h => h('router-view') }, 接下来我们就要将规定好的路由信息渲染到 menu 上去了

SiderMenu.vue

将原有默认的 list 干掉 // 通过路由对象获取所有的路由信息 let menuData = this.getMenuData(this.$router.options.routes) getMenuData(routes) { // 递归的方式获取路由列表,筛选出我们索要呈现的列表 const menuData = [] routes.forEach(item => { if (item.name && !item.hideInMenu) { const newItem = { ...item } delete newItem.children if (item.children && !item.hideChildrenInMenu) { newItem.children = this.getMenuData(item.children) } menuData.push(newItem) } else if ( !item.hideInMenu && !item.hideChildrenInMenu && item.children ) { menuData.push(...this.getMenuData(item.children)) } console.log('去你吗的', menuData) }) return menuData } // 最后将list替换成menuData

修改模板

{{ item.meta.title }}
修改 SubMenu.vue {{ props.menuInfo.meta.title }} {{ item.meta.title }} export default { props: ['menuInfo'], name: 'SubMenu', // must add isSubMenu: true isSubMenu: true } 点击菜单跳转路由 SiderMenu.vue 外层路由链接
this.$router.push({ path: item.path, query: this.$router.query }) " > {{ item.meta.title }}
import SubMenu from './SubMenu' export default { components: { SubMenu }, props: { theme: { type: String, default: 'dark' } }, data() { this.selectedKeysMap = {} this.openKeysMap = {} let menuData = this.getMenuData(this.$router.options.routes) return { menuData, collapsed: false, selectedKeys: this.selectedKeysMap[this.$route.path], openKeys: this.collapsed ? [] : this.openKeysMap[this.$route.path] } }, watch: { '$route.path': function(val) { // 同步观察路由变换实时更新 this.selectedKeys = this.selectedKeysMap[val] this.openKeys = this.collapsed ? [] : this.openKeysMap[val] } }, methods: { toggleCollapsed() { this.collapsed = !this.collapsed }, getMenuData(routes = [], parentKeys = [], selectedKey) { // 递归的方式获取路由列表,筛选出我们索要呈现的列表 const menuData = [] routes.forEach(item => { if (item.name && !item.hideInMenu) { // 过滤只有带name的属性的路由信息和非隐藏路由 this.openKeysMap[item.path] = parentKeys this.selectedKeysMap[item.path] = [item.path || selectedKey] const newItem = { ...item } delete newItem.children if (item.children && !item.hideChildrenInMenu) { // 如果存在子项,就继续递归子项-解决多级路由的问题 newItem.children = this.getMenuData(item.children, [ ...parentKeys, item.path ]) } else { this.getMenuData( item.children, selectedKey ? parentKeys : [...parentKeys, item.path], // 解释这一步,这个解决什么呢,比如分布表单,我们点击步骤,不能按步骤跳吧,是他的父级路由才会发生跳转,所以呢,我们找他的父级路由作为跳转对象 selectedKey || item.path ) } menuData.push(newItem) } else if ( !item.hideInMenu && !item.hideChildrenInMenu && item.children ) { menuData.push( ...this.getMenuData(item.children, [...parentKeys, item.path]) ) } }) return menuData } } } SubMenu.vue 循环多层路由链接 {{ props.menuInfo.meta.title }} parent.$router.push({ path: item.path, query: parent.$router.query }) " > {{ item.meta.title }} 路由权限管理 根据用户权限,来渲染路由列表,达到权限控制的目的 新建 anth 文件夹 auth->index.js // 获取权限 export function getCurrentAuthority() { // 这里返回的权限应该是从后端读取回来的,此时用admin替代 return ['admin'] } // 鉴权 export function check(authority) { const current = getCurrentAuthority() return current.some(item => authority.includes(item)) } // 判断是否登陆 export function isLogin() { const current = getCurrentAuthority() return current && current[0] !== 'guest' } 接下来去判断用户是否具有路由权限 1、给路由添加 authority 范围 2、在路由守卫中做统一处理 router->index.js 在每个路由的 meta 对象中新增 authority 属性,然后 authority 的值就是权限类型 如: { path: "/", meta:{authority:['user','admin']}, // 约定只有user和admin才能访问 } 在比如: { path: '/form', name: 'form', component: { render: h => h('router-view') }, meta: { icon: 'form', title: '表单', authority: ['admin'] }, // 约定表单只有admin才能访问 redirect: '/form/basic-form' }

普及一个新的知识点:lodash.js

这是个什么玩意儿呢?你可以理解成代码兵器库,what?,类似于 jquery 一样,用原生的 js 写出了很多趁手的常用的方法,供我们使用,很神奇,很高效哦~

为了方便咱们开发我们可以引入这个库

npm i --save lodash https://www.lodashjs.com/docs/lodash.concat

使用 lodash import findLast from ‘lodash/findLast’ 引入即可

引入权限控制的文件

import { check, isLogin } from ‘@/auth/index’

router->index

// 路由守卫 const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) router.beforeEach((to, from, next) => { // 只有在路由地址发生变化时触发进度条 if (to.path != from.path) { NProgress.start() } const record = findLast(to.matched, record => record.meta.authority) if (record && !check(record.meta.authority)) { // 如果没有权限 // 再次判断是否登陆了 if (!isLogin() && to.path !== '/user/login') { //登陆直接跳到登录页页面 next({ path: '/user/login' }) } else if (to.path !== '/403') { // 如果权限不够直接去403,需要去新建一个403的页面和路由 next({ path: '/403' }) } nProgress.done() } next() }) router.afterEach(() => { NProgress.done() }) export default router 新增一个 403 // 403页面 { path: '/403', name: '403', component: Forbiden, hideInMenu: true }, views->403.vue 新增一个页面 此时你去修改 auth->index.js 中的权限,比如把 admin,改成 user,你会发现,你的表单直接跳到 403,why? 那是因为你去 router->index,看看你新增的 meta 的 authority,是不是限制了权限?这样就关联起来了,懂么? 但是此时依旧有一个问题,我们通常希望,没有权限的路由咱就直接不让你看见了对吧,所以要微调一下,既然你要控制菜单渲染,就要回到菜单中去 SiderMenu.vue // 引入权限校验的类库 import { check } from '@/auth/index' getMenuData(routes = [], parentKeys = [], selectedKey) { // 递归的方式获取路由列表,筛选出我们索要呈现的列表 const menuData = [] routes.forEach(item => { // 注意这里:这里就是如果权限不够呢,直接阻止列表渲染 if (item.meta && item.meta.authority && !check(item.meta.authority)) { return false } if (item.name && !item.hideInMenu) { // 过滤只有带name的属性的路由信息和非隐藏路由 this.openKeysMap[item.path] = parentKeys this.selectedKeysMap[item.path] = [item.path || selectedKey] const newItem = { ...item } delete newItem.children if (item.children && !item.hideChildrenInMenu) { // 如果存在子项,就继续递归子项 newItem.children = this.getMenuData(item.children, [ ...parentKeys, item.path ]) } else { this.getMenuData( item.children, selectedKey ? parentKeys : [...parentKeys, item.path], // 解释这一步,这个解决什么呢,比如分布表单,我们点击步骤,不能按步骤跳吧,是他的父级路由才会发生跳转,所以呢,我们找他的父级路由作为跳转对象 selectedKey || item.path ) } menuData.push(newItem) } else if ( !item.hideInMenu && !item.hideChildrenInMenu && item.children ) { menuData.push( ...this.getMenuData(item.children, [...parentKeys, item.path]) ) } }) return menuData }

此时你去修改 auth->index 中的权限时,就会发现菜单列表不满足权限的都没有渲染

如果权限不够时,我希望有提示信息,于是乎呢,去 Antd 中找到 Notification 组件

然后在 403 时做一个提示

router->index

import { Notification } from 'ant-design-vue' // 引入组件 } else if (to.path !== '/403') { // 如果权限不够直接去403,需要去新建一个403的页面和路由 Notification.error({ // 做一个403全局提示 message: '403', description: '没有访问权限,请联系管理员' }) 精细化权限控制(权限组件) 权限组件 我们采用函数式组件方式,这样性能更好,但是函数式组件跟template模板不是很友好,所以我们直接采用render方式渲染 components新建Authority组件

import { check } from “@/auth/index”;
import { constants } from “os”;
export default {
functional: true,
props: {
authority: {
type: Array,
required: true
}
},
// 解释一个函数式渲染,render函数有两个参数,一个式creatElement,包含了dom的信息,但是指向的是一个虚拟的dom
// context 则包含了该实例对象的各种属性
// 如果你用了权限校验的组件,那么将会做判断
render(creatElement, context) {
const { props, scopedSlots } = context; // 结构出参数和所有的插槽
// 如果校验通过则执行该组件内部的插槽组件,否则怎么也不做
return check(props.authority) ? scopedSlots.default() : null;
}
};

- 既然是权限校验,那么在整个项目钟肯定会出现多次,所以我们注册成全局组件 - main.js ```js // 引入权限组件 import Authority from "./components/Authority.vue"; // 全局注册 Vue.component("Authority", Authority); 此时经过测试 如:全局样式的抽屉,只有admin才能操作设置 layouts->BasicLayout.vue 此时你会发现只有admin时抽屉参会展示 至此:权限组件就Ok了 精细化权限控制(权限指令) 通过指令的方式来控制权限 新建指令仓库 directives用来存放各种自定义指令 directives->auth.js import { check } from "@/auth/index"; // 是否加载? function auth(Vue, options = []) { Vue.directive(options.name || "auth", { // 父级组件点调用时去判断 inserted(el, binding) { // 如果传过来的值,没有通过校验就移除节点 if (!check(binding.value)) { el.parentNode && el.parentNode.removeChild(el); } } }); } export default auth 然后去进行指令的全局注册 main.js // 引入指令 import auth from "./directives/auth"; // 注册全局指令 Vue.use(auth);

测试

layouts->BasicLayout.vue (collapsed = !collapsed)" />

至此我们通过路由,组件,指令三种方式来控制权限

注意:权限指令旨在第一次加载的时候有效果,如果动态的控制就会有问题

注意: 灵活度比较高,但是写法上稍微复杂度高一些

源码地址:git@github.com:sunhailiang/vue-public-ui.git

欢迎加微信一起学习:13671593005
未完待续…


作者:南麟剑首



part ui框架 路由 通用 前端ui框架

需要 登录 后方可回复, 如果你还没有账号请 注册新账号