記一次vue-element-admin 的動態路由許可權管理和選單渲染的學習

HTML小白發表於2020-03-10

三人行必有我師,今天來記錄一次 對 vue-element-admin 中 動態路由和動態選單渲染的理解,只是學習,不對之處,還請見諒

現實的工作中,尤其是寫後臺管理系統的時候,經常會涉及到不同的使用者角色擁有不同的管理許可權,那麼久需要對應不同的管理者進行不同的動態路由設定和導航選單渲染,這在後臺管理中,變成了一個迫切需要解決的問題,今天久記錄一次對vue-element-admin中這一塊學習的的理解

不會寫文章:那就讓程式碼來說明問題吧! 不喜歡看程式碼的見諒哈,發個地址: github.com/cgq001/admi…
大家相互學習哈,我也是模仿的 哈哈, 不需要星星,純屬娛樂 哈哈 無聊罷了

一.路由設計

1.routee/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'

import Layout from '@/layout' //佈局頁

import Home from '../views/Home.vue'

Vue.use(VueRouter)

// 通用頁面, 這裡的配置不需要許可權
export const constRouter = [
  {
      path: '/login',
      component: () => import('@/views/login/Login'),
      hidden: true //導航選單忽略選項
  },
  {
      path: '/',
      component: Layout, //應用佈局頁
      redirect: '/home',
      meta:{
        title: '佈局',
        icon: 'wx'
      },
      children: [
          {
              path: 'home',
              component: () => {
                  import('@/views/Home.vue')
              },
              name: "Home",
              meta:{
                  title: "Home", //導航選單項標題
                  icon: 'qq' //導航選單圖示
              }
          }
      ]
  }
]

// 動態路由
export const asyncRoutes = [
  {
    path: '/about',
    component: Layout,
    redirect: '/about/index',
    meta:{
      title: "關於",
      icon: 'wx'
    },
    children: [
      {
        path: 'index',
        component: () => import('@/views/about/About.vue'),
        name: 'about',
        meta: {
          title: "About",
          icon: 'qq',
          roles: ['admin','editor']  //角色許可權配置
        }
      }
    ]
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: constRouter
})

export default router
複製程式碼

2.全域性導航守衛

需要在main.js中引入全域性守衛

1.main.js
// 全域性路由守衛
import './router/permission'
複製程式碼
2.router/permission
// 路由的全域性首位

// 許可權控制邏輯
import router from './index'
import store from '../store/index'

import { Message } from 'element-ui'
import { getToken } from '@/utils/auth' // 從cookie獲取令牌

const whiteList = ['/login'] //排除的路徑

router.beforeEach(async (to,from,next) => {

    // 獲取令牌判斷使用者是否登陸
    const hasToken = getToken()
    // 有令牌 表示已經登陸
    if(hasToken){
        if(to.path === '/login'){
            // 已登入重定向到首頁
            next({path: '/'})
        }else{
            //若使用者角色已附加則說明動態路由已經新增
            const hasRoles = store.getters.roles && store.getters.roles.length > 0

            if(hasRoles){
                //角色存在
                next() //繼續即可
            } else {
                try {
                    //先請求獲取使用者角色
                    const { roles } = await store.dispatch('user/getInfo')
                
                    // 根據當前使用者角色動態生成路由
                    const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
                   
                    // 新增這些路由至路由器
                    router.addRoutes(accessRoutes)

                    // 繼續路由切換,確保addRoutes完成
                    next({...to,replace: true})
                } catch(error){
                    // 出錯需要重置令牌並重新登陸(令牌過期,網路錯誤等原因)
                    await store.dispatch('user/resetToken')
                    Message.error(error || "網路錯誤")
                    next(`/login?redirect=${to.path}`)
                }
            }
        }
    }else{
        // 使用者無令牌
        if(whiteList.indexOf(to.path) !== -1){
            //白名單路由放過
            next()
        } else {
            // 重定向至登入頁
            next(`/login?redirect=${to.path}`)
        }
    }
})
複製程式碼

3.1store/index

