许多时候我们的作的后台系统,面向的人群多是五花八门的,后台系统中展现的数据大部分是公司相关的运营数据,因此呢必须严格控制用户的权限。用户是否有权访问这个菜单、用户访问这个菜单以后,是否有权进行增删改查,这都是身为一个合格滴后台系统所要具有的功能(敲黑板)。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
复制代码
来控制相应入口的显示与否
还有不少细节的东西没有详细写出来,我这里贴一下项目地址,有兴趣的能够看一看哦~
效果图: