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文件的固定格式不變,具體的名稱、路徑作拆分,分別存到前端和後端。具體實現方式以下文。
動態路由
附上源碼
個人思路是須要權限控制的菜單分紅三部分:
-
MenuMap 作視圖組件
export default { "/user/manage": () => import("@/views/user/index"), "/system/manage": () => import("@/views/system/index"), };
-
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 }; }
-
動靜分離
經過 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; }