乐趣区

关于vue.js:万字长文vueexpressmysql带你彻底搞懂项目中的权限控制附所有源码

本文略长,倡议珍藏。

线上网站演示成果地址:http://ashuai.work:8891/home

GitHub 仓库地址:https://github.com/shuirongsh…

所谓的权限,其实指的就是:用户是否能看到,以及是否容许其对数据进行增删改查的操作 ,因为当初开发我的项目的支流形式是前后端拆散,所以整个我的项目的权限是 后端权限管制 搭配 前端权限管制 独特实现的

后端权限

1. 晓得是哪个用户(角色)发的申请

  • token 判断(前端申请头中传过来,通过申请拦截器)
  • userid 判断(前端申请头中传过来,通过申请拦截器)
  • cookie 判断(用的少)
  • session 判断(用的很少)

2. 权限设计模式

RBAC 模式 ~ 基于角色的权限访问控制(Role-Based Access Control) 是商业系统中最常见的权限治理技术之一。

对于什么是 RBAC 模式 的概念本文这里不做赘述,大家能够这样了解:

2.1 RBAC 模式

  • 某个零碎有一些人在应用,然而应用这个零碎的这些人分为两个派别。有老板派(老板、老板小姨子、老板小舅子等),也有打工仔派(张三、李四、王二、麻子)。
  • 零碎有一些页面用于展现数据,作为老板派的相干工作人员,必定是哪个页面都可能看到,毕竟公司都是老板的,必须什么都能看到
  • 而作为打工仔派的工作人员,必定是权限比拟低了,只能看到一个业绩页面(假如业绩页面记录了本人每个月为公司发明的业绩)
  • 这个案例中的老板派和打工仔派就是两个角色,而老板、老板小姨子、老板小舅子和张三、李四、王二、麻子这些用户就是隶属于角色的用户
  • 用户是角色的具象化体现
  • 用户的数量个别都大于角色的数量的(想一下 …)
  • 所以咱们在做权限管制的时候,只须要管制角色能看到那个页面即可,至于用户就让他隶属于这个角色便是
  • 通过用户和角色进行关联,也做到了复用和解耦
  • 角色能看到哪个页面,取决于角色和菜单页面进行的关联(通过勾选菜单树关联)

步骤如下:

  • 给对应角色赋予(调配)相应菜单权限
  • 让新建的用户隶属于某个角色,就是给新建的用户调配一下角色

2.2 后端建表

失常状况下须要五张表:

  • 菜单表(用于存储系统的菜单信息,如菜单的名字、点击这个菜单要去跳转的路由 url、以及这个菜单的 icon 图标名、菜单 id 等 …)
  • 角色表(用于存储系统的角色信息,如角色名、角色 id、角色备注等 …)
  • 角色菜单表(用于存储某个角色能看到菜单 id 有哪些,一个角色 id 对应多个菜单 id,一对多关系)
  • 用户表(用于存储用户隶属于哪个角色,比方老板小舅子就是隶属于老板角色,以及用户名、用户 id、用户备注啥的 …)
  • 组织表(用于记录角色属于哪个组织,再大一些的零碎我的项目会建此表,小我的项目没有也行)

三张表也能用

咱们将角色菜单表,糅在角色表中,这样的话,只有新建菜单表、角色表(蕴含角色菜单信息)、用户表,依据用户的用户名和明码进行登录(后端会依据用户名和明码查问到这个用户隶属于那个角色下,从而返回此用户对应角色的菜单信息数据)

本文演示建两张表

为了更好的便于大家了解,本文只新建两张表,一张是菜单表,另一张是角色表(角色表中存此角色能看到的单 id),而登录的时候,大家抉择角色登录,其实抉择用户登录从某种意义上来说,也是相当于角色登录。

第一步:

角色登录发申请,后端返回此角色对应的菜单树数据(须要前端进一步加工一下),前端获取菜单树数据当前,将其存到 vuex 中比方是 menuTree 数组字段,el-menu组件再取到 vuex 中的 menuTree 数组数据应用,依据这个菜单树数据进行主动递归渲染

对于 el-menu 组件的递归自调用,能够参见笔者之前的这篇文章:https://segmentfault.com/a/11… 若对于组件递归常识忘了,能够参见笔者的这篇文章:https://segmentfault.com/a/11…

第二步:

第一步中,实现了 el-menu 组件的渲染展现,然而还少了路由表树结构数组数据(因为点击菜单须要进行路由跳转),所以咱们还须要再搞一份数据 routerTree,这个routerTree 数据是依据后端返回的菜单树数据,加工成合乎路由表树结构的数组,这个数组是给路由表应用的

  • routerTree的值是动静,因为不同角色的 routerTree 不一样
  • 对应的路由表还有动态的,不变的,比方 404 路由、login路由、首页 home路由
  • 动态路由前端间接写死即可,动静路由应用 router.addRoutes(routerTree) 将其增加到路由表中即可

