1. 程式人生 > >Vue管理系統前端系列六動態路由-許可權管理實現

Vue管理系統前端系列六動態路由-許可權管理實現

[toc] ----------------------- ## 為什麼要使用動態路由? 一般系統中,都會有不同許可權的操作人員,這時候他們看到的頁面也將會不同,因此都需要根據他們的許可權來生成對應的選單,這個就得通過動態路由來實現。 ## 主流的兩種實現方式 控制一般都是由前端的路由中設定。後端返回路由表動態生成兩種。 本文主要記錄由資料庫維護的動態路由實現,和相關注意點。即 退出 和 重新整理 相關點導致的路由問題。 其它的可參考文章:[動態路由](https://juejin.im/post/6844904145267195917) ### 前端控制 - 不用後端控制,只用返回角色、 - 根據可能有的角色,在對應路由上維護相關角色 - 在登入後,判斷路由中維護的角色是否吻合來動態新增生成 ### 後端控制 - 路由存在資料庫中,可動態維護。且相對安全。 - 登入後,獲取動態路由資訊。 - 得到路由後,匹配檔案,生成路由,新增 ## 後端控制路由 實現 由於我這裡是用 `fastmock` 模擬的資料,實際中還請自行生成。 mock 資料如下: ```js { "code": 200, "success": true, "data": [ { "menuId": 2, "menuName": "一級選單", "parentMenuId": 0, "url": "menu/singleMenu/index", "type": 1, "icon": "el-icon-wind-power", "orderIndex": 1, "children": [ ] }, { "menuId": 3, "menuName": "二級選單", "parentMenuId": 0, "url": "", "type": 1, "icon": "el-icon-ice-cream-round", "orderIndex": 1, "children": [ { "menuId": 301, "menuName": "二級1-1", "parentMenuId": 3, "url": "menu/secondMenu/second1-1", "type": 2, "icon": "el-icon-milk-tea", "orderIndex": 1, "children": [ ] }, { "menuId": 302, "menuName": "二級1-2", "parentMenuId": 3, "url": "menu/secondMenu/second1-2", "type": 2, "icon": "el-icon-potato-strips", "orderIndex": 2, "children": [ ] }, { "menuId": 303, "menuName": "二級1-3", "parentMenuId": 3, "url": "menu/secondMenu/second1-3", "type": 2, "icon": "el-icon-lollipop", "orderIndex": 3, "children": [ ] } ] }, { "menuId": 4, "menuName": "三級多級選單", "parentMenuId": 0, "url": "", "type": 1, "icon": "el-icon-ice-cream-round", "orderIndex": 1, "children": [ { "menuId": 401, "menuName": "三級1-1", "parentMenuId": 4, "url": "menu/threeMenu/three1-1", "type": 2, "icon": "el-icon-milk-tea", "orderIndex": 1, "children": [ ] }, { "menuId": 402, "menuName": "二級1-2", "parentMenuId": 4, "url": "", "type": 2, "icon": "el-icon-potato-strips", "orderIndex": 2, "children": [ { "menuId": 40201, "menuName": "三級1-2-1", "parentMenuId": 402, "url": "menu/threeMenu/nextMenu/three1-2-1", "type": 2, "icon": "el-icon-milk-tea", "orderIndex": 1, "children": [ ] }, { "menuId": 40202, "menuName": "三級1-2-2", "parentMenuId": 402, "url": "menu/threeMenu/nextMenu/three1-2-2", "type": 2, "icon": "el-icon-potato-strips", "orderIndex": 2, "children": [ ] } ] } ] } ], "message": "成功" } ``` ### 新增選單介面 及 選單狀態管理 由於這裡是 mock 的。所以就判斷了下登入使用者名稱。程式碼如下: ```js export const getMenu = (username) => { if (username == 'user') { return axios.Get('api/usermenu') } else { return axios.Get('api/menu') } } ``` 狀態管理用於儲存當前理由載入狀態,和選單值。 再在 `actions` 中新增一個獲取選單的方法,完整程式碼如下: ```js //引入介面 import { getMenu } from '@/api/modules/system' export default { state: { menuTree: [], menuLoad: false, //選單是否已載入狀態避免重複載入,重新整理又將變為false。 }, getters: { menuLoad: (state) => { return state.menuLoad }, }, mutations: { setMenuLoad(state, menuLoad) { state.menuLoad = menuLoad }, setMenuTree(state, menuTree) { state.menuTree = menuTree }, }, actions: { getMenuTree({ commit }, username) { return new Promise((resolve, reject) => { getMenu(username) .then((res) => { if (res.code === 200) { if (res.success) { commit('setMenuTree', res.data) } else { // TODO 處理錯誤訊息 } resolve(res.data) } }) .catch((error) => { reject(error) }) }) }, }, } ``` ### 根據得到的選單生成動態路由 在這裡由於退出時,會導致路由和載入狀態不會更新,也不會重置路由的原因,完整程式碼中包含相關處理。 ```js import Vue from 'vue' import VueRouter from 'vue-router' import login from '@/views/login' import store from '@/store' import { getMenu } from '@/api/modules/system' Vue.use(VueRouter) const originalPush = VueRouter.prototype.push VueRouter.prototype.push = function push(location) { return originalPush.call(this, location).catch((err) => err) } const routes = [ { path: '/', name: 'home', component: () => import('@/layout'), children: [ { path: '', name: 'index', component: () => import('@/views/home/defaultPage'), meta: { title: '首頁', index: 0, }, }, ], }, { path: '/login', name: 'login', component: login, meta: { title: '登入', }, }, { path: '/notfound', name: 'notfound', component: () => import('@/views/notfound'), meta: { title: '未找到', }, }, ] const defultRouter = () => { return new VueRouter({ routes: routes, }) } //每次使用預設路由 const router = defultRouter() // 解決addRoute不能刪除動態路由問題 export function resetRouter() { const reset = defultRouter() router.matcher = reset.matcher } const WhiteListRouter = ['/login', '/notfound'] // 路由白名單 //導航守衛 路由開始前 router.beforeEach(async (to, from, next) => { let user = store.getters.userInfo let token = store.getters.token var hasAuth = user !== null && token !== null && user !== undefined && token !== undefined if (to.path == '/login') { if (hasAuth) { next({ path: '/' }) } else { next() } } else { if (!hasAuth) { //沒登入的情況下 訪問的是否是白名單 if (WhiteListRouter.indexOf(to.path) !== -1) { next() } else { next({ path: '/login', query: { redirect: to.fullPath, }, }) } } else { if (store.state.app.menuLoad) { // 已經載入過路由了 next() return } else { console.log(user.username) // 使用 await 進行同步處理 const menu = await store.dispatch('getMenuTree', user.username) console.log(menu) // 載入動態選單和路由 addDynamicMenuRoute(menu) //next({ ...to, replace: true }); // hack方法 確保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record next() } } } }) //重新整理 載入完後 載入未找到路由 此方法只會在重新整理後加載一次 router.onReady(() => { var notfund = [ { path: '*', redirect: '/notfound', name: 'notfound', component: () => import('@/views/notfound'), meta: { title: '未找到', }, }, ] router.options.routes = router.options.routes.concat(notfund) router.addRoutes(router.options.routes) }) /** * 載入動態選單和路由 */ function addDynamicMenuRoute(menuData) { if (store.state.app.menuRouteLoaded) { console.log('已載入選單和路由.') return } // 根據返回的選單 拼裝路由模組 let dynamicRoutes = addDynamicRoutes(menuData) // 處理靜態元件繫結路由 router.options.routes[0].children = router.options.routes[0].children.concat(dynamicRoutes) //新增路由 router.addRoutes(router.options.routes) // 儲存載入狀態 store.commit('setMenuLoad', true) } /** * 新增動態(選單)路由 * @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].menuName, meta: { title: menuList[i].menuName, icon: menuList[i].icon, index: menuList[i].menuId, }, } try { // 根據選單URL動態載入vue元件,這裡要求vue元件須按照url路徑儲存 // 如url="menu/singleMenu/index",則元件路徑應是"@/views/menu/singleMenu/index".vue",否則將找不到改元件 let url = menuList[i].url route['component'] = (resolve) => require([`@/views/${url}`], resolve) } catch (e) {} routes.push(route) } } if (temp.length >= 1) { addDynamicRoutes(temp, routes) } return routes } export default router ``` ### 根據 vuex 中的暫存的選單生成側邊選單欄 新建選單元件 遞迴生成選單,新建 `menuTree/index.vue`,程式碼如下: ```vue ``` 在側邊欄中,從 state 中得到選單,生成側邊欄選單,完整程式碼如下: ```vue ``` ### 退出後重置 vuex 因為只要登入過,那麼當前狀態中的 活動窗體 肯定是有值的,那麼只需要判斷該值是否有,有就重新整理一下介面。 這裡使用的是 reload 來重新整理頁面。 ```js created() { //若是使用狀態退出 則重新整理一下 重置vuex if (this.$store.state.app.mainTabsActiveName != '') { window.location.reload() } }, ``` 重新整理的方式可以參考:[vue 重新整理當前頁的三種方法](https://juejin.im/post/6844904000261718023) ### 最終效果 * [線上預覽-github](https://levy-w-wang.github.io/lion-ui/dist/#/) * [github程式碼地址](https://github.com/levy-w-wang/lion-ui) * [線上預覽-gitee (推薦國內使用者)](https://levy_w_wang.gitee.io/lion-ui/dist/#/) * [gitee程式碼地址](https://gitee.com/levy_w_wang/lion-ui) 原文地址:[http://book.levy.net.cn/doc/frontend/uiframe/dynamic_router.html](http://book.levy.net.cn/doc/frontend/uiframe/dynamic_rout