vue实现动态导航与动态路由绑定,并使用mock模拟数据实现

动态导航与动态路由绑定

在页面中左侧的菜单栏的数据是写死的,在实际场景中我们不可能这样做,因为菜单是需要根据登录用户的权限动态显示菜单的,也就是用户看到的菜单栏可能是不一样的,这些数据需要去后端访问获取。

前言

导航栏菜单的数据会根据用户的不同会有不一样的展示

路由跟导航同时进行一个动态绑定,每个用户看到的导航是不一样的,对应的路由也可能是不一样的

比如说用户只有用户管理权限,但是修改路径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。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tl0S9b3I-1619748343398)(VueAdmin.assets/image-20210429234854622.png)]

可以看到,我用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
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yHxaibp1-1619748343403)(VueAdmin.assets/image-20210430093558191.png)]

猜你喜欢

转载自blog.csdn.net/weixin_46195957/article/details/116294668