至于刷新页面 vuex 中数据失落,就重发一次申请,或者本地存一份都行的

实际上,刷新页面,并不是 vuex 中的数据失落,而是,而是 vuex 中的数据初始化了(回到最后的样子,最后时,vuex 中的数据原本就是空的)

通过上述两步骤,一个权限零碎的根本样子构造就进去了,只不过还有一些细节须要解决,这个请持续往下浏览

前端权限细化管制

前端权限细化分类大抵能够分为四种:

  • 菜单的权限管制
  • 页面的权限管制
  • 按钮的权限管制
  • 字段的权限管制

1. 菜单的权限管制(以左侧导航菜单为例)

不同角色的用户登录当前,看到的是不同的菜单内容。比方:

  • 一般角色(的用户),只能看到某一个菜单
  • 管理员角色能看到所有的菜单
  • el-menu 菜单组件进行举例说明

2. 页面的权限管制

  • 角色没登录时,手动在地址栏输出 url 地址跳转,就强制用户跳转到登录页
  • 角色登录后,手动在地址栏输出不存在的url(或者本人不能看的url)地址跳转,让其跳转404 页面
  • 某些非凡的页面,还能够应用 vue-router 中的 beforeRouteEnter 再进行细化管制

假如打工仔张三,想要去看老板的页面,因为本人登录时,后端返回的菜单树中没有老板页面的数据,所以路由表中也没有老板页面的路由,所以地址栏 url 间接跳转时,就会跳转到一个不存在的路由页面,就不会显示出货色了。

对于路由,大家也能够这样了解:就是当地址栏中输出对应的 pathurl时,路由表大佬 去做对应匹配,匹配到了当前,再去渲染对应的 .vue 组件 从而出现对应的数据内容(匹配不到那就 404 呗)

3. 按钮的权限管制

按钮的管制,能够细分为两块:一是 : 是否能看到这个按钮、另外是 : 能看到不过是否能点击这个按钮(按钮是否禁用)

  • 按钮是否展现,取决于是否给角色调配了按钮权限。
  • 如有的只能看到 查看按钮 ,有的 增删改查按钮 都能够看到
  • 再一个,咱们能够把按钮当做是一个非凡的菜单页面
  • 最初,要有一个规定限度,新增节点时,能够新增菜单节点,也能够新增按钮节点,只不过按钮节点永远是最底层的地位,不能在按钮节点下再去新增页面(无意义操作)
  • 不了解下面那句话,请持续往下浏览

留神,在我的项目中最好不要应用禁用按钮去管制权限,如果角色用户没有某个按钮的权限,间接删除这个节点即可,比方v-if,或el.parentNode.removeChild(el),因为应用禁用按钮去管制权限存在肯定危险,如笔者的这篇文章:https://segmentfault.com/a/11…

非凡状况下,应用禁用按钮也能够去调配管制权限,具体情况具体分析

4. 字段的权限管制

如下的表格:

姓名 年龄 他乡 喜好
孙悟空 500 花果山 大闹天宫

比方年龄字段是隐衷,只有老板能看到员工的年龄,老板的小姨子和小舅子都不能看到,这个需要的实现能够前端依据角色 id 进行过滤;或者后端再建表做映射关系,返回给前端。

篇幅起因,这里不赘述了。后续闲暇了,笔者再写一篇介绍

后端建菜单表和角色表

咱们先从后端开始写,对于每个字段的意思,笔者在代码中提到了

1. 菜单表

两张表都在代码中,大家浏览完文章当前,能够在 GitHub 仓库中自行获取

1.1 菜单表数据库截图

这里有几个字段须要着重介绍一下:

1.2pid字段

pid字段,即为:parentId是父级节点字段,就是以后节点的父节点,后端在数据库中存储前端菜单树数据时,是不会存成树结构的数据的(JSON 模式除外,但这种形式很少用)后端存储的数据是:把树结构的数据 铺平(拍平)当前的数据。

比方咱们有这样一个树结构数据

let treeData = [
        {
                id: 1,
                name: '中国',
                children: [ // 有的后端喜爱应用 child 字段,一个意思
                        {
                                id: 3,
                                name: '北京',
                        },
                        {
                                id: 4,
                                name: '上海',
                                children: [
                                        {
                                                id: 6,
                                                name: '浦东新区'
                                        }
                                ]
                        },
                ]
        },
        {
                id: 2,
                name: '美国',
                children: [ // 有的后端喜爱应用 child 字段,一个意思
                        {
                                id: 5,
                                name: '纽约',
                        },
                ]
        },
]