import Vue from 'vue'
import Vuex from 'vuex'
import permission from './modules/permission'
import user from './modules/user'
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
    permission,
    user
  },
  // 定義全域性getters 方便訪問user 模組的roles
  getters:{
    roles: state => state.user.roles,
    permission_routes: state => state.permission.routes
  }
})
複製程式碼

3.1store/modules/user

import { getToken, setToken, removeToken } from '@/utils/auth'

// 村赤使用者令牌和角色資訊
const state ={
    token: getToken(),
    roles: [] //角色
}

const mutations ={
    SET_TOKEN: (state,token) => {
        state.token = token;
    },
    SET_ROLES: (state,roles) => {
        state.roles = roles;
    }
};

const actions = {
    // 使用者登入
    login({ commit }, userInfo) {
        const { username } = userInfo;
        return new Promise((resolve,reject) => {
            setTimeout(() => {
                if(username === 'admin' || username === 'jerry'){
                    // 儲存狀態
                    commit('SET_TOKEN',username);
                    // 寫入cookie
                    setToken(username)
                    resolve()
                }else{
                    reject('使用者名稱或密碼錯誤')
                }
            },1000)
        })
    },
    // 獲取使用者角色資訊
    getInfo({ commit, state }){
        return new Promise((resolve) => {
            setTimeout(() => {
                const roles = state.token === 'admin' ? ['admin'] : ['editor']
                commit('SET_ROLES',roles)
                resolve({roles})
            },1000)
        })
    },
    // 重置令牌
    resetToken({ commit }){
        return new Promise(resolve => {
            commit('SET_TOKEN','')
            commit('SET_ROLES',[])
            removeToken();
            resolve()
        })
    }
}

export default {
    namespaced: true,
    state,
    mutations,
    actions
}
複製程式碼

3.1store/modules/permission

// 許可權管理模組
import { asyncRoutes, constRouter } from '@/router'

/**
 * 根據路由meta.role 確定是否當前使用者擁有訪問許可權
 * @roles 使用者擁有角色
 * @route 待判定路由
 * 
 * 
 * 
*/
function hasPermission (roles,route){
    // 如果當前路由有roles 欄位則需要判斷使用者訪問許可權
    if(route.meta && route.meta.roles){
        // 若使用者擁有的角色中有被包含在待定路由角色表中的則擁有訪問許可權
        return roles.some(role => route.meta.roles.includes(role))
    } else{
        // 沒有設定roles 則無需判定即可訪問
        return true
    }
}

/**
 * 遞迴過濾AsyncRoutes路由表
 * @routes 待過濾路由表,首次傳入的就是AsyncRoutes
 * @roles 使用者擁有角色
 * 
*/
export function filterAsyncRoutes(routes,roles){
    const res = []
    routes.forEach(route => {
        // 複製一份
        const tmp = { ...route}
        // 如果使用者有訪問許可權則加入結果路由表
        if(hasPermission(roles,tmp)){
            // 如果存在子路由則遞迴過濾之
            if(tmp.children){
                tmp.children = filterAsyncRoutes(tmp.children,roles)
            }
            res.push(tmp)
        }
    })
    return res;
}

const state = {
    routes: [], //完整路由表
    addRoutes: []  //使用者可訪問路由表
}

const mutations = {
    SET_ROUTES: (state, routes) => {
      
        // routes 使用者可以訪問的許可權
        state.addRoutes = routes
        // 完整的路由表
        
        state.routes = constRouter.concat(routes)
       
    }
}

const actions = {
    generateRoutes({ commit }, roles) {
        return new Promise(resolve => {
            
            let accessedRoutes;
            // 使用者是管理員則擁有完整訪問許可權
            if(roles.includes('admin')){
                accessedRoutes = asyncRoutes || []
            }else{
                //  否則需要根據使用者角色做過濾處理
                accessedRoutes = filterAsyncRoutes(asyncRoutes,roles)
            }
           
            commit('SET_ROUTES',accessedRoutes)
       
            resolve(accessedRoutes)
        })
    }
}

export default {
    namespaced: true,
    state,
    mutations,
    actions
}
複製程式碼

4.登陸

<template>
    <div>
        <input type="text" v-model="username" />
        <div @click="login">登陸</div>
    </div>
   
