• Vue + Element UI 实现权限管理系统 前端篇(十):动态加载菜单


    动态加载菜单

    之前我们的导航树都是写死在页面里的,而实际应用中是需要从后台服务器获取菜单数据之后动态生成的。

    我们在这里就用上一篇准备好的数据格式Mock出模拟数据,然后动态生成我们的导航菜单。

    接口模块化

    我们向来讲究模块化,之前接口都集中在,interface.js,我们现在把它改名为 api.js,并把里边原来登录、用户、菜单的相关接口都转移到我们新建的接口模块文件中。

    模块化之后的文件结构如下图所示

    模块化之后,模块接口写在相应的模块接口文件中,如下面是登录模块

    login.js

    import axios from '../axios'
    
    /* 
     * 系统登录模块
     */
    
    // 登录
    export const login = data => {
        return axios({
            url: '/login',
            method: 'post',
            data
        })
    }
    
    // 登出
    export const logout = () => {
        return axios({
            url: '/logout',
            method: 'get'
        })
    }

    模块化之后,父模块可以像这样引入

    api.js

    /* 
     * 接口统一集成模块
     */
    import * as login from './moudules/login'
    import * as user from './moudules/user'
    import * as menu from './moudules/menu'
    
    
    // 默认全部导出
    export default {
        login,
        user,
        menu
    }

    因为我们这里是导出的是父模块,所以在具体接口调用的时候,也需要在原来的基础上加上模块了,像这样。

    如上面 api.js 中,我们导出了 login 的整个文件,而 login 文件下有 login,logout 等多个方法。

    导航菜单树接口

    我们在 menu.js 下创建一个查询导航菜单树的接口。

    import axios from '../axios'
    
    /* 
     * 菜单管理模块
     */
    
    export const findMenuTree = () => {
        return axios({
            url: '/menu/findTree',
            method: 'get'
        })
    }

     api.js 中如果没引入要记得引入。

    页面接口调用

    接口已经有了,我们在导航菜单组件 MenuBar.vue 中,加载菜单并存入 store 。

    页面菜单渲染

    还是在  MenuBar.vue 中,页面通过封装的菜单树组件读取store数据,递归生成菜单。

    新建菜单树组件,递归生成菜单,并在点击响应函数里面根据菜单URL跳转到指定路由。

    components/MenuTree/index.js

    <template>
      <el-submenu v-if="menu.children && menu.children.length >= 1" :index="menu.menuId + ''">
        <template slot="title">
          <i :class="menu.icon"></i>
          <span slot="title">{{menu.name}}</span>
        </template>
        <MenuTree v-for="item in menu.children" :key="item.menuId" :menu="item"></MenuTree>
      </el-submenu>
      <el-menu-item v-else :index="menu.menuId + ''" @click="handleRoute(menu)">
        <i :class="menu.icon"></i>
        <span slot="title">{{menu.name}}</span>
      </el-menu-item>
    </template>
    
    <script>
      export default {
        name: 'MenuTree',
        props: {
          menu: {
            type: Object,
            required: true
          }
        },
        methods: {
          handleRoute (menu) {
            // 通过菜单URL跳转至指定路由
            this.$router.push(menu.url)
          }
        }
      }
    </script>

    提供Mock数据

    接口有了,页面调用和渲染也写好了,该提供Mock数据了。

    mock/modules/menu.js 中 mock findTree接口,data 对应数据太多,这里不贴了。

    export function findTree() {
      return {
        url: 'http://localhost:8080/menu/findTree',
        type: 'get',
        data: menuTreeData // json 对象数据
      }
    }

    测试效果

    启动完成,进入主页,我们看到导航菜单已经成功加载进来了,oh yeah!

    然而,我们愉悦的点了点菜单,发现是这样的情况,oh no !

    毛都没有,不过显然,聪明的你已经看穿了一切,我们之前只提供了一个叫 /user 的路由,并没有提供 /sys/user 的路由。

    好吧,我们稍微修改一下,打开路由配置,把 /user 改成 /sys/user 试试。

    果不其然,修改完之后便可以正常跳转到用户界面了。

    但不对呀,这里路由配置是写死的,导航菜单是菜单数据动态生成的,这个路由配置也应该是根据菜单数据动态添加的啊,嗯,所以接下来我们就来讨论动态路由配置的问题。

    动态路由实现

    在 vue 的 route 中提供了 addRoutes 来实现动态路由,打开 MenuBar.vue ,我们在加载导航菜单的同时添加动态路由配置。

    MenuBar.vue

    其中 addDynamicMenuRoutes 是根据菜单返回动态路由配置的关键代码。

    addDynamicMenuRoutes 方法详情:

        /**
         * 添加动态(菜单)路由
         * @param {*} menuList 菜单列表
         * @param {*} routes 递归创建的动态(菜单)路由
         */
        addDynamicMenuRoutes (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].name,
                meta: {
                  menuId: menuList[i].menuId,
                  title: menuList[i].name,
                  isDynamic: true,
                  isTab: true,
                  iframeUrl: ''
                }
              }
              // url以http[s]://开头, 通过iframe展示
              if (isURL(menuList[i].url)) {
                route['path'] = menuList[i].url
                route['name'] = menuList[i].name
                route['meta']['iframeUrl'] = menuList[i].url
              } else {
                try {
                  // 根据菜单URL动态加载vue组件,这里要求vue组件须按照url路径存储
                  // 如url="sys/user",则组件路径应是"@/views/sys/user.vue",否则组件加载不到
                  let array = menuList[i].url.split('/')
                  let url = array[0].substring(0,1).toUpperCase()+array[0].substring(1) + '/' + array[1].substring(0,1).toUpperCase()+array[1]  .substring(1)
                  route['component'] = resolve => require([`@/views/${url}`], resolve)
                } catch (e) {}
              }
              routes.push(route)
            }
          }
          if (temp.length >= 1) {
            this.addDynamicMenuRoutes(temp, routes)
          } else {
            console.log(routes)
          }
          return routes
        }

    动态菜单页面的组件结构稍微调整下,需要跟菜单url匹配,才能根据菜单url确定组件路径来动态加载组件。

    把路由文件清理一下,把动态菜单相关的路由配置处理掉,留下一些固定的全局路由就好。

    动态路由测试

    启动完成,进入主页,点击用户管理,路由到了用户管理页面。

     点击机构管理,路由到了机构管理页面。

     

    好了,到这里动态路由功能已经实现了,给自己鼓个掌吧。

    页面刷新出大坑

    先前我们是将导航菜单和路由的加载放在菜单栏页面MenuBar.vue中,一切显示和路由也都正常,看起来没什么问题。然而当我们在非根据路径刷新页面时,问题出现了。

    如下图所示,我们在用户管理页面的时候,点击刷新浏览器,然后就白茫茫一片了,这是因为浏览器的刷新会导致整个vue重新加载,路由被重新初始化了,后面在Menu.bar添加的动态路由没有了,所以跳转的时候没有找到匹配路由,跳转的是一个不存在的页面,故而白茫茫一片。

    专业填坑指南

    这显然是动态菜单和路由的加载时机不对,怎么解决这个问题呢,既然问题出在加载时机,那就找一个在页面刷新的时候也能触发重新加载的地方就好了。

    这样的地方也不少,像vue加载过程中的钩子函数,路由导航守卫函数等都可以,我们这里就选择在路由导航守卫的 beforeEach 函数内加载,保证每次路由跳转的时候都能够拥有动态菜单和路由。

    把原先在MenuBar.vue中加载动态菜单和路由的代码,转移到路由配置 router/index 中来。

    beforeEach:

    router.beforeEach((to, from, next) => {
      // 登录界面登录成功之后,会把用户信息保存在会话
      // 存在时间为会话生命周期,页面关闭即失效。
      let isLogin = sessionStorage.getItem('user')
      if (to.path === '/login') {
        // 如果是访问登录界面,如果用户会话信息存在,代表已登录过,跳转到主页
        if(isLogin) {
          next({ path: '/' })
        } else {
          next()
        }
      } else {
        // 如果访问非登录界面,且户会话信息不存在,代表未登录,则跳转到登录界面
        if (!isLogin) {
          next({ path: '/login' })
        } else {
          // 加载动态菜单和路由
          addDynamicMenuAndRoutes()
          next()
        }
      }
    })
    addDynamicMenuAndRoutes:
    /**
    * 加载动态菜单和路由
    */
    function addDynamicMenuAndRoutes() {
      api.menu.findMenuTree()
      .then( (res) => {
        store.commit('setMenuTree', res.data)
        // 添加动态路由
        let dynamicRoutes = addDynamicRoutes(res.data)
        router.options.routes[0].children = router.options.routes[0].children.concat(dynamicRoutes)
        router.addRoutes(router.options.routes);
      })
      .catch(function(res) {
        alert(res);
      });
    }
    addDynamicRoutes:
    /**
    * 添加动态(菜单)路由
    * @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].name,
           meta: {
             menuId: menuList[i].menuId,
             title: menuList[i].name,
             isDynamic: true,
             isTab: true,
             iframeUrl: ''
           }
         }
         // url以http[s]://开头, 通过iframe展示
         if (isURL(menuList[i].url)) {
           route['path'] = menuList[i].url
           route['name'] = menuList[i].name
           route['meta']['iframeUrl'] = menuList[i].url
         } else {
           try {
             // 根据菜单URL动态加载vue组件,这里要求vue组件须按照url路径存储
             // 如url="sys/user",则组件路径应是"@/views/sys/user.vue",否则组件加载不到
             let array = menuList[i].url.split('/')
             let url = array[0].substring(0,1).toUpperCase()+array[0].substring(1) + '/' + array[1].substring(0,1).toUpperCase()+array[1]  .substring(1)
             route['component'] = resolve => require([`@/views/${url}`], resolve)
           } catch (e) {}
         }
         routes.push(route)
       }
     }
     if (temp.length >= 1) {
       addDynamicRoutes(temp, routes)
     } else {
       console.log(routes)
     }
     return routes
    }

    当然,别忘了把要用到的几个东西引入进来,把导航菜单栏的代码清理一下。

    测试效果

    启动完成,进入主页,点击用户管理,点击刷新按钮。

    刷新后,菜单收起来了,然而页面还是正确的停留在用户管理页面。妈妈再也不用担心我会刷新了!

    保存加载状态

    现在每次路由跳转前都会重新获取菜单数据生成菜单和路由,及时页面没有刷新也会重复获取,这样很影响性能。我们改良一下,加载成功之后把状态保存到store,每次加载之前先检查store的加载状态,这样就可以避免在非页面刷新的情形下还频发重复的加载了。

     在 store 中添加菜单路由加载状态,避免页面未刷新而重复加载。

    修改路由配置,在加载之前判断加载状态,只有未加载的情况下才加载,并在加载之后保存加载状态。

    求解一个问题

    在路由跳转的时候,路由好像是在原路径基础上叠加路由路径跳转的。

    如路径在 http://localhost:8090/#/sys/dept 的时候,点击用户管理。

    代码对应 this.$router.push(‘’sys/user),路由就赚到了 http://localhost:8090/#/sys/sys/user。

    比正确路由多了一个 sys,目前还不到为什么。

    目前我是在实际跳转之前,先跳回主页面然后在做真正的跳转。

    这样问题可以解决,但无端端多了一步跳转总归不好,求解中。。。

    源码下载

    后端:https://gitee.com/liuge1988/kitty

    前端:https://gitee.com/liuge1988/kitty-ui.git


    作者:朝雨忆轻尘
    出处:https://www.cnblogs.com/xifengxiaoma/ 
    版权所有,欢迎转载,转载请注明原文作者及出处。

  • 相关阅读:
    web前端常见面试题
    pyhton课堂随笔-基本画图
    安装和启动json-server
    Idea破解注册码
    MongoDB基本增删改查
    MogonDB安装及配置
    python和jupyter安装
    web中静态资源和动态资源的概念及区别
    nodejs基础(三)
    C#进程、线程、CPU
  • 原文地址:https://www.cnblogs.com/xifengxiaoma/p/9573439.html
Copyright © 2020-2023  润新知