Vue管理系統前端系列六動態路由-許可權管理實現
阿新 • • 發佈:2020-08-22
[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