vue-element-admin 後臺動態加載菜單

dynamic-router

項目地址

giteejavascript

前端: 基於 vue-element-admin 延用公司 【用戶中心】那一套本身魔改的版本,在此基礎之上重構。前端

後端: Go的語法簡潔,開發速度快,因此後端使用了Go結合Gin框架,作了一個簡單的CURD服務。(主要是由於沒人手協助,也只好本身寫一個了。)這裏不過多介紹後端。vue

前言

動態路由java

因爲公司業務須要,在權限驗證與安全方面,要求是菜單根據權限動態控制。git

在現有的項目當中,經過基於 vue-element-admin 項目提供的菜單權限控制,沒法知足公司的具體業務須要。json

實際上主要的目的是經過後端控制菜單權限。segmentfault

所以也迭代出了兩個版本,版本二也只是提供一個設計思路,具體複雜實現基於此套是是能夠知足的。這裏只作簡單的闡述,具體實現能夠結合源碼。後端

  • 版本一數組

    在公司項目【用戶中心】中,我採用的是經過後端菜單列表以及權限標識,作深度遞歸和匹配。這麼寫一部分緣由是由於是在現有的接口文檔基礎之上作 魔改,第二點也是由於代碼耦合度比較高,重構的話週期長(先後端都須要改,前端工做量會很大)。緩存

    Interceptor

    router.beforeEach(async (to, from, next) => {
      NProgress.start();
      if (getToken()) {
        if (to.path === RouterPath.LOGIN) {
          next({ path: RouterPath.HOME });
          NProgress.done();
        } else {
          try {
            if (!store.getters.routerLoadDone) {
              const { isAdmin } = await store.dispatch("getUserInfo");
              const accessRoutes = await store.dispatch("generateRoutes", isAdmin);
              router.addRoutes(accessRoutes);
              let query = to.query;
              query.token = getToken();
              next({ ...to, replace: true, query });
            } else {
              ...
              } else next();
            }
          } catch (error) {
            ...
            NProgress.done();
          }
        }
      } else {
        if (whiteList.includes(to.path)) {
          next();
        } else {
          next(RouterPath.LOGIN);
          NProgress.done();
        }
      }
    });

    Vuex

    actions: {
        ...
        async generateRoutes({ commit }) {
          const { body, status } = await getUserMenuAndPermission({
            appId: +AppId,
          });
          if (status === 200) {
            const { menus, permissions } = body;
            commit("SET_ROLES", permissions);
            const accessedRoutes = LoadMenus(menus, asyncRoutes);
            // isAdmin
            //   ? matchMenuResource(menus, asyncRoutes)
            //   : LoadMenus(menus, asyncRoutes);
            commit("SET_ROUTES", accessedRoutes);
            return accessedRoutes;
          }
          commit("SET_ROUTES", []);
          return [];
        },
    },

    Utils/Menus

    import { MenuType, MenuStatus } from "@/utils/Enum/Menu";
    import _ from "lodash";
    
    /* 白名單 */
    const whiteList = ["404"];
    
    /**
     * 根據權限 加載菜單
     * @param {Array} assessedMenus 權限菜單
     * @param {Array} asyncRoutes 預加載菜單
     */
    export function LoadMenus(assessedMenus, asyncRoutes) {
      const { CATALOG } = MenuType;
      const { ENABLE } = MenuStatus;
    
      /** 先預加載 須要添加的菜單 */
      let menuCache = {},
        childCache = {};
      asyncRoutes.forEach((item) => {
        const { meta, children } = item;
        /* 目錄 */
        if (meta) menuCache[meta.title] = item;
        /* 菜單 */
        if (children) childsForMap(childCache, children, meta.title);
      });
    
      /* 須要加載的目錄 */
      let resultArray = [];
    
      assessedMenus
        .sort((a, b) => a.orderNo - b.orderNo)
        .forEach((item) => {
          const { menuType, menuPerms, menuIcon, subMenus, status } = item;
          /* 目錄 */
          if (+menuType === CATALOG && +status === ENABLE && menuCache[menuPerms]) {
            menuCache[menuPerms].children = [];
            menuCache[menuPerms].children = getChildMenus(
              subMenus,
              childCache,
              menuPerms
            );
            /* 添加 menuIcon */
            menuCache[menuPerms].menuIcon = menuIcon;
            resultArray.push(menuCache[menuPerms]);
          }
        });
    
      /* 添加白名單 */
      whiteList.forEach((white) => {
        if (menuCache[white]) resultArray.push(menuCache[white]);
      });
    
      return resultArray;
    }
    
    /**
     * 獲取子菜單列表
     * @param {Array} subMenus 權限子菜單
     * @param {Object} asyncChildMap 預加載子菜單 Map
     * @param {String} pCode 父級權限標識
     */
    function getChildMenus(subMenus, childCache, pCode) {
      const { MENU } = MenuType;
      const { ENABLE } = MenuStatus;
    
      /* 子菜單集合 */
      let arr = [];
      subMenus
        .sort((a, b) => a.orderNo - b.orderNo)
        .forEach((item) => {
          const { menuPerms, menuType, menuIcon, status } = item;
          const _childMenu = childCache[menuPerms];
          if (+menuType === MENU && +status === ENABLE && _childMenu) {
            /* 添加 menuIcon */
            childCache[menuPerms].menuIcon = menuIcon;
            arr.push(childCache[menuPerms]);
          }
        });
      /* 添加 hidden頁面 */
      if (childCache[pCode] && childCache[pCode].length > 0)
        arr.push(...childCache[pCode]);
      return arr;
    }
    
    /**
     * 子集菜單 作Map
     * @param {Object} cache 子集菜單hMap
     * @param {Array} childs 預處理子集列表
     * @param {String} pCode 父級權限標識
     */
    function childsForMap(cache, childs, pCode) {
      cache[pCode] = [];
      childs.forEach((item) => {
        const { hidden, meta } = item;
        /* 添加隱藏頁 */
        if (hidden) {
          cache[pCode].push(item);
        } else cache[meta.title] = item;
      });
    }
    
    /* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> */
    
    /**
     * 爲解決菜單渲染時,替換權限菜單有效信息:menuIcon
     * @param {Array} assessedMenus 權限菜單
     * @param {Array} asyncRoutes 預加載菜單
     */
    export function matchMenuResource(assessedMenus, asyncRoutes) {
      /** 資源菜單 to Map */
      let menuCache = {};
    
      const { ENABLE } = MenuStatus;
      assessedMenus.forEach(({ menuPerms, menuIcon, subMenus, status }) => {
        /* 啓用狀態 */
        if (+status === ENABLE) {
          /* 目錄 cache */
          if (menuIcon) menuCache[menuPerms] = { menuIcon };
          /* 菜單 cache */
          if (_.isArray(subMenus)) cacheSubMenus(subMenus, menuCache);
        }
      });
    
      /** Router菜單 */
      asyncRoutes.forEach((item) => {
        const { meta, children } = item;
        /* 目錄 */
        if (meta && menuCache[meta.title])
          item.menuIcon = menuCache[meta.title].menuIcon;
        /* 菜單 */
        if (children) matchChildMenus(children, menuCache);
      });
      return asyncRoutes;
    }
    
    /**
     * 緩存菜單
     * @param {Array} subMenus 菜單列表
     * @param {Object} menuCache 資源菜單Map
     */
    function cacheSubMenus(subMenus, menuCache) {
      const { ENABLE } = MenuStatus;
      subMenus.forEach(({ menuPerms, menuIcon, status }) => {
        if (+status === ENABLE) {
          if (menuIcon) menuCache[menuPerms] = { menuIcon };
        }
      });
    }
    
    /**
     * 匹配菜單
     * @param {Array} children 菜單列表
     * @param {Object} menuCache 資源菜單Map
     */
    function matchChildMenus(children, menuCache) {
      children.forEach((item) => {
        const { meta } = item;
        if (meta && menuCache[meta.title])
          item.menuIcon = menuCache[meta.title].menuIcon;
      });
    }
  • 版本二

    網上的博客分享的第二種解決方案

    附上連接 http://www.javashuo.com/article/p-fwjwwmrq-gw.html

    連接2 https://www.jianshu.com/p/ceef589de5e9

    這兩篇博客的文檔大體看了一下,思路是同樣的,原理實際上就是把 Vue-Router 裏面的配置文件所有放到後端作 Json 存儲。特殊字段如:components,在取出的同時動態 require

    但若是結合公司業務,在【用戶中心】項目下的菜單管理頁面,手動添加 目錄、菜單、icon、path 這種方式用起來感受仍是不太友好。

    版本二我設計的思路是動靜分離,Router文件的固定格式不變,具體的名稱、路徑作拆分,分別存到前端和後端。具體實現方式以下文。