数据库中可不会间接存一个树结构,数据库会把树结构拍平存起来,即这样存储:

pid id name
0 1 中国
1 3 北京
1 4 上海
4 6 浦东新区
0 2 美国
2 5 纽约

留神!数据库中不须要存储 children 字段,children字段是一个虚构字段,是当后端共事查问菜单表数据结构时,将扁平化的数据转成树结构时,递归代码创立的,并返回给前端。

实际上,后端共事能够间接将 sql 语句查问到的扁平化数据库数据数组间接丢给前端,至于加工成树结构,也能够由前端共事去加工,不过失常状况下,扁平化数据转成树结构数据都是后端做的,不过前端也要会写相应递归函数

也就是相当于这样的JSON

[
        {
                "pid": 0,
                "id": 1,
                "name": "中国",
        },
        {
                "pid": 1,
                "id": 3,
                "name": "北京",
        },
        {
                "pid": 1,
                "id": 4,
                "name": "上海",
        },
        {
                "pid": 4,
                "id": 6,
                "name": "浦东新区",
        },
        {
                "pid": 0,
                "id": 2,
                "name": "美国",
        },
        {
                "pid": 2,
                "id": 5,
                "name": "纽约",
        }
]

至于树结构如何拍平的,能够笔者写了两个递归函数大家能够看看,函数写法二的可读性更好一些哦

函数写法一:

// 拍平加 pid 字段                 
function pidFn(data, sqlArr = [], pid = 0) { // 假如顶级的 pid 为 0
        data.forEach((item) => { // 遍历失去每一项
                let obj = JSON.parse(JSON.stringify(item)) // 深拷贝一份
                obj.pid = pid // 给每一项都赋值 pid
                delete obj.children // 拍平不须要 children
                sqlArr.push(obj) // 丢到 sqlArr 数组中
                if (item.children) { // 有子节点就递归操作
                        pidFn(item.children, sqlArr, item.id) // 以后项的 id 就是子项的 pid
                }
        })
        return sqlArr // 一波操作,最初再丢进去即可
}
let res = pidFn(treeData)
console.log('拍平加父 id 字段', res);

函数写法二:

function pidFn(data) {let sqlArr = [] // 定义一个数组用于存储拍平后的数据
    function digui(data, pid) { // 专门定义个递归函数用户清晰的存储数据
            data.forEach((item) => { // 遍历树结构数据
                    let obj = JSON.parse(JSON.stringify(item)) // 深拷贝一份
                    obj.pid = pid // 给每一项都赋值 pid
                    delete obj.children // 拍平了,就不须要 children 了
                    sqlArr.push(obj) // 丢到 sqlArr 数组中
                    if (item.children) { // 如果树结构有子节点就持续递归操作
                            // 以后节点的 id 就是子节点的 pid
                            digui(item.children, item.id) // 递归函数接管树结构数据,当前 pid 参数
                    }
            })
    }
    digui(data, 0) // 递归函数首次执行,假如顶级 pid 是 0
    return sqlArr // 将递归的后果丢进来
}

let res = pidFn(treeData)
console.log('拍平加父 id 字段', res);

1.3pids字段

pids字段是所有的父级节点的组合的数组,比方一个三级节点,它的 pid 是二级节点的 id,而它的pids 是所有的父级节点,包含二级节点 id 和一级节点id

当然数据库存储,不能间接存一个数组进去,所以 toString() 转成字符串存储即可

1.4cUrl字段

cUrlcomponentUrl 组件的地址的意思。就是路由表中用于读取组件的 component 函数。比方有以下一个路由表:

{
    path: "/welcome",
    name: "welcome",
    component: resolve => require(["@/views/pages/welcome.vue"], resolve),
},

在数据库中存储为:

url cUrl
/welcome /pages/welcome.vue

示意:当地址栏的 url 值为 /welcome 时,去读取并渲染 @/views/pages/welcome.vue 文件,即做到了 url页面 的对应关系

其余的字段含意,看上述图片的正文即可了解

1.5 菜单表 sql 代码

