談談如何設計一個友好的權限管理模塊(下)

現在,我們應該拿到了一個獲取菜單列表的接口。

我們在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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章