動態路由

附上源碼

個人思路是須要權限控制的菜單分紅三部分:

  1. MenuMap 作視圖組件

    export default {
      "/user/manage": () => import("@/views/user/index"),
      "/system/manage": () => import("@/views/system/index"),
    };
  2. RouterJson 作工廠函數

    /**
     * 生成路由json
     */
    function getRouterJson(headPath, childPath, menuName, component) {
      return {
        path: headPath,
        name: "",
        component: Layout,
        meta: {
          title: "",
          //   icon: "",
        },
        children: [
          {
            path: childPath,
            name: headPath + childPath,
            component: component,
            meta: {
              title: menuName,
              //   icon: "",
            },
          },
        ],
      };
    }
    
    /**
     * 404頁面
     */
    function notFound() {
      return { path: "*", redirect: "/404", meta: { title: "404" }, hidden: true };
    }
  3. 動靜分離

    經過 Interceptor 調用後端接口獲取 path、name、icon等權限菜單列表

    結合 Vuex下 generateRoutes 函數組裝路由。

    import Layout from "@/layout/index.vue";
    import menuData from "@/router/menu";
    
    ...
    ...
    
    /**
     * 拆分path
     * @param {string} path
     */
    function splitPath(path) {
      const list = path.split("/");
      let headPath = `/${list[1]}`;
      let childPath = "";
      list.forEach((item, index) => {
        if (index > 1) {
          childPath += `/${item}`;
        }
      });
      return { headPath, childPath: childPath.substr(1, childPath.length - 1) };
    }
    
    /**
     * 根據權限 加載菜單
     * @param {Array} assessedMenus 權限菜單
     * @param {Array} asyncRoutes 預加載菜單
     */
    export function LoadMenus(assessedMenus) {
      let resultArray = [];
      assessedMenus.forEach((item) => {
        const { name, path } = item;
        const { headPath, childPath } = splitPath(item.path);
        if (menuData[path]) {
          resultArray.push(
            getRouterJson(headPath, childPath, name, menuData[path])
          );
        } else {
          resultArray.push(
            getRouterJson(headPath, childPath, item.name, () =>
              import("@/views/404/index")
            )
          );
        }
      });
    
      resultArray.push(notFound());
      return resultArray;
    }

效果展現

相關文章
相關標籤/搜索