为什么要使用动态路由?

一般系统中,都会有不同权限的操作人员,这时候他们看到的页面也将会不同,因此都需要根据他们的权限来生成对应的菜单,这个就得通过动态路由来实现。

主流的两种实现方式

控制一般都是由前端的路由中设置。后端返回路由表动态生成两种。

本文主要记录由数据库维护的动态路由实现,和相关注意点。即 退出 和 刷新 相关点导致的路由问题。

其它的可参考文章:动态路由

前端控制

  • 不用后端控制,只用返回角色、
  • 根据可能有的角色,在对应路由上维护相关角色
  • 在登录后,判断路由中维护的角色是否吻合来动态添加生成

后端控制

  • 路由存在数据库中,可动态维护。且相对安全。
  • 登录后,获取动态路由信息。
  • 得到路由后,匹配文件,生成路由,添加

后端控制路由 实现

由于我这里是用 fastmock 模拟的数据,实际中还请自行生成。

mock 数据如下:

{
"code": 200,
"success": true,
"data": [
{
"menuId": 2,
"menuName": "一级菜单",
"parentMenuId": 0,
"url": "menu/singleMenu/index",
"type": 1,
"icon": "el-icon-wind-power",
"orderIndex": 1,
"children": [
]
},
{
"menuId": 3,
"menuName": "二级菜单",
"parentMenuId": 0,
"url": "",
"type": 1,
"icon": "el-icon-ice-cream-round",
"orderIndex": 1,
"children": [
{
"menuId": 301,
"menuName": "二级1-1",
"parentMenuId": 3,
"url": "menu/secondMenu/second1-1",
"type": 2,
"icon": "el-icon-milk-tea",
"orderIndex": 1,
"children": [
]
},
{
"menuId": 302,
"menuName": "二级1-2",
"parentMenuId": 3,
"url": "menu/secondMenu/second1-2",
"type": 2,
"icon": "el-icon-potato-strips",
"orderIndex": 2,
"children": [
]
},
{
"menuId": 303,
"menuName": "二级1-3",
"parentMenuId": 3,
"url": "menu/secondMenu/second1-3",
"type": 2,
"icon": "el-icon-lollipop",
"orderIndex": 3,
"children": [
]
}
]
},
{
"menuId": 4,
"menuName": "三级多级菜单",
"parentMenuId": 0,
"url": "",
"type": 1,
"icon": "el-icon-ice-cream-round",
"orderIndex": 1,
"children": [
{
"menuId": 401,
"menuName": "三级1-1",
"parentMenuId": 4,
"url": "menu/threeMenu/three1-1",
"type": 2,
"icon": "el-icon-milk-tea",
"orderIndex": 1,
"children": [
]
},
{
"menuId": 402,
"menuName": "二级1-2",
"parentMenuId": 4,
"url": "",
"type": 2,
"icon": "el-icon-potato-strips",
"orderIndex": 2,
"children": [
{
"menuId": 40201,
"menuName": "三级1-2-1",
"parentMenuId": 402,
"url": "menu/threeMenu/nextMenu/three1-2-1",
"type": 2,
"icon": "el-icon-milk-tea",
"orderIndex": 1,
"children": [
]
},
{
"menuId": 40202,
"menuName": "三级1-2-2",
"parentMenuId": 402,
"url": "menu/threeMenu/nextMenu/three1-2-2",
"type": 2,
"icon": "el-icon-potato-strips",
"orderIndex": 2,
"children": [
]
}
]
}
]
}
],
"message": "成功"
}

添加菜单接口 及 菜单状态管理

由于这里是 mock 的。所以就判断了下登录用户名。代码如下:

export const getMenu = (username) => {
if (username == 'user') {
return axios.Get('api/usermenu')
} else {
return axios.Get('api/menu')
}
}

状态管理用于保存当前理由加载状态,和菜单值。

再在 actions 中添加一个获取菜单的方法,完整代码如下:

//引入接口
import { getMenu } from '@/api/modules/system' export default {
state: {
menuTree: [],
menuLoad: false, //菜单是否已加载状态避免重复加载,刷新又将变为false。
},
getters: {
menuLoad: (state) => {
return state.menuLoad
},
},
mutations: {
setMenuLoad(state, menuLoad) {
state.menuLoad = menuLoad
},
setMenuTree(state, menuTree) {
state.menuTree = menuTree
},
},
actions: {
getMenuTree({ commit }, username) {
return new Promise((resolve, reject) => {
getMenu(username)
.then((res) => {
if (res.code === 200) {
if (res.success) {
commit('setMenuTree', res.data)
} else {
// TODO 处理错误消息
}
resolve(res.data)
}
})
.catch((error) => {
reject(error)
})
})
},
},
}

根据得到的菜单生成动态路由

在这里由于退出时,会导致路由和加载状态不会更新,也不会重置路由的原因,完整代码中包含相关处理。

import Vue from 'vue'
import VueRouter from 'vue-router'
import login from '@/views/login'
import store from '@/store'
import { getMenu } from '@/api/modules/system' Vue.use(VueRouter)
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch((err) => err)
}
const routes = [
{
path: '/',
name: 'home',
component: () => import('@/layout'),
children: [
{
path: '',
name: 'index',
component: () => import('@/views/home/defaultPage'),
meta: {
title: '首页',
index: 0,
},
},
],
},
{
path: '/login',
name: 'login',
component: login,
meta: {
title: '登录',
},
},
{
path: '/notfound',
name: 'notfound',
component: () => import('@/views/notfound'),
meta: {
title: '未找到',
},
},
] const defultRouter = () => {
return new VueRouter({
routes: routes,
})
}
//每次使用默认路由
const router = defultRouter() // 解决addRoute不能删除动态路由问题
export function resetRouter() {
const reset = defultRouter()
router.matcher = reset.matcher
} const WhiteListRouter = ['/login', '/notfound'] // 路由白名单
//导航守卫 路由开始前
router.beforeEach(async (to, from, next) => {
let user = store.getters.userInfo
let token = store.getters.token
var hasAuth = user !== null && token !== null && user !== undefined && token !== undefined
if (to.path == '/login') {
if (hasAuth) {
next({ path: '/' })
} else {
next()
}
} else {
if (!hasAuth) {
//没登录的情况下 访问的是否是白名单
if (WhiteListRouter.indexOf(to.path) !== -1) {
next()
} else {
next({
path: '/login',
query: {
redirect: to.fullPath,
},
})
}
} else {
if (store.state.app.menuLoad) {
// 已经加载过路由了
next()
return
} else {
console.log(user.username)
// 使用 await 进行同步处理
const menu = await store.dispatch('getMenuTree', user.username)
console.log(menu)
// 加载动态菜单和路由
addDynamicMenuRoute(menu)
//next({ ...to, replace: true }); // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
next()
}
}
}
}) //刷新 加载完后 加载未找到路由 此方法只会在刷新后加载一次
router.onReady(() => {
var notfund = [
{
path: '*',
redirect: '/notfound',
name: 'notfound',
component: () => import('@/views/notfound'),
meta: {
title: '未找到',
},
},
]
router.options.routes = router.options.routes.concat(notfund)
router.addRoutes(router.options.routes)
}) /**
* 加载动态菜单和路由
*/
function addDynamicMenuRoute(menuData) {
if (store.state.app.menuRouteLoaded) {
console.log('已加载菜单和路由.')
return
}
// 根据返回的菜单 拼装路由模块
let dynamicRoutes = addDynamicRoutes(menuData)
// 处理静态组件绑定路由
router.options.routes[0].children = router.options.routes[0].children.concat(dynamicRoutes) //添加路由
router.addRoutes(router.options.routes) // 保存加载状态
store.commit('setMenuLoad', true)
}
/**
* 添加动态(菜单)路由
* @param {*} menuList 菜单列表
* @param {*} routes 递归创建的动态(菜单)路由
*/
function addDynamicRoutes(menuList = [], routes = []) {
var temp = []
for (var i = 0; i < menuList.length; i++) {
if (menuList[i].children && menuList[i].children.length >= 1) {
temp = temp.concat(menuList[i].children)
} else if (menuList[i].url && /\S/.test(menuList[i].url)) {
//将第一个斜杠去掉
menuList[i].url = menuList[i].url.replace(/^\//, '')
// 创建路由配置
var route = {
path: menuList[i].url,
component: null,
name: menuList[i].menuName,
meta: {
title: menuList[i].menuName,
icon: menuList[i].icon,
index: menuList[i].menuId,
},
}
try {
// 根据菜单URL动态加载vue组件,这里要求vue组件须按照url路径存储
// 如url="menu/singleMenu/index",则组件路径应是"@/views/menu/singleMenu/index".vue",否则将找不到改组件
let url = menuList[i].url
route['component'] = (resolve) => require([`@/views/${url}`], resolve)
} catch (e) {} routes.push(route)
}
}
if (temp.length >= 1) {
addDynamicRoutes(temp, routes)
}
return routes
} export default router

根据 vuex 中的暂存的菜单生成侧边菜单栏

新建菜单组件 递归生成菜单,新建 menuTree/index.vue,代码如下:

<!-- 动态菜单组成 -->
<template>
<div>
<!-- :popper-append-to-body="false" 解决 el-menu垂直布局递归生成菜单及菜单折叠后hover报错Maximum call stack size exceeded -->
<!-- 但是会导致最上面的菜单嵌入页面,无法显示出来 因此使用外层嵌套div加样式的解决方案-->
<el-submenu v-if="menu.children && menu.children.length >= 1"
:index="'' + menu.menuId"
:popper-append-to-body="true">
<template slot="title">
<i :class="menu.icon"></i>
<span slot="title">{{ menu.menuName }}
</span>
</template>
<menu-tree v-for="item in menu.children"
:key="item.menuId"
:menu="item">
</menu-tree>
</el-submenu>
<el-menu-item v-else-if="menu.url == ''"
:index="'' + menu.menuId"
disabled>
<i class="el-icon-magic-stick"></i>
<span slot="title">{{ menu.menuName }}
</span>
</el-menu-item>
<el-menu-item v-else
:index="'' + menu.menuId"
@click="$router.push('/' + menu.url)">
<i :class="menu.icon"></i>
<span slot="title">{{ menu.menuName }}
</span>
</el-menu-item>
</div>
</template> <script>
export default {
name: "MenuTree",
props: {
menu: {
type: Object,
required: true
}
},
data () {
return {}
}, }
</script>
<style lang="scss" scoped>
.el-menu--collapse > div > .el-submenu > .el-submenu__title span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
.el-menu--collapse
> div
> .el-submenu
> .el-submenu__title
.el-submenu__icon-arrow {
display: none;
}
</style>

在侧边栏中,从 state 中得到菜单,生成侧边栏菜单,完整代码如下:

<!-- aside -->
<template>
<div class="aside-container"
:class="isCollapse ? 'aside-collapse-width' : 'aside-width'">
<!--导航菜单 default-active="1-1" :background-color="themeColor" -->
<el-menu class="el-menu-vertical-demo"
:class="isCollapse ? 'aside-collapse-width' : 'aside-width'"
:collapse-transition="false"
:unique-opened="true"
:collapse="isCollapse"
ref="menuTreeRef"
:background-color="themeColor"
text-color="#fff"
active-text-color="#ffd04b">
<menu-tree v-for="menu in menuTree"
:key="menu.menuId"
:menu="menu">
</menu-tree>
</el-menu>
</div>
</template> <script>
import { mapState } from 'vuex'
import MenuTree from './menuTree'
export default {
data () {
return {}
},
components: {
MenuTree,
},
computed: {
...mapState({
isCollapse: (state) => state.app.isCollapse,
themeColor: (state) => state.app.themeColor,
menuTree: (state) => state.app.menuTree,
}),
mainTabs: {
get () {
return this.$store.state.app.mainTabs
},
set (val) {
this.$store.commit('updateMainTabs', val)
},
},
mainTabsActiveName: {
get () {
return this.$store.state.app.mainTabsActiveName
},
set (val) {
this.$store.commit('updateMainTabsActiveName', val)
},
},
},
watch: {
$route: 'handleRoute',
},
created () {
console.log(this.$route)
this.handleRoute(this.$route)
},
methods: {
// 路由操作处理
handleRoute (route) {
// tab标签页选中, 如果不存在则先添加
var tab = this.mainTabs.filter((item) => item.name === route.name)[0]
if (!tab) {
tab = {
name: route.name,
title: route.meta.title,
icon: route.meta.icon,
}
this.mainTabs = this.mainTabs.concat(tab)
}
this.mainTabsActiveName = tab.name
//解决刷新后,无法定位到当前打开菜单栏
this.$nextTick(() => {
// 切换标签页时同步更新高亮菜单
if (this.$refs.menuTreeRef != null) {
this.$refs.menuTreeRef.activeIndex = '' + route.meta.index
this.$refs.menuTreeRef.initOpenedMenu()
}
})
},
},
}
</script>
<style lang="scss" scoped>
.aside-container {
position: fixed;
top: 0px;
left: 0;
bottom: 0;
z-index: 1020;
.el-menu {
position: absolute;
top: $header-height;
bottom: 0px;
text-align: left;
}
}
.aside-width {
width: $aside-width;
}
.aside-collapse-width {
width: $aside-collapse-width;
}
</style>

退出后重置 vuex

因为只要登录过,那么当前状态中的 活动窗体 肯定是有值的,那么只需要判断该值是否有,有就刷新一下界面。

这里使用的是 reload 来刷新页面。

    created() {
//若是使用状态退出 则刷新一下 重置vuex
if (this.$store.state.app.mainTabsActiveName != '') {
window.location.reload()
}
},

刷新的方式可以参考:vue 刷新当前页的三种方法

最终效果

原文地址:http://book.levy.net.cn/doc/frontend/uiframe/dynamic_router.html

Vue管理系统前端系列六动态路由-权限管理实现的更多相关文章

  1. Vue管理系统前端系列一vue-cli4.x 初始化项目

    目录 项目介绍 技术基础 开发环境 安装工具 快速原型开发 创建项目 配置相关说明 目录结构 项目介绍 lion-ui 是一个基于 RBAC 的管理系统前端项目,采用 vue 和 element-ui ...

  2. Vue管理系统前端系列三登录页和首页及`vuex`管理登录状态

    目录 登录页面设计 vuex 对应 用户模块 丰富界面 首页相关代码 登录页面设计 该节记录了登录界面的设计,以及 vuex 的简单实用,然后将首页简单搭建完成. 先看最终效果图 先在 views 文 ...

  3. Vue管理系统前端系列四组件拆分封装

    目录 组件封装 首页布局拆分后结构 拆分后代码 状态管理中添加 app 模块 组件封装 在上一篇记录中,首页中有太多的代码,为了避免代码的臃肿,需要对主要的功能模块拆分,来让代码看起来更简洁,且能进行 ...

  4. Vue管理系统前端系列二相关工具引入及封装

    目录 sass-loader/vuex 等的引入说明 引入 element 引入 axios 1.基本使用 2.封装使用 2.1 开发环境配置请求地址 2.2 配置代理 2.3 添加接口相关文件 sa ...

  5. Vue管理系统前端系列五自定义主题

    目录 自定义主题 1.安装「主题生成工具」 2.安装白垩主题 3.新建颜色挑选组件 自定义主题 1.安装「主题生成工具」 由于主题工具需要依赖于 node-sass,而node-sass版本兼容性并不 ...

  6. 在微信框架模块中,基于Vue&Element前端,通过动态构建投票选项,实现单选、复选的投票操作

    最近把微信框架的前端改造一下,在原来基于Bootstrap框架基础上的微信后台管理,增加一套Vue&Element的前端,毕竟Vue的双向绑定开发起来也还是很方便的,而且Element本身也提 ...

  7. Hive 系列(二)权限管理

    Hive 系列(二)权限管理 一.关于 Hive Beeline 问题 启动 hiveserver2 服务,启动 beeline -u jdbc:hive2:// 正常 ,启动 beeline -u ...

  8. Greeplum 系列(七) 权限管理

    Greeplum 系列(七) 权限管理 一.角色管理 Role 分为用户(User)和组(Group),用户有 login 权限,组用来管理用户,一般不会有 login 权限.初始化 gp 时创建了一 ...

  9. SpringCloud系列——Zuul 动态路由

    前言 Zuul 是在Spring Cloud Netflix平台上提供动态路由,监控,弹性,安全等边缘服务的框架,是Netflix基于jvm的路由器和服务器端负载均衡器,相当于是设备和 Netflix ...

随机推荐

  1. 在ASP.NET Core中创建自定义端点可视化图

    在上篇文章中,我为构建自定义端点可视化图奠定了基础,正如我在第一篇文章中展示的那样.该图显示了端点路由的不同部分:文字值,参数,动词约束和产生结果的端点: 在本文中,我将展示如何通过创建一个自定义的D ...

  2. shell 中的${},##, %% , :- ,:+, ? 的使用

    假设我们定义了一个变量为:file=/dir1/dir2/dir3/my.file.txt 可以用${ }分别替换得到不同的值:${file#*/}:删掉第一个/ 及其左边的字符串:dir1/dir2 ...

  3. 在 Laravel 中通过自定义分页器分页方法实现伪静态分页链接以利于 SEO

    我们知道,Laravel 自带的分页器方法包含 simplePaginate 和 paginate 方法,一个返回不带页码的分页链接,另一个返回带页码的分页链接,但是这两种分页链接页码都是以带问号的动 ...

  4. Tomcat Script(python)

    由于刚接触 Python,所以使用Python 书写一些小的脚本,进行备忘同时分享给大家 #!/usr/bin/env python # _*_coding:utf-8_*_ # author: 'l ...

  5. Java面试必问:ThreadLocal终极篇 淦!

    点赞再看,养成习惯,微信搜一搜[敖丙]关注这个互联网苟且偷生的程序员. 本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点.资料以及我的系列 ...

  6. 记一次svg反爬学习

    网址:http://www.porters.vip/confusion/food.html 打开开发者工具后 页面源码并不是真实的数字,随便点一个d标签查看其样式 我们需要找到两个文件,food.cs ...

  7. LQB2013A05前缀判断

    上一道题,,,把if条件写错了,,,,找了半天的bug我都快哭了, 好了好了 看见这种填空题,先理解题意 然后把代码copy下来,把空格注释掉,然后运行到编译没有错. 再理一下它的思路 // // C ...

  8. 360随身WiFi3:纯净版无线网卡驱动下载及安装教程(Windows10版本)

    对于不带无线网卡的台式机,买一个360随身WiFi当无线网卡是很省钱的方法.但是这个随身WiFi3用的芯片较为奇葩,Win10下不太好找驱动.什么,你问我为啥不用360官网上的驱动?那个“驱动”装了之 ...

  9. matplotlib基础汇总_01

    灰度化处理就是将一幅色彩图像转化为灰度图像的过程.彩色图像分为R,G,B三个分量,分别显示出红绿蓝等各种颜色,灰度化就是使彩色的R,G,B分量相等的过程.灰度值大的像素点比较亮(像素值最大为255,为 ...

  10. 比较两个等长的字符串,若相同,则输出Match!,若不同,则输出No Match!

    文章目录 问题 代码 运行结果 问题 比较两个等长的字符串,若相同,则输出Match!,若不同,则输出No Match! 代码 data segment str1 db 'ASDFGHJKL';字符串 ...