vue项目通过路由控制来实现的权限管理
通过路由也就是菜单来管理权限的方式,通常分为两种:
1. 前端控制
静态路由,前端将路由写死,登录的时候根据返回的角色权限(level等级),来动态展示路由
2. 后端控制
动态路由,后台返回角色对应的权限路由,前端通过调用接口结合导航守卫进行路由添加
先说下第一种方式,前端控制的实现思路:
前端将路由写死,也就是将所有的路由映射表都拿到前端来维护,和我们不做菜单权限管理时一样,在router.js里面配置好所有的路由
然后在登录的时候获取角色对应的level存入storage中,在侧边菜单栏组件的cretaed中根据level处理路由,给匹配的路由添加hidden属性
最后我们用处理后的数据渲染菜单栏
这种方式存在比较明显的缺点,也是router.js写死的缺点,那就是:
我们如果记住了path,可以直接在浏览器网址栏中手动输入path,然后回车就可以看到任何页面。
再重点说下第二种方式,后端控制的实现思路,这也当前是常用到的一种权限控制方式:
这种方式我们通常会将一些不需要权限的路由写死在router.js之中,比如login和404页面等
routes: [{ path: '/login', name: 'login', component: () => import('./views/Login/index'), hidden: true, meta: { title: '登陆' } }]
而其他的路由有两种处理方式,要么全部由我们的后端返回,
要么定义一个routerList.js将组件资源放到里面,然后通过后端返回的路由去做匹配,将匹配成功的通过addRouters添加到路由中
routerList.js
import LayOut from '@/components/layOut/index' export const mockRouter = [ { path: '/activeIssue', component: LayOut, redirect: '/activeIssue/index', meta: { title: '活动发布', }, children: [ { path: 'specialList', name: 'activeIssue_specialList', component: () => import('@/views/activeIssue/specialList.vue'), meta: { title: '专场活动列表', } }, { name: 'activeIssue_banner', path: 'banner', component: () => import('@/views/activeIssue/banner.vue'), meta: { title: '首页banner', } } ] }]
这里再说一下我们的项目结构,通常会是在app.vue中有一个router-viev用来渲染我们的登录页面和主页面,
我们定义一个Layout组件作为主页面,而在我们的Layout组件中再分为侧边菜单栏和渲染对应page的router-view,这个router-view也就是我们渲染大多页面的容器了。
这里的Layout就是上面routerList.js引入的Layout组件。
而动态添加路由这个方法要写到导航守卫beforeEach这个钩子函数中,这样可以避免写在登录后的页面刷新丢失后台返回的路由表。
导航守卫的意思是我路由跳转到下个页面之前要做些什么,就是说我们登录后会跳到主页面,在进到这个页面之前我们需要将后端请求回来的路由表进行二次封装,
根据返回的路由与我们前端的routerList.js去做匹配,需要做些什么根据需要来定,最后将处理后的路由通过router的addRoutes方法添加到我们的路由中,
之后再进入到我们的主页面,通过主页面进入对应的page页面,也就是说权限控制(菜单权限)需要在进入主页面之前完成。
总结大致步骤:beforEach拦截路由 => 接口获取路由 => vuex保存路由 => 路由匹配处理 => 添加路由 => 跳转进入主页面
定义一个permisson.js来做路由处理:
import router from '@/router' //引入路由 import NProgress from 'nprogress' // progress bar import store from '@/store' //引入状态机 import 'nprogress/nprogress.css' // 获取token import { getSession } from '@/utils/saveStroage' import { mockRouter } from '@/assets/js/routerList.js'; //本地routerList import LayOut from '@/components/layOut/index' //LayOut组件 import errorPage from '@/views/error/404.vue' import Vue from 'vue'; let saveMenu = []; let activeIssue = {}; function clearHttpRequestingList(){ //清除cancleToken请求列表 if (Vue.$httpRequestList.length > 0) { Vue.$httpRequestList.forEach((item) => { item() }) Vue.$httpRequestList = [] } } // 将后台返回的菜单进行筛选,返回对应的菜单名称,组成一维数组 function formatMenu(arr) { for (let i = 0; i < arr.length; i++) { if (arr[i].children) { saveMenu.push(arr[i].menuName) formatMenu(arr[i].children); } else { saveMenu.push(arr[i].menuName) } } return saveMenu; } // 递归筛选路由 function screenRoute(userRouter = [], allRouter = []) { var realRoutes = allRouter .filter(item => { if (item.meta && item.meta.title != '活动发布') { return userRouter.includes(item.meta.title) } else { return item } }) .map(item => ({ ...item, children: item.children ? screenRoute(userRouter, item.children) : null })) return realRoutes } // 添加路由 function addRout(arr) { arr.filter(t => { // 一级菜单 if (t.level == 'levelOne' && !t.children) { t.component = LayOut; t.type = 'One' t.redirect = t.path + '/index'; t.children = [{ path: 'index', meta: { title: t.menuName }, component: () => import('@/views' + t.path + '/index'), }] } // 多级菜单 else { // 当等级为一级时,添加title及引入模块 if (t.level == 'levelOne') { t.component = LayOut; t.meta = { title: t.menuName } t.redirect = t.children[0].path } // 反之添加路径 else { t.component = () => import('@/views' + t.path); t.meta = { title: t.menuName } } } if (t.children && t.type != 'One') { addRout(t.children); } }) return arr; } // 路由替换,所有具有详情的路由替换成固定路由 function replaceDetails(arr) { mockRouter.filter(t => { arr.filter((r, index) => { if (t.path == r.path) { arr[index].children = [ ...arr[index].children, ...t.children ]; } }) }) return arr; } router.beforeEach(async (to, from, next) => { clearHttpRequestingList(); // start progress bar NProgress.start() document.title = to.meta.title; let token = getSession('token') || ''; const menuList = store.state.menuList && store.state.menuList.length > 0; if (token) { //已登录 if (to.path == '/login') { next({ path: '/' //进入登录页面 }) NProgress.done() } else { if (menuList) { //已获取路由 next(); } else { //未获取路由 try { let { data } = await store.dispatch('getUserMenu'); //接口获取路由 let addRouter = addRout(data); //路由处理成符合routes选项要求的数组(addRoutes的参数必选是一个符合 routes 选项要求的数组) replaceDetails(addRouter); //按需要接口返回路由和本地routerList.js做匹配处理 let t = [{ path: '/', name: 'Home', hidden: true, redirect: addRouter[0].redirect, }, { name: 'error', hidden: true, meta: { title: '404' }, path: '/404', component: errorPage }, { path: "*", hidden: true, redirect: "/404" }]; addRouter.push(...t); //根据vue-router中的匹配优先级来最后addRoutes 404和*这个页面,这样就可以在访问非权限或不存在页面时直接到达404页面而非空页面。 router.options.routes = addRouter; //手动添加,解决在addroutes后,router.options.routes不会更新的问题 // console.log('addRouter',addRouter) router.addRoutes(addRouter); //添加路由 store.commit('getMenuList', data); next({ ...to, replace: true }) } catch (error) { await store.dispatch('resetToken'); next(`/login?redirect=${to.path}`) NProgress.done() } } } } else { //未登录 if (to.name == 'login') { next(); } else { next(`/login?redirect=${to.path}`) NProgress.done() } } }) router.afterEach(() => { // finish progress bar NProgress.done() })
在main.js中引入promisson.js
import Router from 'vue-router' import store from './store' import './assets/js/permisson.js'
通过这种方式来控制权限,能够很好的解决我们在浏览器导航栏改path,进入对应页面的问题,
这样操作会回归到导航守卫中,当我addRoutes后如果没有匹配到这个值就跳转到404页面。