DROP TABLE IF EXISTS `menus`;
CREATE TABLE `menus`  (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '惟一 id',
  `pid` int(11) NOT NULL COMMENT '下级父节点的 id,即为 parentId(留神,children 字段是不必存储,children 字段是递归时,增加进去的)',
  `pids` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '下级节点的 id 数组转的字符串',
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '树节点的名字',
  `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '即为菜单的 path',
  `cUrl` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '当拜访 url 时,前端路由须要读取并渲染的.vue 文件的门路,个别是绝对于 views 里的',
  `type` int(255) NULL DEFAULT NULL COMMENT 'type 为 1 是菜单,为 2 是按钮',
  `icon` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '菜单的图标名',
  `sort` int(255) NULL DEFAULT NULL COMMENT '菜单的高低排序',
  `status` int(255) NULL DEFAULT NULL COMMENT '是否开启字段,1 是开启,2 是敞开',
  `isHidden` int(255) NULL DEFAULT NULL COMMENT '是否暗藏菜单,1 是显示,2 是暗藏',
  `isCache` int(255) NULL DEFAULT NULL COMMENT '是否缓存,1 是缓存,2 是不缓存',
  `remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '备注',
  `isDel` int(255) NULL DEFAULT 1 COMMENT '删除标识,1 代表未删除可用,2 代表已删除不可用',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 105 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Compact;

2. 角色表

2.1 角色表数据库截图

2.2menuIds 字段

理论我的项目中,会有一个 角色菜单表 用于做角色和菜单的映射关系,这里笔者间接糅在一块了,便于大家了解。

2.3 角色表 sql 代码

DROP TABLE IF EXISTS `roles`;
CREATE TABLE `roles`  (`roleId` int(255) NOT NULL AUTO_INCREMENT COMMENT '每一个角色的 id',
  `roleName` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '每一个角色的 name 名字',
  `roleRemark` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '角色的备注',
  `menuIds` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL COMMENT '以后的这个角色能看到(勾选)的菜单的 id(给角色赋予菜单)',
  PRIMARY KEY (`roleId`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Compact;

前端应用角色去登录管制页面的权限

1. 登录页(抉择角色发申请获取对应菜单树数据)

1.1 效果图

1.2 代码思路

el-select点击下拉框

<el-select
v-model="value"
placeholder="请抉择角色登录"
@visible-change="
  (flag) => {if (flag) {listRoles();
    }
  }
"
>
    <el-option
      v-for="item in roleList"
      :key="item.value"
      :label="item.roleName +' ('+ item.roleRemark +')'":value="item.menuIds"
    >
    </el-option>
</el-select>

发申请获取角色列表有哪些

async listRoles() {const res = await this.$auth.listRoles();
  if (res.code == 0) {this.roleList = res.data;}
}

接口返回角色列表有以下数据

[
    {
        "roleId": 29,
        "roleName": "超级管理员",
        "roleRemark": "能看到所有",
        "menuIds": "1,19,21,42,30,31,41,22,32,33,27,34,39,40,35,36,37,38,100,101,99,25,26"
    },
    {
        "roleId": 30,
        "roleName": "前端",
        "roleRemark": "只能看前端相干",
        "menuIds": "1,19,21,42,30,31,41,22,32,33,27,34,39,40,35"
    },
    {
        "roleId": 31,
        "roleName": "后端",
        "roleRemark": "只能看后端相干",
        "menuIds": "1,36,37,38,100,101"
    }
]

当咱们抉择以超级管理员登录时,就依据超级管理员 roleId 为 29 对应的能看到的 menuIds 有哪些的菜单,去查问对应的菜单数据。

/auth/roleMenuByMenuId?menuIds=1,19,21,42,30,31,41,22,32,33,27,34,39,40,35,36,37,38,100,101,99,25,26

后端接口如下:

// 依据角色 id 查问能看到的菜单有哪些(前提是这些菜单是启用状态的)route.get('/roleMenuByMenuId', (req, res) => {
  // 1. 接管前端传的参数
  let menuIds = req.query.menuIds 
  // 2. 拼接 sql 语句筹备去数据库查问
  let sql = `SELECT * FROM menus WHERE id IN (${menuIds}) AND isDel = 1 AND status = 1`
  // 数据库连接池建设连贯去数据库中捞数据
  pool.getConnection(function (err, connection) {if (err) {throw err}
    connection.query(sql, function (error, results, fields) {connection.release()
      let apiRes = {
        code: 0,
        msg: "胜利",
        // 留神!留神!留神!// 这时候捞到的数据还是扁平化的数组
        // 须要将扁平化的数组转成树结构
        // 所以这里定义了一个 changeTree 函数用于加工数据
        data: changeTree(results, 0, 0) 
      }
      res.send(apiRes)
    })
  })
})

如果大家对于 express 写接口,做数据库查问忘了,能够看下笔者之前写的一篇全栈文章温习一下忘记的常识:https://segmentfault.com/a/11…

打印查到的 results 后果:扁平化数组数据截图:

所以须要有个函数办法(工具类),可能将扁平化构造转成树结构,函数如下:

/**
 * 想要将扁平化数组转成树结构,首先必须晓得顶级的 pid 是啥(0)* 第一步,假如咱们只须要找顶级的这一项,* 只须要比照一下那一项的 pid 是这个 pid 即可
 * 而后递归即可
 * */
// 通过 pid 将扁平化的数组转成树结构。给树结构增加 level 字段(数据库中没存,当然存也能够)module.exports = function changeTree(arr, pid, level) {let treeArr = []
    level = level + 1 // 增加层级字段
    arr.forEach((item, index) => {if (item.pid == pid) {
            // 把不是这一项的残余几项,都丢到这个 children 数组外面,再进行递归(这一项曾经确定了,是父级,没必要再递归了)let restArr = arr.filter((item, index) => {return item.pid != pid})
            item['children'] = changeTree(restArr, item.id, level) // 这里须要进行传 id 当做 pid(因为本人的 id 就是子节点的 pid)if (item.children.length == 0) {delete item.children} // 加一个判断,若是 children: [] 即没有内容就删除,不返回给前端 children 字段
            item['level'] = level // 增加层级字段
            // 操作完当前,把一整个的都追加到数组中去
            treeArr.push(item)
        }
    });
    return treeArr
}

