前端权限应用-页面访问和菜单
目标
: 在当前项目应用用户的页面访问权限
权限受控的主体思路
到了最关键的环节,我们设置的权限如何应用?
在上面的几个小节中,我们已经把给用户分配了角色, 给角色分配了权限,那么在用户登录获取资料的时候,会自动查出该用户拥有哪些权限,这个权限需要和我们的菜单还有路由有效结合起来
我们在路由和页面章节中,已经介绍过,动态权限其实就是根据用户的实际权限来访问的,接下来我们操作一下
在权限管理页面中,我们设置了一个标识, 这个标识可以和我们的路由模块进行关联,也就是说,如果用户拥有这个标识,那么用户就可以拥有这个路由模块,如果没有这个标识,就不能访问路由模块
用什么来实现?
vue-router提供了一个叫做addRoutes的API方法,这个方法的含义是动态添加路由规则
思路如下
新建Vuex中管理权限的模块
在主页模块章节中,我们将用户的资料设置到vuex中,其中便有权限数据,我们可以就此进行操作
我们可以在vuex中新增一个permission模块
src/store/modules/permission.js
// vuex的权限模块 import { constantRoutes } from '@/router' // vuex中的permission模块用来存放当前的 静态路由 + 当前用户的 权限路由 const state = { routes: constantRoutes // 所有人默认拥有静态路由 } const mutations = { // newRoutes可以认为是 用户登录 通过权限所得到的动态路由的部分 setRoutes(state, newRoutes) { // 下面这么写不对 不是语法不对 是业务不对 // state.routes = [...state.routes, ...newRoutes] // 有一种情况 张三 登录 获取了动态路由 追加到路由上 李四登录 4个动态路由 // 应该是每次更新 都应该在静态路由的基础上进行追加 state.routes = [...constantRoutes, ...newRoutes] } } const actions = {} export default { namespaced: true, state, mutations, actions }
在Vuex管理模块中引入permisson模块
import permission from './modules/permission' const store = new Vuex.Store({ modules: { // 子模块 $store.state.app. // mapGetters([]) app, settings, user, permission }, getters })
Vuex筛选权限路由
OK, 那么我们在哪将用户的标识和权限进行关联呢 ?
我们可以在这张图中,进一步的进行操作
访问权限的数据在用户属性menus
中,menus
中的标识该怎么和路由对应呢?
可以将路由模块的根节点
name
属性命名和权限标识一致,这样只要标识能对上,就说明用户拥有了该权限
这一步,在我们命名路由的时候已经操作过了
接下来, vuex的permission中提供一个action,进行关联
import { asyncRoutes, constantRoutes } from '@/router' const actions = { // 筛选权限路由 // 第二个参数为当前用户的所拥有的菜单权限 // menus: ["settings","permissions"] // asyncRoutes是所有的动态路由 // asyncRoutes [{path: 'setting',name: 'setting'},{}] filterRoutes(context, menus) { const routes = [] // 筛选出 动态路由中和menus中能够对上的路由 menus.forEach(key => { // key就是标识 // asyncRoutes 找 有没有对象中的name属性等于 key的 如果找不到就没权限 如果找到了 要筛选出来 routes.push(...asyncRoutes.filter(item => item.name === key)) // 得到一个数组 有可能 有元素 也有可能是空数组 //【 ☆☆☆ 一定要确保路由里面的name字段是否和数据库返回的标识是否统一,检查一致,否则出现丢失】 }) // 得到的routes是所有模块中满足权限要求的路由数组 // routes就是当前用户所拥有的 动态路由的权限 context.commit('setRoutes', routes) // 将动态路由提交给mutations return routes // 这里为什么还要return state数据 是用来 显示左侧菜单用的 return 是给路由addRoutes用的 }
权限拦截出调用筛选权限Action
在拦截的位置,调用关联action, 获取新增routes,并且addRoutes
// 权限拦截在路由跳转 导航守卫 import router from '@/router' import store from '@/store' // 引入store实例 和组件中的this.$store是一回事 import nprogress from 'nprogress' // 引入进度条 import 'nprogress/nprogress.css' // 引入进度条样式 // 不需要导出 因为只需要让代码执行即可 // 前置守卫 // next是前置守卫必须必须必须执行的钩子 next必须执行 如果不执行 页面就死了 // next() 放过 // next(false) 跳转终止 // next(地址) 跳转到某个地址 const whiteList = ['/login', '/404'] // 定义白名单 router.beforeEach(async(to, from, next) => { nprogress.start() // 开启进度条的意思 if (store.getters.token) { // 只有有token的情况下 才能获取资料 // 如果有token if (to.path === '/login') { // 如果要访问的是 登录页 next('/') // 跳到主页 // 有token 用处理吗?不用 } else { // 只有放过的时候才去获取用户资料 // 是每次都获取吗 // 如果当前vuex中有用户的资料的id 表示 已经有资料了 不需要获取了 如果没有id才需要获取 if (!store.getters.userId) { // 如果没有id才表示当前用户资料没有获取过 // async 函数所return的内容 用 await就可以接收到 const { roles } = await store.dispatch('user/getUserInfo') // 如果说后续 需要根据用户资料获取数据的话 这里必须改成 同步 // 筛选用户的可用路由 // actions中函数 默认是Promise对象 调用这个对象 想要获取返回的值话 必须 加 await或者是then // actions是做异步操作的 const routes = await store.dispatch('permission/filterRoutes', roles.menus) // routes就是筛选得到的动态路由 // 动态路由 添加到 路由表中 默认的路由表 只有静态路由 没有动态路由 // addRoutes 必须 用 next(地址) 不能用next() router.addRoutes(routes) // 添加动态路由到路由表 铺路 // 添加完动态路由之后 next(to.path) // 相当于跳到对应的地址 相当于多做一次跳转 为什么要多做一次跳转 // 进门了,但是进门之后我要去的地方的路还没有铺好,直接走,掉坑里,多做一次跳转,再从门外往里进一次,跳转之前 把路铺好,再次进来的时候,路就铺好了 } else { next() } } } else { // 没有token的情况下 if (whiteList.indexOf(to.path) > -1) { // 表示要去的地址在白名单 next() } else { next('/login') } } nprogress.done() // 解决手动切换地址时 进度条不关闭的问题 }) // 后置守卫 router.afterEach(() => { nprogress.done() // 关闭进度条 })
静态路由动态路由解除合并
注意: 这里有个非常容易出问题的位置,当我们判断用户是否已经添加路由的前后,不能都是用next(),
在添加路由之后应该使用 next(to.path), 否则会使刷新页面之后 权限消失,这属于一个vue-router的已知缺陷
同时,不要忘记,我们将原来的静态路由 + 动态路由合体的模式 改成 只有静态路由 src/router/index.js
此时,我们已经完成了权限设置的一半, 此时我们发现左侧菜单失去了内容,这是因为左侧菜单读取的是固定的路由,我们要把它换成实时的最新路由
在src/store/getters.js
配置导出routes
const getters = { sidebar: state => state.app.sidebar, device: state => state.app.device, token: state => state.user.token, name: state => state.user.userInfo.username, // 建立用户名称的映射 userId: state => state.user.userInfo.userId, // 建立用户id的映射 companyId: state => state.user.userInfo.companyId, // 建立用户的公司Id映射 routes: state => state.permission.routes // 导出当前的路由 } export default getters
在左侧菜单组件中, 引入routes
computed: { ...mapGetters([ 'sidebar', 'routes' ]),
OK,到现在为止,我们已经可以实现不同用户登录的时候,菜单是动态的了
<span style="background-color:red;color:#fff">注意: ☆☆☆, 如果在拥有管理员角色的情况下不能拥有所有菜单,请检查router里面定义的路由里的name字段内容是否和后端数据库存储的标识符一致</span>
提交代码
本节任务
前端权限应用-页面访问和菜单
登出时,重置路由权限和 404问题
目标: 处理当登出页面时,路由不正确的问题
上一小节,我们看似完成了访问权限的功能,实则不然,因为当我们登出操作之后,虽然看不到菜单,但是用户实际上可以访问页面,直接在地址栏输入地址就能访问
这是怎么回事?
这是因为我们前面在addRoutes的时候,一直都是在加,登出的时候,我们并没有删,也没有重置,也就是说,我们之前加的路由在登出之后一直在,这怎么处理?
大家留意我们的router/index.js文件,发现一个重置路由方法
// 重置路由 export function resetRouter() { const newRouter = createRouter() router.matcher = newRouter.matcher // 重新设置路由的可匹配路径 }
没错,这个方法就是将路由重新实例化,相当于换了一个新的路由,之前加的路由
自然不存在了,只需要在登出的时候, 处理一下即可
// store/modules/user.js // 导入重置路由方法 import { resetRouter } from '@/router' // 登出的action lgout(context) { // 删除token context.commit('removeToken') // 不仅仅删除了vuex中的 还删除了缓存中的 // 删除用户资料 context.commit('removeUserInfo') // 删除用户信息 // 重置路由 resetRouter() // 还有一步 vuex中的数据是不是还在 // 要清空permission模块下的state数据 // vuex中 user子模块 permission子模块 // 子模块调用子模块的action 默认情况下 子模块的context是子模块的 // 父模块 调用 子模块的action context.commit('permission/setRoutes', [], { root: true }) // 子模块调用子模块的action 可以 将 commit的第三个参数 设置成 { root: true } 就表示当前的context不是子模块了 而是父模块 }
除此之外,我们发现在页面刷新的时候,本来应该拥有权限的页面出现了404,这是因为404的匹配权限放在了静态路由,而动态路由在没有addRoutes之前,找不到对应的地址,就会显示404,所以我们需要将404放置到动态路由的最后
src/permission.js
router.addRoutes([...routes, { path: '*', redirect: '/404', hidden: true }]) // 添加到路由表
提交代码