返回 导航

其他

hangge.com

Vue.js - 动态加载用户菜单样例(解决F5刷新后Store数据丢失问题)

作者:hangge | 2020-01-15 08:10
    通常来说,用户在登录成功后会在首页左侧或者上方显示一个用户菜单。而这个菜单数据是根据用户的角色动态加载的,即不同身份的用户登录成功后看到的菜单是不一样的。下面通过样例演示如何实现菜单的加载与展示。

一、后端部分

(1)后端接口实现比较容易,先根据登录用户的 id 查询该用户具有的角色,在根据角色信息查看对应的 Menu,最后将 Menu 返回到前端。
这里的服务端使用 Java 实现,关于如何通过数据库进行用户认证,以及获取当前登录用户的 id,可以参考我之前写的文章:

(2)这里假设后端接口为 /sysmenu,返回的数据格式如下:
(1)这里菜单一共有两级,结构上采用嵌套的形式,关于如何查询树形结构的数据,可以参考我之前写的文章:
(2)每个菜单节点主要关注这几个属性:
  • name:菜单名称
  • component:菜单对应的 vue 模块名(客户端会根据这个名字记载实际的 component 组件)
  • path:菜单对应的 vue 模块路径
[
  {
    "id":2,
    "path":"/home",
    "component":"Home",
    "name":"人员管理",
    "iconCls":"fa fa-user-circle-o",
    "children":[
      {
        "id":null,
        "path":"/emp/basic",
        "component":"EmpBasic",
        "name":"基本资料",
        "iconCls":null,
        "children":[
        ],
        "meta":{
          "keepAlive":false,
          "requireAuth":true
        }
      }
    ],
    "meta":{
      "keepAlive":false,
      "requireAuth":true
    }
  },
  {
    "id":5,
    "path":"/home",
    "component":"Home",
    "name":"统计管理",
    "iconCls":"fa fa-bar-chart",
    "children":[
      {
        "id":null,
        "path":"/sta/all",
        "component":"StaAll",
        "name":"综合信息统计",
        "iconCls":null,
        "children":[
        ],
        "meta":{
          "keepAlive":false,
          "requireAuth":true
        }
      },
      {
        "id":null,
        "path":"/sta/pers",
        "component":"StaPers",
        "name":"人事信息统计",
        "iconCls":null,
        "children":[
        ],
        "meta":{
          "keepAlive":false,
          "requireAuth":true
        }
      }
    ],
    "meta":{
      "keepAlive":false,
      "requireAuth":true
    }
  },
  {
    "id":6,
    "path":"/home",
    "component":"Home",
    "name":"系统管理",
    "iconCls":"fa fa-windows",
    "children":[
      {
        "id":null,
        "path":"/sys/basic",
        "component":"SysBasic",
        "name":"基础设置",
        "iconCls":null,
        "children":[
        ],
        "meta":{
          "keepAlive":false,
          "requireAuth":true
        }
      },
      {
        "id":null,
        "path":"/sys/log",
        "component":"SysLog",
        "name":"日志管理",
        "iconCls":null,
        "children":[
        ],
        "meta":{
          "keepAlive":false,
          "requireAuth":true
        }
      }
    ],
    "meta":{
      "keepAlive":false,
      "requireAuth":true
    }
  }
]

二、前端部分

1,初始路由设置(router/index.js)

系统初始路由只有一个 /home (即进入首页面)。等后面菜单完毕后,会根据菜单项自动添加对应的路由以及模块。
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'

Vue.use(Router)

export default new Router({
  routes: [
/*    {
      path: '/',
      name: 'Login',
      component: Login,
      hidden: true
    },*/
    {
      path: '/home',
      name: 'Home',
      component: Home
    }
  ]
})

2,创建 store 用来保存菜单数据(store/index.js)

    首先在 store 中创建一个 routes 数组,这个是一个空数组,后面我们将会把服务端返回的 JSON 格式的菜单数据保存在 store 中,然后各个 Vue 页面根据 store 中的数据来渲染菜单。
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    routes: []
  },
  mutations: {
    initMenu(state, menus){
      state.routes = menus;
    }
  }
});

3,菜单初始化工具类(utils/menuUtils.js)

(1)该工具类主要用于初始化菜单,其中一个重要的工作是将服务器返回的 JSON 格式的数据转成 router 需要的格式。因为服务端返回的 component 是一个字符串,而 router 中需要的却是一个组件,我们要在这里动态加载相应的组件:
import axios from 'axios';

// 请求菜单数据并初始化
export const initMenu = (router, store)=> {
  // 首先判断 store 中数据是否存在,如果存在,则说明这次跳转是正常的跳转
  // 而不是用户按F5键或者直接在地址栏输入某个地址进入的,这时直接返回,不必执行菜单初始化
  if (store.state.routes.length > 0) {
    return;
  }
  // 若 store 中不存在菜单数据,则需要初始化数据
  axios.get("/sysmenu").then(resp=> {
    if (resp && resp.status == 200) {
      // 将服务器返回的 JSON 格式的数据转成 router 需要的格式
      var fmtRoutes = formatRoutes(resp.data);
      // 将准备好的数据动态添加到路由中
      router.addRoutes(fmtRoutes);
      // 同时也将数据存到 store 中
      store.commit('initMenu', fmtRoutes);
    }
  })
}

