首先讓我們瞭解一下前端路由:路由router全部配置在前端,根據用戶權限判斷可以進入哪些頁面
缺點:
- vue初始化的時候需要掛載全部路由,對性能有影響
- 安全性低,用戶可以在地址欄跳轉到無權訪問的頁面(可優化)
動態路由則是根據用戶信息獲取權限,簡單來說就是根據用戶信息獲取其對應的權限,生成對應的路由掛載,然後動態渲染有權限的菜單于側邊欄
實現
- 定義靜態路由(登錄或者公用頁面)、動態路由,vue初始化時只掛載靜態路由
- 用戶登錄後,拿到用戶token,調接口拿到動態路由權限DynamicRoutes,將DynamicRoutes和定義的動態路由比較,篩選出相應的用戶可訪問路由表
- 執行router.addRoutes(DynamicRoutes)添加動態路由
- 使用vuex存儲路由表,根據vuex中可訪問的路由渲染側邊欄sidebar
// beforeEach中
if (getToken() && getToken() !== 'undefined') {
// 權限判斷
if (!store.state.app.menuPermissions) {
/* 獲取後臺給的權限數組 */
return new Promise((resolve, reject) => {
getPermissionList().then(response => {
if (response.data.stat === 1) {
const userRouter = response.data.data
// 檢查並生成新的路由表
const DynamicRoutes = ChecAndSetPermissionRouter(userRouter)
// 默認使/重定向到第一個有效的路由
for (let i = 0, leni = DynamicRoutes.length; i < leni; i++) {
if (DynamicRoutes[i].children.length > 0) {
DynamicRoutes[i].path = '/'
DynamicRoutes[i].redirect = DynamicRoutes[i].children[0].path
break
}
}
DynamicRoutes.push({ path: '*', redirect: '/404', hidden: true }) // 全局404
/* 生成左側導航菜單 */
store.dispatch('SetMenuPermissions', DynamicRoutes)
/* 動態添加路由 */
router.addRoutes(DynamicRoutes)
// /* 完整的路由表 */
store.dispatch('SetRouterPemissions', [...constantRouterMap, ...DynamicRoutes])
next(to)
}
}).catch(error => {
router.push('/404')
// /* 生成左側導航菜單 */
store.dispatch('SetMenuPermissions', [])
next()
reject(error)
})
})
}
if (to.path === '/login') {
next({ path: '/' })
} else {
next()
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next(`/login?redirect=${to.path}`) // 否則全部重定向到登錄頁
}
}
這裏因爲後臺返回的不是標準的路由,所以只能根據後臺返回的權限數據中的菜單名字或者其他唯一性字段到默認定義的動態路由中篩選出有權限的路由組成路由表,這裏的ChecAndSetPermissionRouter方法就是幹這個的(ChecAndSetPermissionRouter方法比較多,放末尾參考)
退出登錄
- 置menuPermissions爲null,這樣下次登錄之後就會重新調用權限接口獲取新的權限,否則另一個用戶登錄之後還是上一個用戶的權限路由
- 重置路由,否則下次addRoutes會提示路由定義重複
this.$store.dispatch('FedLogOut').then(() => {
this.$store.dispatch('SetMenuPermissions', null)
// 重置路由,以防再次進入時提示路由重複
const createRouter = () => new Router({
mode: 'hash',
routes: constantRouterMap
})
const newRouter = createRouter()
// resetRouter()
this.$router.matcher = newRouter.matcher
this.$router.push('/login')
// 清除水印
Watermark.clear()
})
}
踩坑來了
Q:爲什麼404 頁面一定要最後加載,放置在靜態路由中會怎麼樣?
放在靜態路由裏,後面的所以頁面都會被攔截到404,所以應該獲取動態路由權限之後push
Q:權限獲取成功,不跳轉新生成的動態路由,跳404?
beforeEach中router.addRoutes之後的next()可能會失效,因爲可能next()的時候路由並沒有完全add完成,可替換成next(to)
,重新進入router.beforeEach這個鉤子,這時候再通過next()來釋放鉤子,就能確保所有的路由都已經掛在完成了。
Q:$router.addRoutes()動態添加的路由怎麼刪除掉?
在開發中,有新增編輯刪除菜單並要求左側邊欄菜單及時更新的需求,如果直接addRoutes,warn如下:
解決:addRoutes之前要清除掉上次addRoutes的路由,所以操作菜單調取最新權限後重新初始化router,進行matcher賦值
// DynamicRoutes是最新權限路由
const createRouter = () => new Router({
mode: 'hash',
routes: []
})
const newRouter = createRouter()
// resetRouter()
this.$router.matcher = newRouter.matcher
this.$router.addRoutes(DynamicRoutes)
Q:莫名其妙的無限循環
vue-admin-template,遇到二級菜單children爲空的權限,報錯如下:
解決:按照github-issues上方法,在SidebarItem.vue裏改一下data就好了(沒想通爲啥)
// 更改後如下,return {}
data() {
this.onlyOneChild = null
return {}
}
附:ChecAndSetPermissionRouter
import { dynamicRouterMap } from '@/router'
export function ChecAndSetPermissionRouter(permissionDatas) {
// 獲取到權限hashmap
var permissionHashMap = null
permissionHashMap = GetPermissionHashMap(permissionDatas)
// 標記路由表
var newDynamicRouterMap = []
newDynamicRouterMap = objDeepCopy(dynamicRouterMap)
newDynamicRouterMap.forEach(item => {
MarkRouter(null, item, permissionHashMap)
})
// 重設路由表
for (let i = 0; i < newDynamicRouterMap.length; i++) {
if (ResetRouter(newDynamicRouterMap, newDynamicRouterMap[i])) {
i-- // 注意:防止移除後索引錯位
}
}
return newDynamicRouterMap
}
function GetPermissionHashMap(permissionDatas) {
var permissionHashMap = {}
permissionDatas.forEach(item => {
SetKeyValueOfNodes(null, item, permissionHashMap)
})
return Object.assign({}, permissionHashMap)
}
// 深拷貝,遞歸重新設置前端路由表,避免數據複用
function objDeepCopy(source) {
var sourceCopy = source instanceof Array ? [] : {}
for (var item in source) {
sourceCopy[item] = typeof source[item] === 'object' ? objDeepCopy(source[item]) : source[item]
}
return sourceCopy
}
// 爲權限hashmap的屬性賦值,新增屬性tempKey/tempKey2
function SetKeyValueOfNodes(p, c, permissionHashMap) {
// 需要匹配的組合類型
var tempKey = (p ? p.name : 0) + '_' + c.name
var tempKey2 = c.name + '_' + c.name
// 賦值
permissionHashMap[tempKey] = 1
permissionHashMap[tempKey2] = 1
// 遞歸遍歷子節點賦值
if (c.children != null && c.children.length > 0) {
c.children.forEach(item => {
SetKeyValueOfNodes(c, item, permissionHashMap)
})
}
}
// 標記路由表
function MarkRouter(p, c, permissionHashMap) {
var key = (p ? p.meta.title : 0) + '_' + c.meta.title
// 使用拼接的key作爲參考標記去匹配有權限的路由表
if (HasPermission(key, permissionHashMap)) {
if (p != null) {
p.keep = true // 保留當前節點
}
if (c != null) {
c.keep = true
}
}
if (c.children && c.children.length > 0) {
c.children.forEach(item => {
MarkRouter(c, item, permissionHashMap)
})
}
}
// 校驗後端接口是否存在當前節點
function HasPermission(key, permissionHashMap) {
return permissionHashMap[key] === 1
}
// 重置路由表
function ResetRouter(p, c) {
if (c == null) {
return false
}
if (p.children && !c.keep) {
p.children.splice(p.children.indexOf(c), 1)
return true
} else if (!c.keep) {
p.splice(p.indexOf(c), 1)
return true
}
if (c.children && c.children.length > 0) {
for (let i = 0; i < c.children.length; i++) {
if (ResetRouter(c, c.children[i])) {
i-- // 注意:防止移除後索引錯位
}
}
}
return false
}