vue-element-admin 后台動態加載菜單;動態路由


dynamic-router

在線訪問

Online

項目地址

Gitee
Github

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

后端: Go的語法簡潔,開發速度快,所以后端使用了Go結合Gin框架,做了一個簡單的CURD服務。(主要是因為沒人手協助,也只好自己寫一個了。)這里不過多介紹后端。

前言

動態路由

由於公司業務需要,在權限驗證與安全方面,要求是菜單根據權限動態控制。

在現有的項目當中,通過基於 vue-element-admin 項目提供的菜單權限控制,無法滿足公司的具體業務需要。

實際上主要的目的是通過后端控制菜單權限。

因此也迭代出了兩個版本,版本二也只是提供一個設計思路,具體復雜實現基於此套是是可以滿足的。這里只做簡單的闡述,具體實現可以結合源碼。

  • 版本一

    在公司項目【用戶中心】中,我采用的是通過后端菜單列表以及權限標識,做深度遞歸和匹配。這么寫一部分原因是因為是在現有的接口文檔基礎之上做 魔改,第二點也是因為代碼耦合度比較高,重構的話周期長(前后端都需要改,前端工作量會很大)。

    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;
      });
    }
    
  • 版本二

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

    附上鏈接 https://segmentfault.com/a/1190000021900731

    鏈接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;
    }
    

效果展示


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM