谈谈如何设计一个友好的权限管理模块(下)

现在,我们应该拿到了一个获取菜单列表的接口。

我们在store的user模块里,加一个获取菜单的方法:

 GetUserMenuList({ commit }) {
      return new Promise((resolve, reject) => {
        getUserMenu().then(
          response => {
            resolve(response.data)
          }
        ).catch(error => {
          reject(error)
        })
      })
    },

在menu模块,我们设置一个menuList来全局存储menu列表:

const state = {
    menuList: undefined,
}
const mutations = {
    SET_MENU_LIST: (state, result) => {
        state.menuList = result;
    },
}
const actions = {
    setMenuList({ commit, state }, menuSourceList) {
        commit('SET_MENU_LIST', menuSourceList)
    }
}
export default {
    namespaced: true,
    state,
    actions,
    mutations
}

首先, 用户登录之后,会将获取的Token传入setToken方法,token是存储在cookie里面,这里使用的是很常用的js-cookie。

import Cookies from 'js-cookie'

const TokenKey = 'Admin-Access-Token'
const Username = 'Username'

export function getToken() {
  return Cookies.get(TokenKey)
}

export function setToken(token) {
  return Cookies.set(TokenKey, token, { expires: 7 })
}

export function removeToken() {
  return Cookies.remove(TokenKey)
}

然后,在每次进入路由之前,我们需要判断是否有权限进入:

router.beforeEach((to, from, next) => {
  if (to.path === '/login') {
    if (getToken()) {
      next('/dashboard')
    } else {
      next()
    }
  } else if (to.path === '/401' || to.path === '/404' || to.path.includes('others')) {
    next()
  } else if (!getToken()) {
    next('/login')
  } else {
    const handleAuth = (source, current) => {
      if (judgeCanActivate(source, current)) {
        next();
      } else {
        next('/401')
      }
    }
    if (store.state.menu.menuList) {
      handleAuth(store.state.menu.menuList, to.path);
    } else {
      store.dispatch('GetUserMenuList').then((userMenuSource) => {
        store.dispatch('menu/setMenuList', userMenuSource);//初始化菜单栏
        handleAuth(userMenuSource, to.path);
      })
    }

  }
})

这里前面几种情况是我这里比较常见的情况,比如没有token,就跳转到登录页面;如果输入login路由,但是有Token就跳转到 Dashboard页面;如果是401,404,others页面,就不做权限判断。

然后是其他的情况了,都是要做判断的,首先,我们通过store.state.menu.menuList可以知道,是否应用已经缓存了menuList全局变量,如果没有,我们就需要调用后端接口。然后judgeCanActivate是我这边判断的方法,这个方法目前是一个比较粗陋的版本,需要后面再重构一下:

const judgeCanActivate = (source, current) => {
  let flatMenuList = [];
  for (let index = 0; index < source.length; index++) {
    const menu = source[index];
    if (menu.type === 1 && menu.route) {
      flatMenuList.push(menu);
      if (menu.children && menu.children.length) {
        menu.children.forEach(
          m => {
            if (m.type === 1 && m.route) {
              flatMenuList.push(m)
            }
          }
        )
      }
    }
  }
  let currentRoutePath = current.split('/');
  const activeMenu = flatMenuList.findIndex(
    (fm) => {
      return fm.route === currentRoutePath[currentRoutePath.length - 1];
    }
  )
  return activeMenu >= 0;
};

这里的逻辑也很简单,就是通过判断menu数组里的route字段是否包含当前路由最后的单词(因为路由有父子)。

这样,不包含的就认为没有权限,会跳转到401页面。

 我们使用store.dispatch('menu/setMenuList', userMenuSource);来设置来全局数据menuList,然后在渲染菜单栏的页面,我们可以获取到它:

 computed: {
    ...mapState({
      menuList: state => state.menu.menuList,
      taskSearchName: state => state.app.taskSearchName
    }),
    
  },

然后我们就开始动态的渲染菜单栏。

新建一个MenuBarItem的vue文件:

<template>

  <div v-if="item.type===1&&item.visible">

    <template v-if="hasNoChildMenu(item.children,item)">
      <el-menu-item :index="`${basePath}/${item.route}`">
        {{$t(`menu.${item.route}`)}}
      </el-menu-item>
    </template>

    <el-submenu v-else :index="(item.route)" :popper-append-to-body="false" :ref="item.route">
      <template slot="title">
        {{$t(`menu.${item.route}`)}}
      </template>

      <template v-for="child in item.children">
        <menu-bar-item v-if="child.children&&child.children.length" :item="child" :is-nest="true" :key="child.route"
          :base-path="`/${item.route}`" />
        <el-menu-item v-else :index="`/${item.route}/${child.route}`" :key="child.route">
          {{$t(`menu.${child.route}`)}}
        </el-menu-item>
      </template>
    </el-submenu>

  </div>

</template>

<script>
export default {
  name: "MenuBarItem",

  props: {
    item: {
      type: Object,
      required: true
    },
    isNest: {
      type: Boolean,
      default: false
    },
    basePath: {
      type: String,
      default: ""
    }
  },
  methods: {
    hasNoChildMenu(children, parent) {
      if (!children) {
        return true;
      }
      const showingChildren = children.filter(item => {
        return item.visible;
      });
      if (showingChildren.length >= 1) {
        return false;
      } else {
        return true;
      }
    }
  }
};
</script>

basePath就是父级路由的地址。

代码看上去很简洁。hasNoChildMenu方法来判断是否有子菜单,如果有,会渲染<el-submenu>元素,注意这里有一个递归的算法,如果子元素还有子元素,会渲染自身的menu-bar-item元素,否则就是el-menu-item元素。

这样子不管下面有多少层children,都可以高效的渲染出来。

然后在最外面使用整个组件:

      <menu-bar-item v-for="menu in menuList" :key="menu.path" :item="menu" :base-path="''" />

这里会遇到一个BUG,因为我们采用组件渲染菜单栏,那么el-menu-item外面多了一层div,导致Element默认的菜单样式不生效了,解决方法也很简单,就是把样式自定义,但是加上div,比如:


.el-menu--horizontal>div>.el-menu-item,
.el-menu--horizontal>div>.el-submenu .el-submenu__title {
  height: 64px !important;
  line-height: 64px !important;
  &:hover {
    background-color: rgba(255, 255, 255, .2) !important;
  }
}

但是水平方向菜单,每个menu-item一直占用整个屏幕宽度,将其设置为flex布局:

.el-menu--horizontal {
  display: flex;
}

现在,菜单栏就会根据接口动态的渲染,输入地址也会根据权限判断是否可以进入。而且我们还拿到了每个页面的具体按钮功能的权限,至于在页面里面如何隐藏按钮,那都不是事儿。

你也可以把前端这一套搬到Angular里,只要把router.beforeEach方法改为canActivate()里面,store的数据管理改为rxJS即可。

所以权限管理模块本身就很简单,关键在于设计要足够灵活,动态。现在我们这样一套设计的权限管理逻辑,足够的简单,足够的强大。

 

发布了49 篇原创文章 · 获赞 27 · 访问量 13万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章