阅读 912

在Vue项目中优雅的实现权限控制

对于前端开发而言,有一类很大的业务场景,就是管理后台。在这些管理后台系统中,不可避免的一个很重要的需求,就是权限控制。

可能会遇到的问题

权限控制可以粗分为页面级页面元素级。换成人话就是:这个菜单只有管理员可以看到,这个页面某某某不能访问,这个按钮没有权限不能点击,这个区域别让普通用户看到!!

接下来我们将产品经理的人话,转换成技术开发的鬼话。

假设某用户拥有一系列权限编码(permissionCodes):

  • 如果某个菜单需要code=100才能访问,且当前用户不拥有该code,则该菜单不渲染
  • 如果某个菜单需要code=100才能访问,且当前用户不拥有该code,则用户直接访问该菜单对应的页面时,提示无权限
  • 如果某个按钮需要code=200才能访问,且当前用户不拥有该code,则将该按钮的disabled属性设置为true
  • 如果某个区域需要code=200才能访问,且当前用户不拥有该code,则将该区域的display设置为none

大致流程如下:

image

具体实现

获取用户权限列表

我们需要在渲染之前拿到用户权限列表,可以先从服务端请求获取到当前用户信息,再进行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

让我们先把问题拆解一下:

  1. 如何判断用户是否有某个组件的权限?
  2. 如何将组件的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是反模式 (不推荐的) 的。

image

所以,在开发模式下,当我们直接在组件内修改prop时,会得到一条警告。

image

不幸的是,这里不符合官方文档所说的大多数情况,所以我们选择了强行忽略警告

这种方法有个问题就是,如果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)。

还有什么

如果系统比较庞大,涉及到的权限非常多非常复杂,那么一开始把用户所有的权限拉取到本地,可能就不是一个非常合适的做法了。这时候就要结合实际的系统架构,再根据上面提供的基本方法来灵活处理了。

关注下面的标签,发现更多相似文章
评论