vue router 根据路由区分用户权限并生成动态侧边栏

5,119 阅读4分钟

这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战

背景

在管理后台系统中,vue-router 往往要承担更多的事情。

因为在管理后台,一个非常重要也非常常见的功能,就是用户权限的区分。一般用户,管理用户,超级管理员,审计用户等等。

针对不同的用户,既要做接口的检验,也要对页面的权限进行划分。

比如:

  • 超级管理员拥有所有的权限,能查看所有的页面,能进行所有的操作
  • 管理用户,则不能添加,删除,变更系统里的用户
  • 审计用户,只能查看需要审计的内容,如系统操作日志等

以上种种,其实都是跟页面的展示,路由的访问,接口的校验强相关的。当前用户如果是审计用户,那么只能展示某些页面,如果是管理用户,那要隐藏某些页面。

另外,管理项目都有一套侧边栏作为不同功能页面的入口。

假设我们的路由和侧边栏是分开的逻辑,那么就有以下的问题:

  • 每次更新一次路由,就要更新一次侧边栏,
  • 路由中做了权限,侧边栏还要单独加一个权限管理的逻辑

这样会显得我们前端开发很呆板。

所以总结来说,vue-router 要实现这样的功能

  • 挂载组件到页面
  • 根据不同的用户角色,如:超级管理员,审计用户,普通用户等来做路由权限管理
  • 根据路由生成复杂的侧边栏

那么,如何使用一套定义好的 router 来做到以上三个需求呢?

分析

首先,我们来细致的分析一下这三个需求有哪些具体需要

挂载组件到页面中

想必不用多说了,vue-router 本来就是做这个的

区分不同用户角色

用户角色一般是保存在数据库中,用户登录之后由后台的接口返回的,比如这个字段是user_role,那么这里怎么实现动态的路由区分呢?

  • 首先,我们可以把这个字段存储在 vuex 中,这样可以做到全局访问,全局自动更新
  • 其次,在 vue-router 的每一个路由对象中插入一个字段,比如叫permission,来记录当前这个路由能够被谁访问,如permission: [0,1,2]
  • 根据user_rolepermission来匹配筛选所有的路由,筛选后的结果,就是当前用户能够访问的路由
  • 使用 vue-router 的方法将得到的路由添加到项目的路由对象里,地址

生成复杂的侧边栏

侧边栏是管理后台都有的功能,这里参考 vue-element-admin 的实现。 本质上 router 是一个javascript 的数组对象,遍历这个对象,利用 elementui 的 el-menu 组件来生成侧边栏就可以了。

如果 router 变化了 那么侧边栏也会随之变化。

实现

我们先定义一个整体的 router 对象,这个对象是整个项目里所有的页面路由,也是超级管理员默认的访问权限,这个对象的每一个route 对象,都可以含有如下的属性或者方法:

  • hidden: 是否显示在侧边栏
  • name: 侧边栏对应的文字内容,当name不存在,则children中的路由为一级路由,否则为二级路由,侧边栏最多显示到二级路由
  • iconName:侧边栏的 icon 图标
  • permission:可以访问辞路由的用户列表
// routerData.js
const router1 = [
  {
    path: '/',
    redirect: '/homepage',
    hidden: true, 
    meta: { permission: [1, 2, 3] }
  },
  {
    path: '/homepage',
    meta: { permission: [1, 2, 3] },
    redirect: '/homepage/dashboard',
    component: layout,
    children: [
      {
        path: 'dashboard', 
        name: '安全状态监控',
        meta: { iconName: 'icon-erji-loudongsaomiaoguanli', permission: [1, 2, 3] },
        component: () => import('../views/dashboard/index.vue'),
      },
    ]
  },
  {
    path: '/assetManagement',
    name: '资产管理',
    meta: { iconName: 'icon-erji-loudongsaomiaoguanli', permission: [1, 2, 3] },
    redirect: '/assetManagement/assetList',
    component: layout,
    children: [
      {
        path: 'assetList',
        name: '资产列表',
        meta: { permission: [1, 2, 3] },
        component: () => import('../views/assetManagement/assetList.vue'),
      },
      {
        path: 'assetDetail',
        name: '资产详情',
        meta: { permission: [1, 2, 3] },
        component: () => import('../views/assetManagement/assetList.vue'),
      },
    ]
  },
  
  { path: '*', redirect: '/', hidden: true, meta: { permission: [1, 2, 3] }, }
]
export commonRoute = [{ 
    path: '/login', 
    component: () => import('@/views/login.vue'), 
    hidden: true, 
    meta: { permission: [1, 2, 3] },  
}]
    