// 将服务器返回的 JSON 转为 router 需要的格式
export const formatRoutes = (routes)=> {
  let fmRoutes = [];
  routes.forEach(router=> {
    let {
      path,
      component,
      name,
      meta,
      iconCls,
      children
    } = router;
    if (children && children instanceof Array) {
      // 如果有子节点则递归转换
      children = formatRoutes(children);
    }
    let fmRouter = {
      path: path,
      // 根据服务器返回的 component 动态加载需要的组件
      component(resolve){
        if (component.startsWith("Home")) {
          require(['../components/' + component + '.vue'], resolve)
        } else if (component.startsWith("Emp")) {
          require(['../components/emp/' + component + '.vue'], resolve)
        } else if (component.startsWith("Sta")) {
          require(['../components/statistics/' + component + '.vue'], resolve)
        } else if (component.startsWith("Sys")) {
          require(['../components/system/' + component + '.vue'], resolve)
        }
      },
      name: name,
      iconCls: iconCls,
      meta: meta,
      children: children
    };
    fmRoutes.push(fmRouter);
  })
  return fmRoutes;
}

(2)红框部分为项目里菜单对应的各个组件位置:

4,项目主入口代码(main.js)

    main.js 中除了将前面定义的 routestore 引入外,还需要开启一个路由全局守卫,在每次访问某个页面前都去加载一次菜单数据:
1,为什么每次访问页面前都需要加载一次菜单数据?
通常情况下,我们只在登录成功之后请求一次菜单资源,然后将 JSON 数据保存在 store 中,以便下一次使用。但是这样会有一个问题:
  • 假如用户登录成功之后,单击 Home 页的某一个按钮,进入某一个子页面中,然后按一下 F5 键进行刷新,这个时候就会出现空白页面,因为按 F5 键刷新之后 store 中的数据就没了。
2,解决F5刷新后Store数据丢失问题?
要解决这个问题有如下两种方案:
  • 方案一,不要将菜单资源保存到 store 中,而是保存到 localStorage 中,这样即使按 F5 键刷新之后数据还在。由于菜单资源是非常敏感的,因此不建议将其保存到本地,故舍弃。
  • 方案二,直接在每一个页面的 mounted 方法中都加载一次菜单资源。但这种做法工作量有点大,而且也不易维护,这里可以使用路由中的导航守卫来简化这个方案的工作量。
import Vue from 'vue';
import App from './App';
import router from './router';
import store from './store';
import {initMenu} from './utils/menuUtils'

Vue.config.productionTip = false;

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);

// 配置一个全局前置守卫
router.beforeEach((to, from, next)=> {
   // 首先判断目标页面是不是Login,若是Login页面,则直接通过,因为登录页不需要菜单数据
   if (to.name == 'Login') {
     next();
     return;
   }
   // 判断当前用户是否已经登录,否则跳回登录页
   // ........

   // 先初始化菜单数据
   initMenu(router, store);
   // 再进入下一个页面
   next();
 }
)

new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})


5,主视图代码(Home.vue)

菜单渲染操作在 Home.vue 组件中完成,并且菜单点击后里面的子路由视图会进行切换:
<template>
  <div>
    <el-container class="home-container">
      <el-header class="home-header">
        <span class="home_title">动态菜单 DEMO</span>
      </el-header>
      <el-container>
        <el-aside width="180px" class="home-aside">
          <div style="display: flex;justify-content: flex-start;width: 180px;text-align: left;">
            <el-menu style="background: #ececec;width: 180px;" unique-opened router>
              <!-- 遍历routes数据,根据routes中的数据渲染出el-submenu和el-menu-item -->
              <template v-for="(item,index) in this.routes" v-if="!item.hidden">
                <el-submenu :key="index" :index="index+''">
                  <template slot="title">
                    <i :class="item.iconCls" style="color: #20a0ff;width: 14px;"></i>
                    <span slot="title">{{item.name}}</span>
                  </template>
                  <el-menu-item width="180px"
                                style="padding-left: 30px;width: 170px;text-align: left"
                                v-for="child in item.children"
                                :index="child.path"
                                :key="child.path">{{child.name}}
                  </el-menu-item>
                </el-submenu>
              </template>
            </el-menu>
          </div>
        </el-aside>
        <el-main>
          <el-breadcrumb separator-class="el-icon-arrow-right">
            <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
            <el-breadcrumb-item v-text="this.$router.currentRoute.name"></el-breadcrumb-item>
          </el-breadcrumb>
          <keep-alive>
            <router-view v-if="this.$route.meta.keepAlive"></router-view>
          </keep-alive>
          <router-view v-if="!this.$route.meta.keepAlive"></router-view>
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>
<script>
  export default{
    methods: {
    },
    data(){
      return {
      }
    },
    computed: {
      // 在计算属性中返回 routes 数据
      routes(){
        return this.$store.state.routes
      }
    }
  }
</script>
<style>
  .home-container {
    height: 100%;
    position: absolute;
    top: 0px;
    left: 0px;
    width: 100%;
  }

  .home-header {
    background-color: #20a0ff;
    color: #333;
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: space-between;
    box-sizing: content-box;
    padding: 0px;
  }

  .home-aside {
    background-color: #ECECEC;
  }

  .home_title {
    color: #fff;
    font-size: 22px;
    display: inline;
    margin-left: 8px;
  }

  .el-submenu .el-menu-item {
    width: 180px;
    min-width: 175px;
  }
</style>

6,运行效果

(1)默认访问 /home 首页显示效果如下:

(2)点击左侧菜单栏自动切换右侧组件,并且无论当前在那个页面,按下 F5 刷新后菜单都不会丢失。

评论

全部评论(0)

回到顶部