vue-element-admin 如何使用动态菜单
最近在使用 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 接口来获取动态的菜单了,并且前端在开发时也不需要找服务端来新增路由信息了,路由定义依然在前端。