介绍
前端世贤编辑好页面的各种信息,送给后端。前端请求时,后端需要返回符合权限的页面信息,并实例化生成路由。
配置权限的前提,是前端需要先编写好所有的页面与按钮,并且将正确的组件地址、路由等信息告诉后端;按钮权限也需要先在按钮上编写好权限标识的参数,这样后端提供权限信息时,前端才能控制页面、控制权限展示出来。一般来说,在登录成功的时候,就要获取这些信息了。而提供参数的方法,后台系统一般都有菜单管理,基本都是在这里进行操作。我整理了一张步骤,可以参考一下。
![流程][linktu]
创建静态路由
在router
文件里新建并实例化静态路由,如登录界面、404页面、框架页面等,这些都是不需要后端提供的,在此先生成。
// 配置基础不需要权限的路由,路由权限的判定见permission.js
const routes = [
{
name: 'loginPage',
path: '/login',
component: () => import('@/home/login.vue'),
meta: {
title: '登录',
keepAlive: false
}
},
{
name: '404',
path: '/404',
component: () => import('@/views/404/index.vue'),
meta: {
title: '找不到页面',
keepAlive: false
}
},
{
path: '/',
name:'root', //一定要设置这边的name
component:RootPage,
redirect:'/index',
meta: {
title: '监管报送',
},
children: [
{
name: 'index',
path: '/index',
component: routerReplaceSelf(() => import('@/views/home/index.vue')),
meta: {
title: '首页',
keepAlive: true,
icon: 'home'
},
children:[]
},
]
},
];
const router = new VueRouter({
mode: 'history',
routes,
});
创建管理路由的Vuex文件
在store
里新建一个用于维护路由的router.js
,并填入一下代码,使之可以完成数据的存、取、处理以及清空。这里先不考虑LoadView
方法做的逻辑,先主要看路由的存取。
import routerReplaceSelf from '@/router/routeReplaceSelf';
export default {
namespaced: true,
state: {
//动态路由配置
asyncRouters: []
},
mutations: {
// 设置动态路由
setAsyncRouter (state, asyncRouters) {
state.asyncRouters = asyncRouters;
}
},
actions: {
//获取菜单
SetAsyncRouter ({ commit }, data) {
//获取菜单的路由配置,并配置
let asyncRouters = filterAsyncRouter(data,0);
commit('setAsyncRouter', asyncRouters);
},
ClearAsyncRouter ({ commit }) {
commit('setAsyncRouter', []);
}
},
getters: {
//获取动态路由
asyncRouters (state) {
return state.asyncRouters;
},
}
};
function filterAsyncRouter (routers,level) {
// 遍历后台传来的路由字符串,转换为组件对象
let accessedRouters = routers.filter(router => {
//后端控制菜单的状态为禁用(1)时,跳过这个
if(router.status === '1'){
return false;
}
if (router.meta) {
// 默认图标处理
router.meta.icon = router.meta.icon ? router.meta.icon : "smile";
}
//处理组件---重点
if (!router.componentBackUp) {
router.componentBackUp = router.component;
}
router.name = router.menuCode;
router.component = loadView(router.componentBackUp,level);
//存在子集
if (router.children && router.children.length) {
router.children = filterAsyncRouter(router.children,level+1);
}
return true;
});
return accessedRouters;
}
function loadView (view,level) {
// 路由懒加载
if(level > 0){
return routerReplaceSelf((resolve) => require([`@/views/${view}`], resolve));
}else{
return ((resolve) => require([`@/views/${view}`], resolve));
}
}
维护获取路由方式
在登录成功后,立即获取用户所拥有的权限,并填入本地的路由树。
/*
* 获取菜单信息,
* '1,2,3'代表的是菜单的类型;1-目录,2-项目,3-菜单
* null是parentId,因为要获取所有,就填空告诉后端。
*/
getMenuTreeForView('1,2,3', null).then(res => {
//转义为菜单树所需要的数据格式
let data = this.convertTreeData(res.data);
//发请求获取菜单,并将菜单设置到vuex中,
this.$store.dispatch('router/SetAsyncRouter',data);
this.$router.push({
path: this.redirect || '/index',
query: this.otherQuery
});
})
设置路由守卫
路由守卫在页面的路由发生变化的每一次都会执行。beforeEach
阶段为路由进入之前所需要执行的逻辑。next()
代表路由放行。其中参数的to
代表目标路由、from
代表来源路由、next
是一个放行参数。
registerRouteFresh
是自定义的一个标志,他代表是否已经从vuex获取路由,每当页面刷新时,这个标志会重置,而无刷新的情况下进行路由跳转,则就只使用最开始获取的路由信息。
而在这里,需要注意注释中标记的重点部分。如果有存在component没有被挂载的组件,需要自己再一次挂载。
挂载的结果可以按F12看一下路由信息(console输出 this.$router 后,在options.routes
里的就是路由信息了。),在组件的component
属性,展开时如果有写ƒ VueComponent(options)
,这说明这项路由已经成功被挂载,否则需要注意一下哪里写的不对了。
路由守卫非常容易陷入死循环,所以要注意逻辑。
// 进度条引入设置如上面第一种描述一样
import router from './router';
import store from './store';
import { getToken } from '@/utils/auth'; // get token from cookie
const whiteList = ['/login'];
let registerRouteFresh = false;
router.beforeEach(async (to, from, next) => {
console.log('BEFORE_TO', to);
document.title = `${to.meta.title} - ESRS统一监管报送平台`;
// 获取用户token,用来判断当前用户是否登录
const hasToken = getToken();
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' });
} else {
//异步获取store中的路由
let route = await store.getters['router/asyncRouters'];
const hasRoute = route && route.length > 0;
// //判断store中是否有路由,若有,进行下一部分
if (!(store.getters.baseInfo && store.getters.baseInfo.userName)) {
await store.dispatch('user/getAndSetInfo');
}
if (registerRouteFresh) {
if (to.matched.length === 0) {
next(to.path);
} else {
next();
}
} else {
console.log('没有路由信息');
//store中没有路由,则需要获取获取异步路由,并进行格式化处理
try {
const accessRoutes = (await store.getters['router/asyncRouters']);
console.log(accessRoutes);
// 重点:动态添加格式化过的路由
await accessRoutes.map(asyncRouter => {
console.log('asyncRouter', asyncRouter);
if (!asyncRouter.component) {
let finalUrl = asyncRouter.componentBackUp;
asyncRouter.component =(resolve) => require([`@/views/${finalUrl}`], resolve);
}
router.options.routes[2].children.push(asyncRouter);
router.addRoute('root', asyncRouter);
});
registerRouteFresh = true;
console.log(router.options.routes[2]);
await next({ ...to, replace: true });
} catch (error) {
console.log(error);
next(`/login`);
}
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next();
} else {
next(`/login`);
}
}
});
router.afterEach(() => {
});
路由视图router-view嵌套情况的处理
接着需要关心的是嵌套视图的问题了。后端项目避免不了多级父子菜单的问题。而常规情况下,按理论上来说,有多少个子项,就需要有多少个视图(就是router-view
标签)。
但是,这一次,希望他不论是儿子还是孙子,都只在根节点这边的视图中显示,在往上搜集了资料过后,发现有一个比较普遍的答案,名叫routeReplaceSelf
。(某一篇答案见:https://www.cnblogs.com/senjer/p/15407301.html)
这个自定义的js主要做的是,把子视图挪到父视图中显示。在需要挪动的地方,在其父级的component
属性上包裹一层这个函数即可。
/**
* 将子级的路由界面显示在当前路由界面的组件
* 使用指南:在子级路由的父级上,将此函数包裹在components上。
*/
export default function routeReplaceSelf (component) {
return {
name: 'routerReplaceSelf',
computed: {
showChild () {
const deepestMatchedRoute = this.$route.matched[this.$route.matched.length - 1];
return deepestMatchedRoute.instances.default !== this;
},
cache () {
return this.$route.meta.cache;
},
},
render (h) {
const child = this.showChild ? h('router-view') : h(component);
if (this.cache) {
return h('keep-alive', [child]);
} else {
return child;
}
},
};
}
在动态路由的信息处理时,不能直接将所有的路由信息都包裹这个函数;否则,单击菜单时,会发生路由视图外面的信息丢失的情况。这里主要是在第二级的菜单时,就对接下来的子级路由包裹routeReplaceSelf
。所以在LoadView
函数上,除了传入组件地址之外,再额外传入一个级别的变量,级别随着每一次的递归而自增,就能达到该效果。
当然,我看了一下大佬们的框架,进入子路由视图时,是先创建了ParentView
或者是Layout
等组件里先有了router-view才能实现的效果。但是这样的方法,父级菜单一般无法点击。举例说,RuoYi的菜单管理里,日志模块,拥有两个菜单项,但是“日志管理”本身无法被点击,因为他不算是一个页面,就不太符合我这一次的要求,因为父级菜单在ESRS项目中也要求是一个页面。
按钮权限的处理
在登录或者需要的逻辑里,从后端那获取到了权限信息后,将这个信息数组存到Vuex里。之后我们自定义一个名叫v-hasPermi
的指令,这个指令会将参数跟权限数组里进行比对,如果没有比对到,则证明这个用户没有这个按钮的权限,在页面上就不予展示了。
当然,按钮权限通常跟用户权限挂钩在一起,如果用户没有权限但还能强行发送请求,后端也应该拦截这个请求。这里需要与后端配合,但本次只讲前端如何处理。
新建按钮权限的处理函数hasPermi.js
。
/**
* v-hasPermi 操作权限处理
*/
import store from '@/store';
export default {
inserted (el, binding, vnode) {
const { value } = binding;
const all_permission = "*:*:*";
const permissions = store.getters && store.getters.permission;
if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value;
const hasPermissions = permissions.some(permission => {
return all_permission === permission || permissionFlag.includes(permission);
});
console.log('hasPermissions',hasPermissions);
if (!hasPermissions) {
el.parentNode && el.parentNode.removeChild(el);
}
} else {
throw new Error(`请设置操作权限标签值`);
}
}
};
注册这个自定义指令v-hasPermi
。
import hasPermi from './permission/hasPermi';
const install = function (Vue) {
Vue.directive('hasPermi', hasPermi);
};
if (window.Vue) {
window['hasPermi'] = hasPermi;
Vue.use(install); // eslint-disable-line
}
export default install;
之后就可以在需要的按钮上放置这个自定义的指令;vue就会自己去比对权限信息数组里是否有你指定的权限信息。如果没有,在页面上就不会见到这个按钮了。
<a-button type="link"
icon="edit"
@click="handleEdit(row.data.sid)"
v-hasPermi="['system:menu:edit']">修改</a-button>
<a-button type="link"
icon="delete"
v-hasPermi="['system:menu:delete']"
@click="handleDelete([row.data.sid])">删除</a-button>
看看效果。用户user1
的菜单模块是没有删除和新增两个权限的,而管理员admin
是拥有全部。
然后就可以登录user1和admin两个账号,都进入菜单模块的页面看下权限的效果。发现user1就直接没有了新增和删除两个按钮,说明按钮权限已经正常生效了。