許多時候我們的作的後臺系統,面向的人羣多是五花八門的,後臺系統中展現的數據大部分是公司相關的運營數據,因此呢必須嚴格控制用戶的權限。用戶是否有權訪問這個菜單、用戶訪問這個菜單以後,是否有權進行增刪改查,這都是身爲一個合格滴後臺系統所要具有的功能(敲黑板)。javascript
權限模塊能夠說是後臺系統的重中之重,它可簡單,可複雜,具體看產品大大如何定義。css
平時後臺兄弟的接口返回的數據體結構,都是他說了算,他怎麼給滴,咱就怎麼渲染。可是其實這樣是很被動的,爲了提升咱們的開發效率,咱們要把精力更多的放在頁面上而不是把精力放在絞盡腦汁想怎麼把後臺給的數據遍歷轉化爲我想要的結構,數據的二次處理有時正是咱們被吐槽開發慢的緣由之一呀!(摔杯前端
因此適當的和後臺大兄弟溝通一下返回的數據體的結構,能讓後臺大兄弟處理的,就讓他處理,相信我,其實開口溝通沒那麼難。vue
扯遠了,話又說回來,由於權限模塊的特殊性,因此這一塊返回的結構是怎麼樣的,咱們須要給後臺大兄弟提供大體的維度結構。java
個人項目裏是這樣去定義這個結構的:git
這裏是簡化了的結構,保留了核心字段,在這個項目裏菜單是二級結構的,一級是菜單大類,children表示底下的二級頁面,二級下面就是頁面的路由名稱和該用戶在這個菜單下面擁有的權限,這裏定義了增add、刪delete、改edit、查check四個github
[
{
name: 'Table',
children: [
{
name: 'TableDemo',
auth: {
add: true,
check: true,
delete: true,
edit: true
}
}
]
}
]
複製代碼
假設如今有一個路由是須要權限才能訪問的,咱們在router/modules下定義一個table.js文件,這下面的demo頁是須要後臺返回了相關菜單,用戶纔能有權訪問。vuex
// table.js
const table = {
path: 'table',
component: () => import('@/layout'),
redirect: '/table/demo',
name: 'Table',
meta: {
title: 'parentTitle',
icon: 'table'
},
children: [
{
path: '/table/demo',
name: 'TableDemo',
component: resolve => void require(['@/views/table/demo'], resolve),
meta: {
title: 'tableDemo'
}
},
{
path: '/table/demoTest',
name: 'DemoTest',
component: resolve => void require(['@/views/table/demoTest'], resolve),
meta: {
title: 'demoTest'
}
}
]
}
export default table
複製代碼
mock接口數據,這裏咱們只給用戶了第一個子菜單,第二個不給看element-ui
// mock/index.js
const permissionData = () => {
result.data = [
{
name: 'Table',
children: [
{
name: 'TableDemo',
auth: {
add: true,
check: true,
delete: true,
edit: true
}
}
]
}
]
return result
}
Mock.mock('/apiReplace/permission', 'post', permissionData)
複製代碼
接口數據咱們已經mock中定義了,能夠着手寫如何獲取動態路由的邏輯了json
在store/modules目錄下新建permission.js,咱們須要在vuex中定義路由和權限的邏輯,包括初始化動態路由、重置路由等。
// permission.js
/** 這些在上一篇路由模塊的定義裏有講到,或者是小夥伴能夠去項目裏頭看看router文件,我這裏不貼router文件的代碼了~~ * constantRoutes 常規路由,不須要權限便可訪問 * asyncRoutes 須要訪問權限的路由 * notFoundRoutes 404路由 * resetRouter 重置路由的方法 */
import { asyncRoutes, constantRoutes, notFoundRoutes, resetRouter } from '@/router'
import API from '@/assets/http/apiUrl'
import Request from '@/assets/http'
const permission = {
state: {
routes: [],
addRoutes: [] // 異步加載的路由
},
mutations: {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
},
actions: {
// 獲取動態路由
GenerateRoutes({ commit }, isSuperAdmin) {
resetRouter() // 先初始化路由
return new Promise((resolve, reject) => {
// 若是是超級管理員,掛載所有路由所有權限
if (isSuperAdmin) {
// 重定向404的匹配規則須要在整個完整路由定義的最後面,不然刷新會出錯。
const accessedRoutes = [...asyncRoutes, ...notFoundRoutes]
accessedRoutes.forEach(item => {
if (item.children) {
// 超級管理員賦所有權限
item.children.forEach(elem => {
elem.meta = {
...elem.meta,
check: true,
delete: true,
add: true,
edit: true
}
})
}
})
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
} else {
Request.httpRequest({
method: 'post',
url: API.GetPermissionData,
noLoading: true,
params: {},
success: (data) => {
console.log(data)
let accessedRoutes = []
// 匹配前端路由和後臺返回的菜單
accessedRoutes = filterAsyncRoutes(asyncRoutes, data)
// 重定向404的匹配規則須要在整個完整路由定義的最後面,不然刷新會出錯。
accessedRoutes.push(...notFoundRoutes)
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
},
error: res => {
reject(res)
}
})
}
})
}
}
}
/** * Filter asynchronous routing tables by recursion * 匹配後臺返回的菜單信息和前端定義的路由 * @param routes 前端定義好的異步路由 * @param menus 後臺返回的菜單 */
export function filterAsyncRoutes(routes = [], menus = []) {
const res = []
routes.forEach(route => {
// 複製一遍路由,這樣改變tmp的同時路由不會受影響
const tmp = {
...route
}
// 是否匹配到了
if (hasPermission(menus, tmp)) { // 有符合的匹配項
// 找出那一條匹配成功的路由項
const findMenu = menus.find((menu, index, menus) => {
return menu.name.includes(tmp.name)
})
// 賦權
if (findMenu.hasOwnProperty('auth')) {
tmp.meta = {
...tmp.meta,
...findMenu.auth
}
}
// 若是該路由項中含有子路由,子路由也是須要和菜單進行匹配的
if (findMenu.hasOwnProperty('children') && findMenu.children.length) {
// 子路由匹配的步驟和父路由同樣
tmp.children = filterAsyncRoutes(tmp.children, findMenu.children)
} else {
// 將匹配不到的子路由從路由中刪除
delete tmp.children
}
// 最後獲得的結果就是和後臺返回菜單匹配一致的異步路由值
res.push(tmp)
}
})
return res
}
/** * Use meta.role to determine if the current user has permission * @param menus 後臺返回的菜單 * @param route 前端定義好的異步路由中的項 */
function hasPermission(menus, route) {
// 進行匹配
if (route.name) { // 前提是異步路由要存在name
// 匹配的規則是,name要一致,只要匹配到就返回true,中止繼續往下循環
return menus.some(menu => route.name.includes(menu.name))
} else {
return true
}
}
export default permission
複製代碼
一切都準備就緒了,接下來就剩,咱們應該在哪裏調用生成動態路由的方法呢。我更趨向於,每次切換路由時進行判斷,若是當前用戶是第一次進入項目,則在路由跳轉前,來調用生成動態路由的方法,路由生成以後再往下走。因此咱們能夠在router.beforeEach的鉤子函數中調用生成動態路由的方法。
在src目錄下新建permission.js,用來定義router.beforeEach中的邏輯
import router from '@/router'
import store from '@/store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // Progress 進度條
import 'nprogress/nprogress.css'// Progress 進度條樣式
import getPageTitle from '@/assets/utils/get-page-title'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login', '/register', '/resetPsw'] // 不重定向白名單
router.beforeEach(async(to, from, next) => {
NProgress.start()
// set page title
document.title = getPageTitle(to.meta.title)
// 有無token判斷
const token = localStorage.getItem('ADMIN_TOKEN')
if (token) {
if (whiteList.includes(to.path)) {
next()
NProgress.done()
} else {
// 判斷當前用戶是否是進行了刷新操做,防止進入死循環,若是存在就表示正常跳轉,若是不存在就表示刷新了,vuex中的狀態丟失了,須要從新掛載路由
const hasUser = store.state.user.token
if (hasUser) {
next()
} else {
try {
// 防止進入死循環
await store.commit('SET_TOKEN', token)
// 是否是超級管理員
const isSuperAdmin = store.state.user.roles.some(item => item.id === 1)
const accessRoutes = await store.dispatch('GenerateRoutes', isSuperAdmin)
// 異步加載路由
router.addRoutes(accessRoutes)
router.options.routes = store.state.permission.routes
// 設置replace:true,導航不會留下歷史記錄
next({ ...to, replace: true })
} catch (error) {
// 移除token,重定向到登陸頁
await store.dispatch('ResetToken')
Message.error(error || '身份驗證出錯,請從新登陸。')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
// 沒有token
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
// next(`/login?redirect=${to.path}`) // 不然所有重定向到登陸頁
next('/login') // 不然所有重定向到登陸頁
NProgress.done()
}
}
})
router.afterEach(() => {
NProgress.done() // 結束Progress
})
複製代碼
而後在入口文件引入,全局註冊:
// main.js
import '@/permission'
複製代碼
而後運行項目,你會發現用戶只能訪問第一個子菜單了,是否是不難呢。
細心的大兄弟會發現咱們給每一個頁面路由的meta下頭都定義了增add、刪delete、改edit、查check四個權限。咱們在頁面中經過$route.meta
就能獲取增刪改查的具體權限哦。這裏貼一個栗子,咱們定義一個表格:
<template>
<div class="table-demo">
<el-card class="list-content" shadow="hover">
<template v-if="$route.meta.check">
<el-table
v-loading="tableLoading"
:data="tableData"
:cell-style="{ whiteSpace: 'nowrap'}"
:header-row-style="{ background: '#EBEEF5'}"
style="width: 100%"
class="table-content"
>
<el-table-column
type="index"
label="序號"
align="center"
sortable
width="50"
/>
<el-table-column
v-for="(item,index) in tableHeader"
:key="index"
:prop="index"
sortable
:label="item"
align="center"
/>
<el-table-column
label="操做"
width="230"
align="center"
class-name="operation"
>
<template slot-scope="scope">
<a v-if="$route.meta.edit" class="item" @click="test(scope.row)">修改</a>
<a v-if="$route.meta.delete" class="item" @click="test(scope.row)">刪除</a>
</template>
</el-table-column>
</el-table>
</template>
<div v-else class="no-data">
您暫時沒有查看的權限
</div>
</el-card>
<!-- 分頁 -->
<el-pagination
v-if="$route.meta.check"
:total="total"
:pager-count="5"
:page-sizes="[10, 20, 30, 50]"
:page-size="pageSize"
:current-page="currentPage"
background
layout="total, sizes, prev, pager, next, jumper"
class="pagination"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
複製代碼
咱們能夠根據
$route.meta.add
$route.meta.edit
$route.meta.delete
$route.meta.check
複製代碼
來控制相應入口的顯示與否
還有不少細節的東西沒有詳細寫出來,我這裏貼一下項目地址,有興趣的能夠看一看哦~
效果圖: