对于前端开发而言,有一类很大的业务场景,就是管理后台。在这些管理后台系统中,不可避免的一个很重要的需求,就是权限控制。
可能会遇到的问题
权限控制可以粗分为页面级
和页面元素级
。换成人话就是:这个菜单只有管理员可以看到,这个页面某某某不能访问,这个按钮没有权限不能点击,这个区域别让普通用户看到!!
接下来我们将产品经理的人话,转换成技术开发的鬼话。
假设某用户拥有一系列权限编码(permissionCodes
):
- 如果某个菜单需要
code=100
才能访问,且当前用户不拥有该code,则该菜单不渲染 - 如果某个菜单需要
code=100
才能访问,且当前用户不拥有该code,则用户直接访问该菜单对应的页面时,提示无权限 - 如果某个按钮需要
code=200
才能访问,且当前用户不拥有该code,则将该按钮的disabled
属性设置为true
- 如果某个区域需要
code=200
才能访问,且当前用户不拥有该code,则将该区域的display
设置为none
大致流程如下:
具体实现
获取用户权限列表
我们需要在渲染之前拿到用户权限列表,可以先从服务端请求获取到当前用户信息,再进行Vue
实例化。
$fetch.getPermissions(token)
.then(user => {
Vue.prototype.$user = user;
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app');
});
假设我们需要渲染一个登录页呢?我们只需要把登录服务独立于主应用即可,另外增加一个入口,或者Vue
实例都可以解决这个问题。
控制菜单渲染
控制菜单渲染很简单,因为菜单通常是一个配置化的数组,只需要把当前用户无权限的菜单过滤即可。
// sidebar menus
const asideMenus = [
{
code: '100',
name: '首页',
path: '/home',
},
{
code: '500',
name: '系统设置',
path: '/manage',
children: [
{
code: '510',
name: '用户设置',
path: 'user',
},
{
code: '520',
name: '访问设置',
path: 'visit',
},
],
},
];
// Vue component options
{
computed: {
asideMenus() {
const { $user } = this;
return (function filter(arr) {
return arr.filter(menu => {
if (Array.isArray(menu.children)) {
menu.children = filter(menu.children);
}
if (menu.children && menu.children.length) {
return true
} else if (menu.code && $user && $user.codes) {
return ~$user.codes.indexOf(menu.code);
} else {
return true;
}
})
})(asideMenus);
},
},
}
限制路由访问
Vue
路由提供了通用的路由拦截钩子,方便我们做权限控制。
const noPermissionPage = '';
router.beforeEach((to, from, next) => {
const code = to.meta.code;
const user = router.app.$user;
if (code && user && user.codes && !~user.codes.indexOf(code)) {
next(noPermissionPage);
} else {
next();
}
});
禁用按钮操作/隐藏部分区域
接下来,我们要来到本文的核心部分:如何根据权限禁用按钮操作和隐藏部分区域?
在Vue
类型的项目中,页面中的元素几乎都是Vue component
,所以这个问题我们可以等同于:如何根据权限将组件的disabled
属性或者visible
属性设置为false
?
让我们先把问题拆解一下:
- 如何判断用户是否有某个组件的权限?
- 如何将组件的disabled属性或者visible属性设置为false?
接下来我们就来解决这两个问题。
我们可以很容易的想到下面的实现思路:
// 代码片段
<el-button :disabled="$user && $user.codes && !~$user.codes.indexOf('200')"></el-button>
<div v-show="!$user || !$user.codes || ~$user.codes.indexOf('300')">some content</div>
这种方案可以解决问题,但是写起来比较繁杂,在维护上也比较困难。另外,如果在用户有权限的前提下,disabled
属性或者v-show
可能还受其它条件的影响,这时候写出来的表达式就会更复杂和难以维护。
接下来介绍另外一种思路:
import Vue from 'vue';
Vue.directive('p', {
bind: handler,
update: handler,
});
function handler(el, binding, vnode) {
const user = vnode.context.$user;
const code = binding.arg || binding.value;
const prop = binding.modifiers.visible ? 'visible': 'disabled';
const value = prop !== 'visible';
if (code && user && user.codes && !~user.codes.indexOf(code)) {
const vm = vnode.componentInstance;
if (vm && vm.hasOwnProperty(prop)) {
const silent = Vue.config.silent;
Vue.config.silent = true; // 强行忽略警告
vm[prop] = value;
Vue.config.silent = silent;
} else {
if (prop === 'visible') {
el.classList.add('display-none');
} else if (prop === 'disabled') {
el.setAttribute('disabled', true);
el.classList.add('is-disabled');
}
}
}
}
使用方式:
<sa-button type="primary" v-p:200>操作</sa-button>
<p v-p:300.visible>操作提示</p>
怎么样,这样写起来是不是舒服很多?
接下来解释一下,我们是如何在指令中去直接修改组件的属性的?
在Vue
官方文档中明确提及,组件内修改prop
是反模式 (不推荐的) 的。
所以,在开发模式下,当我们直接在组件内修改prop
时,会得到一条警告。
不幸的是,这里不符合官方文档所说的大多数情况,所以我们选择了强行忽略警告
。
这种方法有个问题就是,如果disabled
属性还可能受其它条件影响时,就会达不到我们预期的效果。比如:
<!-- 权限的优先级大于submitting条件 -->
<sa-button type="primary" :disabled="submitting" v-p:200>操作</sa-button>
<p v-p:300.visible>操作提示</p>
通常,用户所拥有的权限是不变的,所以我们可以在bind
钩子函数里面,根据权限直接将属性的值固化掉。下面是最终的实现方式:
import Vue from 'vue';
Vue.directive('p', {
bind: function (el, binding, vnode) {
const user = vnode.context.$user;
const code = binding.arg || binding.value;
const prop = binding.modifiers.visible ? 'visible': 'disabled';
const value = prop !== 'visible';
if (code && user && user.codes && !~user.codes.indexOf(code)) {
const vm = vnode.componentInstance;
if (vm && vm._props.hasOwnProperty(prop)) {
const silent = Vue.config.silent;
const property = Object.getOwnPropertyDescriptor(vm._props, prop);
Object.defineProperty(vm._props, prop, { ...property, get() { return value } });
Vue.config.silent = true; // 强行忽略警告
property.set(value); // 触发computed依赖更新
Vue.config.silent = silent;
} else {
if (prop === 'visible') {
el.classList.add('display-none');
} else if (prop === 'disabled') {
el.setAttribute('disabled', true);
el.classList.add('is-disabled');
}
}
}
}
});
如果不理解为什么会这么实现,可以去看下Vue
源码中的initProps
方法(位于src/core/instance/state.js#L64-L110
)。
还有什么
如果系统比较庞大,涉及到的权限非常多非常复杂,那么一开始把用户所有的权限拉取到本地,可能就不是一个非常合适的做法了。这时候就要结合实际的系统架构,再根据上面提供的基本方法来灵活处理了。
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 楼东 原文链接:https://juejin.im/post/6844903999737430023