调用:changeTree(results, 0, 0) 这里要告知顶级的 pid,这里我假如为0,同时level 层级从 第 0 层 开始

通过这样一加工再返回给前端,就是一个树结构的数据了,如下图:

这样的话,前端就能够应用了

因为业务简略,所以这里笔者的后端 express 没有分层解决

对于后端的分层,前端共事能够这样简略了解:

管制层
    接管参数
    校验参数
    解决参数
    .....
业务层 
    数据处理查、改什么的...
长久层
    生成 sql 语句
    数据库操作
    数据返回

2. 加工后端返回的菜单树数据存在 vuex 中以供应用

2.1 登录胜利跳转

async loginIn() {
  // vuex 中发相应的申请
  const res = await this.$store.dispatch("menu/tree_menu", this.value);
  if (res.code == 0) {
    // 存一份登录的角色名
    let i = this.roleList.findIndex((item) => {return item.menuIds == this.value;});
    sessionStorage.setItem("username", this.roleList[i].roleName);
    // 而后登录胜利去跳转
    this.$message({
      type: "success",
      message: "登录胜利",
    });
    this.$router.push({path: "/"});
  }
},

2.2vuex 中的操作~el-menu 须要的菜单树

  • 留神,上述图中,后端是返回了菜单树,然而!然而!仍旧是不能间接应用!
  • 起因是前端还须要再进行一次加工,将菜单树加工成两个树结构的数据
  • 一个是menuTree: [], // el-menu 须要的菜单树
  • 另一个是routerTree: [] // vue-router 须要的路由树
  • el-menu菜单树须要的 menuTree 的值是不蕴含按钮节点的,而因为按钮被当做了非凡的页面节点,所以这里须要过滤一下
  • vue-router须要的路由树 routerTree 也要依据后端返回的菜单树进行加工,加工成具备 component 函数属性值的路由树数据

咱们先看 el-menu 菜单树须要的 menuTree 数据的加工函数

// 加工后端返的树结构数据 是给菜单递归组件 el-menu 应用的
export function setElMenuTreeWithoutBtn(oldTree, newTree) {oldTree.forEach((item) => {
        let newTreeObj = {
            ...item,
            children: null
        }
        if (item.children) { // 有子集内容,且子集内容为菜单 type= 1 才去递归(按钮 type= 2 就不要了,这样就过滤了...)if (item.children[0].type == 1) {setElMenuTreeWithoutBtn(item.children, newTreeObj.children = [])
            }
        }
        if (newTreeObj.children == null) { // 为 null 阐明没有子集,或者子集都是按钮被忽略了,删除之
            delete newTreeObj.children
        }
        newTree.push(newTreeObj)
    })
    return newTree
}

调用此办法是在 vuex 中的登录逻辑中