</template>

<script>
export default {
    data(){
        return {
            username:undefined
        }
    },
    methods:{
        login(){
            this.$store
                .dispatch('user/login',{username: this.username})
                .then(()=>{
                    // 登陸成功後重定向
                    this.$router.push({
                        path: this.$route.query.redirect || '/'
                    })
                })
                .catch(err=>{
                    console.log(err)
                })
        }
    }
}
</script>
複製程式碼

二.選單渲染

1.Sidebar/index

<template>
    <div>
  
            <el-menu
                :default-active="activeMenu"
                :background-color="variables.menuBg"
                :text-color="variables.menuText"
                :unique-opened="false"
                :active-text-color="variables.menuActiveText"
                :collapse-transition="false"
                mode="vertical"
                >
                    <SidebarItem
                        v-for="route in permission_routes"
                        :key="route.path"
                        :item='route'
                        :base-path='route.path'
                        >
                    
                    </SidebarItem>
            </el-menu>

    </div>
</template>

<script>
import { mapGetters } from 'vuex';
import SidebarItem from './SidebarItem'
console.log('一層')
export default {

    components:{ SidebarItem },
    computed:{
        ...mapGetters(['permission_routes']),
        activeMenu(){
            const route = this.$route;
            const { meta, path } = route
            // 預設啟用項
            if(meta.activeMenu){
                return meta.activeMenu
            }
            return path;
        },
        variables(){
            return {
                menuText: "#bfcbd9",
                menuActiveText: "#409EFF",
                menuBg: "#304156"
            }
        }
    },
    mounted(){
 
        console.log(this.permission_routes,12)
    }
}
</script>
複製程式碼

1.Sidebar/SidebarItem

<template>
    <div v-if="!item.hidden" class="menu-wrapper">
        <!-- 僅有一個可顯示的子路由,並且沒有孫子路由 -->
        <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && item.alwaysShow">
            <router-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
                <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown' : isNest }">
                   
                    <item :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" :title="onlyOneChild.meta.title" />
                </el-menu-item>
            </router-link>
        </template>
        <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
            <template v-slot:title>
                <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
            </template>
            <sidebar-item
                v-for="child in item.children"
                :key="child.path"
                :is-nest='true'
                :item='child'
                :base-path="resolvePath(child.path)"
                class="nest-menu"
                />
        </el-submenu>
    </div>
</template>

<script>
import path from 'path'
import Item from './Item'

export default {
    name: "SidebarItem",
    components: { Item },
    props: {
        item: {
            type: Object,
            required: true
        },
        isNest: {
            type: Boolean,
            required: false
        },
        basePath: {
            type: String,
            default: ''
        }
    },
    data(){
        this.onlyOneChild = null
        return {
            
        }
    },
    mounted(){
        console.log(this.item)
    },
    methods:{
        hasOneShowingChild(children = [],parent){
           
            const showingChildren = children.filter(item =>{
                if(item.hidden){
                    return false
                } else {
                    // 如果只有一個子選單時設定
                    this.onlyOneChild = item
                    return true
                }
            })
            // 當只有一個子路由,子路由預設展示
            if(showingChildren.length === 1){
                return true
            }
            // 沒有子路由則顯示父路由
            if(showingChildren.length === 0){
                this.onlyOneChild = {...parent, path: '', noShowingChildren: true }
                return true
            }
            console.log( this.onlyOneChild)
            return false
        },
        resolvePath(routePath){
            return path.resolve(this.basePath, routePath)
        }
    }    
}
</script>
複製程式碼

1.Sidebar/index

<script>
export default {
    name: "MenuItem",
    functional: true,
    props:{
        icon: {
            type: String,
            default: ''
        },
        title: {
            type: String,
            default: ''
        }
    },
    render(h, context){
        const { icon, title } = context.props
        const vnodes = []
      
        if(icon){
            // vnodes.push(<svg-icon icon-class={icon} />)
            vnodes.push(<i class={icon}></i>)
        }
        if(title){
  
            vnodes.push(<span slot='title'>{title}</span>)
        }
          
        return vnodes
    }
}
</script>
複製程式碼

相關文章