动态导航与动态路由绑定
在页面中左侧的菜单栏的数据是写死的,在实际场景中我们不可能这样做,因为菜单是需要根据登录用户的权限动态显示菜单的,也就是用户看到的菜单栏可能是不一样的,这些数据需要去后端访问获取。
前言
导航栏菜单的数据会根据用户的不同会有不一样的展示
路由跟导航同时进行一个动态绑定,每个用户看到的导航是不一样的,对应的路由也可能是不一样的
比如说用户只有用户管理权限,但是修改路径localhost:8080/sys/roles 还是能跳转进去,这是不被允许的
解决方法
1、有多少导航就加载多少路由,不加载某个路由,用户走不属于自己权限的路由肯定是不通过的
2、我们加载菜单导航的同时去加载权限信息,然后通过权限信息去判断用户有没有路由这个权限,在下一次发起路由进行之前进行拦截检查,看看用户有没有这个权限信息
从安全性来看,我们更偏向于动态绑定路由,因为动态绑定路由可以直接告诉用户跳转不到这个页面
所以我们决定选用动态绑定路由,就不能直接在菜单组件里显示菜单信息了,也就是菜单栏里面要展示的数据就不能直接通过在菜单组件的data里面进行获取了,因为我们这个菜单栏信息后面会跟路由进行一个绑定,所以我们这个信息就应该放到router-index.js中,也就是说我们在获取路由到一个页面之前(首次登陆),首先进行一个菜单栏导航数据的加载,并且进行路由动态绑定,绑定完之后在查看有没有这个路由,有就允许跳转
1、菜单栏组件(为渲染从store拿到的数据进行修改)
菜单栏的数据应该在页面打开时就拿到数据,通常我们在methods定义这个获取菜单数据的方法,然后在created中进行调用,但是我们打算路由跟导航同时进行一个动态绑定,就是说我们的用户看到的导航栏是不一样的,路由也是有多有少的
首先我们先把写死的数据简化成一个json数组数据,然后for循环展示出来
- /src/views/inc/SideMenu.vue
<template>
<el-menu
default-active="0"
class="el-menu-vertical-demo"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b">
<router-link to="/index">
<el-menu-item index="0" style="text-align: left">
<template slot="title">
<i class="el-icon-s-home"></i>
<span slot="title">首页</span>
</template>
</el-menu-item>
</router-link>
<el-submenu :index="menu.name" v-for="menu in menuList">
<template slot="title">
<i :class="menu.icon"></i>
<span>{
{menu.title}}</span>
</template>
<router-link :to="item.path" v-for="item in menu.children">
<el-menu-item :index="item.name">
<template slot="title">
<i :class="item.icon"></i>
<span slot="title">{
{item.title}}</span>
</template>
</el-menu-item>
</router-link>
</el-submenu>
</el-menu>
</template>
<script>
export default {
name: "SideMenu",
data() {
return {
// 这里的数据我们不在用自己定义的了,而是去store获取
// menuList:""
}
},
computed:{
// 我们使用计算属性去动态监听路由信息,这里使用到了计算属性的get回调函数
menuList: {
get(){
return this.$store.state.menus.menuList
}
}
},
methods:{
}
}
</script>
<style scoped>
.el-aside {
background-color: #D3DCE6;
color: #333;
/*text-align: center;*/
line-height: 200px;
}
.el-submenu {
text-align: left;
}
</style>
为什么我们在菜单组件中获取store的数据时需要通过计算属性computed的回调函数get进行数据加载呢?
因为这里涉及到了一个加载的优先级,我们在加载SideMenu的组件时比router-index.js要早,所以在router-index.js还未向后端发送axios请求,也自然无法提交到store进行数据共享
使用vue中的router.beforeEach 全局导航钩子实现进入路由前验证,直接使用url打开页面时,先执行vue单页面中的mounted钩子,再执行的router.beforeEach。
可以看到,我用for循环显示数据,那么这样变动菜单栏时候只需要修改data中的menuList即可。效果和之前的完全一样。 现在menuList的数据我们是直接写到页面data上的,一般我们是要请求后端的,所以这里我们定义一个mock接口,因为是动态菜单,一般我们也要考虑到权限问题,所以我们请求数据的时候一般除了动态菜单,还要权限的数据,比如菜单的添加、删除是否有权限,是否能显示该按钮等,有了权限数据我们就定动态决定是否展示这些按钮了。
2、Mock.js 路由权限数据
Mock.mock('/sys/menu/nav', 'get', () => {
// 菜单栏信息
let nav = [
{
name: 'SysManga',
title: '系统管理',
icon: 'el-icon-s-operation',
// 告诉系统path对应的是哪个组件
component: '',
path: '',
children: [{
name: 'SysUser',
title: '用户管理',
icon: 'el-icon-s-custom',
path: '/sys/users',
component: 'system/User',
children: []
}]
}, {
name: 'SysTools',
title: '系统工具',
icon: 'el-icon-s-tools',
path: '',
component: '',
children: [{
name: 'SysDict',
title: '数字字典',
icon: 'el-icon-s-order',
path: '/system/dicts',
component: '',
children: []
},]
}
]
let authoritys = []
Result.data = {
nav: nav,
authoritys: authoritys
}
return Result;
})
这样我们就定义好了导航菜单的接口,什么时候调用呢?应该登录成功完成之后调用,但是并不是每一次打开我们都需要去登录,也就是浏览器已经存储到用户token的时候我们不需要再去登录的了,所以我们不能放在登录完成的方法里。那么是当前这个Home.vue页面吗?看起来没什么问题,方正每次都会进入这个页面,然后搞个开关控制是否重新加载就行?
我们这里还要考虑一个问题,就是导航菜单的路由问题,啥意思?就是点击菜单之后路由到哪个页面是需要在router中声明的。
这个路由问题我提供两个解决方案:
-
1、全部写死,也就是提前写好所有的路由,不管用户有没有权限,后面在通过权限数据来判断用户是否有权限访问路由。
-
2、动态渲染,就是把加载到的导航菜单数据动态绑定路由
这里我们使用第二种解决方案,这类简单点,后续我们再开发页面的时候就不需要去改动路由,可以动态绑定。
综上,我们把加载菜单数据这个动作放在router.js中。Router有个前缀拦截,就是在路由到页面之前我们可以做一些判断或者加载数据。
3、Router-index.js做前置拦截
使用vuex+router.beforeEach()+动态路由实现页面拦截
页面刷新时会清楚vuex里面的值;(防止直接修改地址栏)router.beforeEach()对跳转前进行拦截判断(对vuex里面的值进行判断)
当用户登录时请求后台拿到数据,加载路由.(跳转页面)
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Login from "../views/Login";
import axios from "axios";
import store from "../store"
Vue.use(VueRouter)
const routes = [
// 这种是预先加载的
{
path: '/',
name: 'Home',
component: Home,
children: [
{
path: '/index',
name: 'Index',
component: () => import('../views/index')
},
{
path: '/userCenter',
name: 'UserCenter',
component: () => import('../views/UserCenter')
}
]
},
// 这种是懒加载模式
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue')
},
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
// to 代表即将跳转到哪个路由 from 代表从哪个路由发出的请求 next 继续走
router.beforeEach((to, from, next) => {
//我们定义参数用判断有没有请求过这个路由
let hasRoute = store.state.menus.hasRoute
if (!hasRoute) {
// 获取导航的信息
axios.get("/sys/menu/nav", {
// 我们在main.js 导入的axios是我们自定义的axios.js文件,所以在axios.js文件里所配置的前置拦截这里不会被拦截到
// 所以向后端发送请求时我们需要手动添加请求头的token 认证\
// 这里做到判断用户是否有效登录,如若用户还没有登录,那么也拿不到下面的数据加载
headers: {
Authorization: localStorage.getItem("token")
}
}).then(res => {
// 拿到menuList,保存到store进行数据共享
store.commit("setMenuList", res.data.data.nav)
// 拿到用户的权限(一般是操作的权限,而不是路由的权限,因为路由的权限早在我们动态绑定路由的时候,有绑定的路由就说明能够进行访问)
store.commit("setPermList", res.data.data.authoritys)
console.log(store.state.menus.menuList);
// 动态路由绑定
let newRoutes = router.options.routes;
res.data.data.nav.forEach(menu => {
if (menu.children) {
menu.children.forEach(e => {
// 路由绑定,转换成路由
let route = menuToRoute(e);
// 把路由添加到路由管理中
if (route) {
// newRoutes[0] 代表Home路由,也就是说遍历出来的都是Home组件下的子组件
newRoutes[0].children.push(route)
}
})
}
})
console.log("newRoutes");
console.log(newRoutes);
// 进行路由绑定
router.addRoutes(newRoutes)
hasRoute = true
store.commit("changeRouteStatus",hasRoute)
})
}
next()
})
// 导航转成路由
const menuToRoute = (menu) => {
if (!menu.component) {
return null
}
let route = {
name: menu.name,
path: menu.path,
meta: {
icon: menu.icon,
title: menu.title
}
}
// 这里使用懒加载模式加载组件
route.component = () => import('../views/' + menu.component + '.vue')
return route
}
export default router
可以看到,我们通过menuToRoute就是把menu数据转换成路由对象,然后router.addRoutes(newRoutes)动态添加路由对象。 同时上面的menu对象中,有个menu.component,这个就是连接对应的组件,我们需要添加上去,比如说/sys/users链接对应到component(sys/User)。
同时上面router中我们还通过判断是否登录页面,是否有token等判断提前判断是否能加载菜单,同时还做了个开关hasRoute来动态判断是否已经加载过菜单。
4、store-menus.js
src/store/modules/menus.js
还需要在store中定义几个方法用于存储数据,我们定义一个menu模块,所以在store中新建文件夹modules,然后新建menus.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default {
state: {
menuList: [],
authoritys: [],
// hasRoute: sessionStorage.getItem("hasRoute")
hasRoute: false
},
// setter方法
mutations: {
setMenuList(state,menus){
state.menuList = menus
},
setPermList(state,perms){
state.permList = perms
},
changeRouteStatus(state,hasRoute){
state.hasRoute = hasRoute
// sessionStorage.setItem("hasRoute",hasRoute)
}
},
// getter方法
actions: {
},
modules: {
}
}
记得在store中import这个模块,然后添加到modules:
- src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import menus from "./modules/menus";
Vue.use(Vuex)
export default new Vuex.Store({
state: {
token: ''
},
// setter方法
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
localStorage.setItem("token", token)
},
resetState: (state) => {
state.token = ''
}
},
// getter方法
actions: {
},
modules: {
menus
}
})
5、效果展示
这样我们菜单的数据就可以加载了,然后再SideMenu.vue中直接获取store中的menuList数据即可显示菜单出来了。
data() {
return {
menuList: this.$store.state.menus.menuList
}
}