const state = {
    isCollapse: false,
    menuTree: [], // el-menu 须要的菜单树
    routerTree: [] // vue-router 须要的路由树};

const mutations = {...}

const actions = {
    // 相当于 login 登录接口
    tree_menu({commit, dispatch}, menuIds) {return new Promise((resolve, reject) => {roleMenuByMenuId(menuIds)
            .then((res) => {let menuTree = res.data[0].children // 顶级节点 PC 不要
                    commit('TREE_MENU', setElMenuTreeWithoutBtn(menuTree, [])) // 加工菜单树给到 el-menu 组件应用
                    commit('ROUTE_TREE', setRouterTree(menuTree, [])) // 加工菜单树给到路由表应用
                    sessionStorage.setItem("token", "token"); // 模仿 token 存储
                    sessionStorage.setItem("menuIds", menuIds); // 存一份以后角色的 menuIds
                    setBtnAuth(menuTree) // 设置按钮
                    resolve(res) // 抛出去告知后果
                }
            ).catch(...)
        })
    }
}

最终加工好的 menuTree 树结构数据,会被 el-menu 组件应用:

留神,el-menu组件放在视图层组件中:

让咱们来看一下 layout 文件夹中的 index.vue 组件是如何应用 menuTree 数据的吧

layout-->index.vue

<el-menu
    ref="elMenu"
    :collapse="isCollapse"
    :default-active="activeIndex"
    class="elMenu"
    background-color="#333"
    text-color="#B0B0B2"
    active-text-color="#fff"
    :unique-opened="false"
    router
    @select="menuSelect"
  >
    <!-- 一般菜单(前端写死固定,如首页、欢送页等)-->
    <el-menu-item index="/">
      <i class="el-icon-s-home"></i>
      <span slot="title"> 首页 </span>
    </el-menu-item>
    <!-- 递归动静菜单(后端返回,不同角色菜单不统一)-->
    <myitem :data="menuArr"></myitem>
  </el-menu>
  
  import myitem from "./components/myitem.vue";
  components: {myitem}
  data(){
      return {menuArr: this.$store.state.menu.menuTree}
  }

再温习一下递归组件

layout-->components-->myitem.vue

<template>
  <div>
    <template v-for="(item, index) in data">
      <!-- isHidden 值等于 1 才去显示,等于 2 暗藏 -->
      <template v-if="item.isHidden == 1">
        <!-- 因为有子集和无子集渲染 html 标签不一样,所以要分为两种状况:状况一:有子集的状况:-->
        <template v-if="item.children">
          <!-- 有子集去递归显示 -->
          <el-submenu :key="index" :index="item.url">
            <template slot="title">
              <i class="el-icon-platform-eleme"></i>
              <span>{{item.name}}</span>
            </template>
            <myitem :data="item.children"></myitem>
          </el-submenu>
        </template>
        <!-- 状况二:没子集的状况 -->
        <template v-else>
          <!-- 没子集间接显示内容即可 -->
          <el-menu-item :key="index" :index="item.url">
            <i class="el-icon-eleme"></i>
            <span slot="title">{{item.name}}</span>
          </el-menu-item>
        </template>
      </template>
    </template>
  </div>
</template>

<script>
export default {
  name: "myitem",
  props: {
    data: {
      type: Array,
      default: [],},
  },
  // 留神:在 template 标签上应用 v -for,:key="index" 不能写在 template 标签上,因为其标签不会被渲染,会引起循环谬误
};
</script>

对于 组件的 name 属性 ,其实不写也行,不过当须要应用到组件的递归自调用或应用keep-alive 缓存组件 时,就得加上了。当然也有别的计划 …

到目前为止,菜单可能显示了,然而点击菜单却是空白页,因为少了路由表树结构数据了

2.3vuex 中的操作~vue-router 须要的路由树

路由表中的路由分为动态路由(固定路由)和动静路由(后端依据不同的用户 / 角色返回的路由),比方能够有以下的动态固定路由:

// 固定的动态路由,比方登录页、首页、404 页等...
const staticRoutes = [
  {
    path: '/',
    // component: resolve => require(["@/layout/index.vue"], resolve),
    component: Layout, // 二者一个意思
    redirect: '/home',
    children: [
      {
        path: "/home",
        name: "home",
        component: resolve => require(["@/views/home.vue"], resolve),
      },
    ]
  },
  {
    path: '/login',
    component: resolve => require(["@/views/login.vue"], resolve),
  },
  {
    path: '/404',
    component: resolve => require(["@/views/404.vue"], resolve),
  },
  // {path: '*', redirect: '/404'} 
]

这里有一个坑,404 页面的匹配兜底路由 在最初,然而不能间接写在动态路由中的最初一个,会刷新主动到 404 页面了。因为是动静路由的做法,所以 404 页面的匹配兜底路由 拼接在 vue-router 的路由树数组数据中即可。所以上述的 {path: '*', redirect: '/404'} 笔者正文掉了。

登录获取到后端返回的树结构数据后,加工,给路由表应用

// vuex 中
roleMenuByMenuId(menuIds).then((res) => {let menuTree = res.data[0].children // 顶级节点 PC 不要
        commit('ROUTE_TREE', setRouterTree(menuTree, []))
    }
)

setRouterTree加工函数

// 加工后端返的树结构数据 是给 vue-router 路由表应用
import Layout from '@/layout/index.vue'
let page404 = {path: '*', redirect: '/404'}
export function setRouterTree(oldTree, newTree) {oldTree.forEach((item) => {
        let newTreeObj = {path: item.level == 2 ? `/${item.url}` : `${item.url}`,
            name: item.name,
            component(resolve) {require([`@/views${item.cUrl}`], resolve) },
            meta: {title: item.name}
        }
        if (item.level == 2) { // 如果是二级,就对立应用 layout 组件视图层
            // newTreeObj['component'] = Layout // 这两个一个意思
            newTreeObj['component'] = (resolve) => {require(["@/layout/index.vue"], resolve)
            }
        }
        if (item.children) {if (item.children[0].type == 1) { // 路由表树结构也不须要按钮哦,只有 type 等于 1 的菜单
                setRouterTree(item.children, newTreeObj.children = [])
            }
        }
        newTree.push(newTreeObj)
    });
    return newTree.concat(page404) // 将 404 页面拼接到最初面,做通配路由应用
}

打印加工好的路由树后果:

留神箭头指向的中央,component函数要去援用指向解析组件哦,这个肯定要有

所以 commit('ROUTE_TREE', setRouterTree(menuTree, [])) 的值给到路由表去应用

那么?路由表如何应用加工好的这个路由表树数据呢?应用 vueaddRoutes办法增加即可,在登录胜利当前。加工好当前,间接增加即可

import router from "@/router";
import store from "@/store";

async loginIn() {const res = await this.$store.dispatch("menu/tree_menu", this.value);
      if (res.code == 0) {
        /**
         * 登录胜利当前也要动静增加一下路由 tip1,或者异步重加载一下 tip2
         * 否则会呈现首次登录 router.beforeEach 中动静路由办法不触发
         * 即首次登录点击动静菜单局部呈现空白页(刷新后失常)* */
        router.addRoutes(store.state.menu.routerTree); // tip1: 注掉成果显著
        let i = this.roleList.findIndex((item) => {return item.menuIds == this.value;});
        sessionStorage.setItem("username", this.roleList[i].roleName);
        this.$message({
          type: "success",
          message: "登录胜利",
        });
        this.$router.push({path: "/"});
        // setTimeout(() => { // tip2: 注掉成果显著
        //   location.reload();
        // }, 10);
      }
    },

最初一步,刷新页面时,vuex初始化,所以在 beforeEach 钩子函数中从新发申请,获取菜单树数据即可

// 路由全局拦挡
router.beforeEach((to, from, next) => {
  // 去登录页面必定放行的,管他有没有 token
  if (to.path === '/login') {next()
  }
  // 去的不是登录页面,再看看有没有 token 认证
  else {const token = sessionStorage.getItem('token')
    if (!token) { // 没 token,就让其回到登录页登录
      next({path: "/login"})
    } else { // 有 token,再看看有没有菜单路由信息
      if (store.state.menu.routerTree.length > 0) { // 有菜单信息,就放行
        next()} else { // 没有菜单信息,就再发一次申请获取菜单信息
        let menuIds = sessionStorage.getItem('menuIds')
        store.dispatch('menu/tree_menu', menuIds).then((res) => {if (res.code == 0) {router.addRoutes(store.state.menu.routerTree)
            next({...to, replace: true}) // 确保动静路由已被齐全增加进去了
          }
        })
      }
    }
  }
})

前端应用角色去登录管制按钮的权限

按钮的权限思路,在登录时,依据菜单树转成一个按钮树,有按钮的权限,就为 true,没有就间接没有。

而后再定义函数去获取某某页面下的某某按钮的值是否为 true,为 true 就阐明有权限,为 false 就阐明没有权限,能够应用函数引入形式,或者自定义指令形式。

将菜单树的按钮转成 key/value 布尔值按钮树模式

指标按钮树结构

{
    "前端框架": {
        "vue 页面": {
            "新增 vue": true,
            "编辑 vue": true,
            "删除 vue": true,
            "占位按钮": true
        },
        "react 页面": {
            "新增 react": true,
            "编辑 react": true
        },
        "angular": {
            "agl1": {
                "agl1 新增 / 编辑": true,
                "agl1 删除": true
            },
            "agl2": {}}
    },
    "后端框架": {"springBoot": {},
        "myBatis": {},
        "特工 001": {},
        "特工 002": {}},
    "零碎设置": {"角色治理": {},
        "菜单治理": {}}
}

定义函数去设置

仍旧是在登录的时候,去依据菜单树去设置按钮树

roleMenuByMenuId(menuIds).then((res) => {let menuTree = res.data[0].children // 顶级节点 PC 不要
            //......
            setBtnAuth(menuTree) // 设置按钮树
            resolve(res) // 抛出去告知后果
        }
    )

设置按钮树递归函数

// 按钮权限设置
export function setBtnAuth(tree, btnAuthObj = {}) {
    // 循环加工
    tree.forEach((item) => {if (item.type == 1) { // 类型为 1 阐明是菜单
            btnAuthObj[item.name] = {} // 菜单就加上一个对象属性
            if (item.children) { // 若有子集,就递归加对象属性
                // 因为对象是援用类型,所以间接赋值整个按钮权限对象就都有了
                setBtnAuth(item.children, btnAuthObj[item.name])
            }
        }
        if (item.type == 2) { // 类型为 2 阐明是按钮
            btnAuthObj[item.name] = true // 按钮的赋值 true 示意有按钮权限
        }
    })
    // 加工结束当前,丢进来以供应用
    sessionStorage.setItem('btnAuthObj', JSON.stringify(btnAuthObj))
    return btnAuthObj
}

获取按钮树递归函数

// 按钮权限获取
export function getBtnAuth(whichPage, btnName) { // 查找:那个页面下的什么按钮名字是否有权限
    let flag // 找到了,才阐明有权限
    function getBtn(whichPage, btnName, btnAuthObj = JSON.parse(sessionStorage.getItem('btnAuthObj'))) {for (const key in btnAuthObj) {if (key == whichPage) {flag = btnAuthObj[key][btnName]
            } else {getBtn(whichPage, btnName, btnAuthObj[key])
            }
        }
    }
    getBtn(whichPage, btnName) // 递归查找标识赋值
    return flag ? true : false // 找到了为 true,没找到 undefined,这里再判断一下,返回布尔值
}

函数应用形式

<el-button type="danger" v-if="isShowDeleteBtn"> 删除 vue</el-button>

computed: {isShowDeleteBtn() {return getBtnAuth("vue 页面", "删除 vue");
    },
  },

自定义指令形式

<el-button type="primary" v-btn="{vue 页面:' 新增 vue'}"> 新增 vue</el-button>
<el-button type="primary" v-btn="{vue 页面:' 编辑 vue'}"> 编辑 vue</el-button>

自定义指令 v -btn

import {getBtnAuth} from "@/utils";
export default {inserted(el, binding, vnode) {const whichPage = Object.keys(binding.value)[0]
        const btnName = Object.values(binding.value)[0]
        let flag = getBtnAuth(whichPage, btnName)
        if (!flag) {el.parentNode.removeChild(el)
        }
    },
    unbind(el, binding, vnode) {}}

相当于去查问,查 某个页面 中有没有 某个按钮

共用的动静路由组件

比方好几个页面,都是同一个构造内容,只是 id 不同,这个时候就须要应用动静路由组件了传参

/dynaOne/:001

<template>
  <div>
    <span> 共用的动静组件:</span>
    <h2>
      依据 <span class="val">{{val}}</span> 的不同发相应申请
    </h2>
  </div>
</template>

<script>
export default {data() {
    return {val: Object.values(this.$route.params)[0].slice(1),
      /*
        留神 url 的配置:思路是多个路由 path 应用同一个组件,须要加上监听,解决路由变了,组件不刷新的问题
            身份标识 id 间接 url 中拼接即可应用
            url:'xxx/:001'
      */
    };
  },
  watch: {
    $route: {handler: function () {this.val = Object.values(this.$route.params)[0].slice(1);
      },
    },
  },
};
</script>

<style>
.val {
  color: brown;
  font-size: 36px;
  margin: 0 8px;
}
</style>

总结

  • 通过这种形式,就能够做到动静菜单得了。
  • 然而纸上得来终觉浅,还是得实操
  • 又因为篇幅起因,笔者对于一些细节文章中没有提到
  • 所以大家能够去笔者公布出的网站上看成果
  • 以及 star 一下笔者的仓库
  • 将代码拉下来跑起来,一点点捋清代码思路
  • 这样印象才会粗浅

比方后端接口如何写,前端菜单树 el-tree 勾选传参细节问题,详情,请看代码

另外,后盾管理系统,都有 tab 标签页,动静路由的 tab 标签页 在三层菜单中会呈现 缓存问题,后续笔者不忙了,会持续更新这个仓库的哦。

等等 …

大家能够随便新增角色,并赋予菜单权限,不过不能编辑菜单数据哦。笔者为了升高本人的数据不被打乱了

如果帮到了您,还请赏赐一个star,也是咱更文的能源。(PS: 写文章真的要花费不少工夫哦😭)

退出移动版