最近在使用 vue-element-admin 將相關心得進行總結:
vue-element-admin 是 vue 生態中一個后界面解決方案,文檔地址:https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/
在使用過程中有這樣一個問題,vue-element-admin 的菜單列表是通過遍歷路由進行渲染的,由前端定義,可以在 router.js 中看到相關代碼,即是路由也是菜單;
好處是我們不用重復定義菜單列表信息和路由之間的綁定了;但是我們的菜單信息想通過服務端進行動態輸出來達到權限控制的效果就不是那么容易了;
網上搜索了一圈,基本上的方案是由服務端輸出完整的 vue-element-admin 路由信息並進行綁定,這樣雖然能達到動態菜單的效果,但是給服務端也造成了不必要的煩惱;
作為服務端開發:不關心 菜單對應的是哪個 vue 里面的 component ,也不希望將菜單的格式限定得那個嚴格,甚至不關心菜單的圖標是什么,只需要嚴格按照服務端的要求顯示或隱藏菜單即可;
為了解決這個問題,我的優化方案如下,服務端只需輸出菜單顯示或隱藏,路由信息定義都在前端寫死,這樣達到完美的前后端分離要求。
1.定義路由
在 src/router/index.js 中將 constantRoutes 常量中定義的側邊欄顯示的菜單信息刪除掉;然后重新定義一個 dynamicRoutes 常量寫入菜單信息,dynamicRoutes 中每個節點都添加 srvName 屬性,通過它來和服務端返回的菜單信息進行關聯。
export const constantRoutes = [ { path: '/login', component: () => import('@/views/login/index'), hidden: true }, { path: '/404', component: () => import('@/views/404'), hidden: true }, { path: '/', component: Layout, redirect: '/dashboard', children: [{ path: 'dashboard', name: 'Dashboard', component: () => import('@/views/dashboard/index'), meta: { title: 'Dashboard', icon: 'dashboard' } }] }, // 404 page must be placed at the end !!! { path: '*', redirect: '/404', hidden: true } ] export const dynamicRoutes = [ { path: '/example', component: Layout, redirect: '/example/table', srvName: '/example', name: 'Example', meta: { title: 'Example', icon: 'example' }, children: [ { path: 'table', name: 'Table', srvName: '/example/table', component: () => import('@/views/table/index'), meta: { title: 'Table', icon: 'table' } }, { path: 'tree', name: 'Tree', srvName: '/example/tree', component: () => import('@/views/tree/index'), meta: { title: 'Tree', icon: 'tree' } } ] } ]
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes.concat(dynamicRoutes) // 初始化時將所有路由都加載上,否則會出現刷新頁面404的情況
})
2. 服務端接口
服務端接口返回數據格式如下,節點中 srvName 和前端的路由進行匹配,通過 show 屬性來確定顯示或隱藏
服務端也無需將菜單的子/父級關系輸出,只需要將所有的菜單信息輸出一個數組即可。
[ { srvName: '/example', id: 1, show: true }, { id: 2, srvName: '/example/table', show: true }, { id: 3, srvName: '/example/tree', show: true }, { id: 4, srvName: '/nested', show: true }, { id: 5, srvName: '/nested/menu1', show: true } ]
3.定義 api 請求模塊
在 src/api/ 目錄下創建 menus.js
import request from '@/utils/request' export function getMenus(token) { return request({ url: '/menus', method: 'get', params: { token } }) }
4.配置 store 調用
新增文件 src/store/modules/menus.js
import { getMenus } from '@/api/menus' import { getToken } from '@/utils/auth' import { dynamicRoutes } from '@/router/index' const getDefaultState = () => { return { token: getToken(), menuList: [] } } const state = getDefaultState() const mutations = { SET_MENUS: (state, menus) => { state.menuList = menus } } // 動態菜單還是定義在前端,后台只會返回有權限的菜單列表,通過遍歷服務端的菜單數據,沒有的將對於菜單進行隱藏 // 這樣的好處是服務端無需返回前端菜單相關結構,並且菜單顯示又可以通過服務端來控制,進行菜單的動態控制 // 前端新增頁面也無需先通過服務端進行菜單添加,遵循了前后端分離原則 export function generaMenu(routes, srvMenus) { for (let i = 0; i < routes.length; i++) { const routeItem = routes[i] var showItem = false for (let j = 0; j < srvMenus.length; j++) { const srvItem = srvMenus[j] // 前后端數據通過 srvName 屬性來匹配 if (routeItem.srvName !== undefined && routeItem.srvName === srvItem.srvName && srvItem.show === true) { showItem = true routes[i]['hidden'] = false break } } if (showItem === false) { routes[i]['hidden'] = true } if (routeItem['children'] !== undefined && routeItem['children'].length > 0) { generaMenu(routes[i]['children'], srvMenus) } } } const actions = { getMenus({ commit }) { return new Promise((resolve, reject) => { getMenus(state.token).then(response => { const { data } = response console.log(response) if (!data) { reject('Verification failed, please Login again.') } const srvMenus = data.items var pushRouter = dynamicRoutes generaMenu(pushRouter, srvMenus) commit('SET_MENUS', pushRouter) resolve(data) }).catch(error => { reject(error) }) }) } } export default { namespaced: true, state, mutations, actions }
以下標紅是修改部分
src/store/index.js
import Vue from 'vue' import Vuex from 'vuex' import getters from './getters' import app from './modules/app' import settings from './modules/settings' import user from './modules/user' import menus from './modules/menus' Vue.use(Vuex) const store = new Vuex.Store({ modules: { app, settings, user, menus }, getters }) export default store
src/store/getters.js
const getters = { sidebar: state => state.app.sidebar, device: state => state.app.device, token: state => state.user.token, avatar: state => state.user.avatar, name: state => state.user.name, menuList: state => state.menus.menuList } export default getters
5.渲染動態菜單
這里是最關鍵一步,通過修改 src/permission.js 文件來渲染動態菜單,再該文件中可以看到登陸成功后的相關操作,我們在登陸成功后添加渲染菜單相關代碼即可
await store.dispatch('menus/getMenus').then((res) => { console.log(store.getters.menuList) router.addRoutes(store.getters.menuList) router.options.routes = constantRoutes.concat(store.getters.menuList) })
完整的 src/permission.js 內容
import router from './router' import store from './store' import { Message } from 'element-ui' import NProgress from 'nprogress' // progress bar import 'nprogress/nprogress.css' // progress bar style import { getToken } from '@/utils/auth' // get token from cookie import getPageTitle from '@/utils/get-page-title' import { constantRoutes } from './router/index' NProgress.configure({ showSpinner: false }) // NProgress Configuration const whiteList = ['/login'] // no redirect whitelist router.beforeEach(async(to, from, next) => { // start progress bar NProgress.start() // set page title document.title = getPageTitle(to.meta.title) // determine whether the user has logged in const hasToken = getToken() if (hasToken) { if (to.path === '/login') { // if is logged in, redirect to the home page next({ path: '/' }) NProgress.done() } else { const hasGetUserInfo = store.getters.name if (hasGetUserInfo) { next() } else { try { // get user info await store.dispatch('user/getInfo') await store.dispatch('menus/getMenus').then((res) => { console.log(store.getters.menuList) router.addRoutes(store.getters.menuList) router.options.routes = constantRoutes.concat(store.getters.menuList) }) next() } catch (error) { // remove token and go to login page to re-login await store.dispatch('user/resetToken') Message.error(error || 'Has Error') next(`/login?redirect=${to.path}`) NProgress.done() } } } } else { /* has no token*/ if (whiteList.indexOf(to.path) !== -1) { // in the free login whitelist, go directly next() } else { // other pages that do not have permission to access are redirected to the login page. next(`/login?redirect=${to.path}`) NProgress.done() } } }) router.afterEach(() => { // finish progress bar NProgress.done() })
這樣我們就可以通過 api中的 /menus 接口來獲取動態的菜單了,並且前端在開發時也不需要找服務端來新增路由信息了,路由定義依然在前端。