export default router1

然后我们在 index.js 中实现 router 的挂载

// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import { commonRoute } from './routerData'
import layout from '@/layout'

Vue.use(VueRouter)

export const permissionRouter = function(router, roleType) {
  return router.filter(item => {
    if(item.children)
      item.children = permissionRouter(item.children)
    return item.meta.permission.includes(roleType)
  })
}

export default new VueRouter({
  commonRoute
})

当用户登录后,我们先获取用户的角色,然后放到 vuex 中存储后,就可以根据用户角色,动态的计算出权限路由了

import store from '@/store'
import router1  from '@/router/routerData'
import router, {permissionRouter} from '@/router/index'

const roleType = store.getters.user_role
const permissionRouterList = permissionRouter(router1, roleType)

router.addRoute(permissionRouterList)

到这里整个 router 就写好了,router 接下来就可以准备动态的生成侧边栏,侧边栏逻辑复杂一点,所以我也是拆成了三个文件

这是文件的目录:

sidebar
├── index.vue
├── item.vue
├── sidebarItem.vue

然后来实现动态侧边栏的逻辑

// index.vue
<template>
  <div class="sidebar-container">
    <el-menu :unique-opened="true"
             :collapse-transition="false"
             background-color="rgba(0,0,0,0)"
             :default-active="activeMenu">
      <sidebar-item v-for="(item, index) in permissionRouterList"
                    :key="index"
                    :item="item" />
    </el-menu>
  </div>
</template>
<script>
import store from '@/store'
import router1  from '@/router/routerData'
import {permissionRouter} from '@/router/index'
import SidebarItem from './sidebarItem.vue'
export default {
  components: {
    SidebarItem
  },
  computed: {
    activeMenu() {
      return this.$route.path
    },
    permissionRouterList(){
        const roleType = store.getters.user_role
        return permissionRouter(router1, roleType)
    }
  }
}
</script>
// item.vue
<template>
  <el-menu-item :index="compilePath(child.path)"
                @click="clickEvent(child)"
                v-if="!child.hidden">
    <i v-if="child.meta"
       class="iconfont"
       :class="child.meta.iconName"></i>
    <span slot="title">{{child.name}}</span>
  </el-menu-item>
</template>

<script>
import path from 'path'
export default {
  props: {
    child: {
      type: Object
    },
    basePath:{
      type: String
    }
  },
  methods: {
    compilePath(childpath) {
      return path.resolve(this.basePath, childpath)
    },
    clickEvent(child) {
      const path = this.compilePath(child.path)
      this.$router.push(path)
    }
  }
}
</script>
// sidebarItem.vue
<template>
  <el-submenu v-if="(item.children && hasName(item)) && !item.hidden"
              :index="item.path">
    <template slot="title">
      <i v-if="item.meta"
         class="iconfont"
         :class="item.meta.iconName"></i>
      <span slot="title">{{item.name}}</span>
    </template>
    <menu-item v-for="child in item.children"
               :key="compilePath(item.path,child.path)"
               :child="child"
               :basePath="item.path" />
  </el-submenu>
  <menu-item :child="item.children[0]"
             v-else-if="item.children && !item.hidden"
             :basePath="item.path" />
</template>

<script>
import path from 'path'
import MenuItem from './item.vue'
export default {
  props: {
    item: {
      type: Object
    }
  },
  components: {
    MenuItem
  },
  methods: {
    compilePath(itempath, childpath) {
      return path.resolve(itempath, childpath)
    },
    hasName(item){
        return item.name? true: false
    }
  }
}
</script>

这样,我们的整个需求就完成了,然后将 sidebar 组件正常引入,加入自己的样式,就可以了

在这里插入图片描述

最后的最后,代码未经测试,有任何问题请私信或评论,谢谢。