如何用一个自定义指令搞定Vue前端权限控制(告别满屏 v-if)

 更新时间:2026年08月05日 10:03:54   作者:keyV  
经常做中后台管理系统的同学都知道系统权限控制的功能,这篇文章主要介绍了如何用一个自定义指令搞定Vue前端权限控制的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

前言

在企业级应用开发中,权限控制是一个绑不开的话题。前端权限控制虽然不能替代后端校验,但能极大提升用户体验——让用户只看到自己能操作的内容,避免无效点击和困惑。

本文将分享一个 Vue 2 自定义指令的设计思路,实现了声明式的权限控制方案。

设计目标

在动手写代码之前,我先梳理了几个核心诉求:

  1. 使用简单:一行代码搞定权限控制,不需要写一堆 v-if
  2. 性能友好:同一权限不重复请求,利用缓存
  3. 灵活可控:支持隐藏、禁用、提示等多种交互方式
  4. 支持多场景:既能用在模板里,也能在 JS 逻辑中调用

核心实现

1. 权限缓存设计

权限校验通常需要请求后端接口,如果每个按钮都单独请求一次,那页面性能会非常糟糕。这里采用了 Promise 缓存 的方式:

Vue.prototype.$getPerm = (options) => {
    const argId = options.type + '-' + (options.type === 'space' ? options.spaceId : options.wikiId)
    
    // 如果缓存中没有,创建新的 Promise
    if (!spaceInfoStore.permissionMap[argId]) {
        spaceInfoStore.permissionMap[argId] = new Promise((resolve, reject) => {
            // 请求后端获取权限...
            Auth.getUserPermBySpaceId(options.spaceId).then((res) => {
                const permissions = res.reduce((acc, category) => {
                    category.permissions.forEach(permission => {
                        acc.push(`${category.value}.${permission.authorityKey}`)
                    })
                    return acc
                }, [])
                resolve({ permissions })
            })
        })
    }
    return spaceInfoStore.permissionMap[argId]
}

这个设计的巧妙之处在于:缓存的是 Promise 本身,而不是结果。

这样做的好处是,即使多个组件同时调用 $getPerm,也只会发出一次请求。后续的调用会直接拿到同一个 Promise,等待第一次请求的结果。

2. 双重使用方式

为了覆盖不同的使用场景,我设计了两种调用方式:

方式一:指令式(模板中使用)

<button v-perm:[permOptions]="'SPACE.EDIT'">编辑</button>

方式二:编程式(JS 中使用)

this.$hasPerm({ spaceId: 123 }, 'SPACE.EDIT').then(() => {
    // 有权限,执行操作
}).catch(() => {
    // 无权限
})

指令式适合静态权限控制,编程式适合需要在逻辑中判断的场景。两者底层共用同一套缓存机制。

3. DOM 处理策略

无权限时如何处理 DOM?这里提供了三种策略:

function domHandler (el, binding) {
    let placeholderDom = null
    
    if (binding?.arg?.showTips || binding?.arg?.disabled) {
        // 策略1&2:保留元素,但禁用或添加点击提示
        placeholderDom = el.cloneNode(true)
        
        if (binding?.arg?.showTips) {
            placeholderDom.onclick = function () {
                Vue.prototype.$bkMessage({
                    message: binding?.arg?.tipsText || '没有权限',
                    theme: 'warning'
                })
            }
        }
        
        if (binding?.arg?.disabled) {
            placeholderDom.classList.add('disabled')
        }
    } else {
        // 策略3:完全隐藏,用注释节点占位
        placeholderDom = document.createComment('permission-placeholder')
    }
    
    el.placeholderDom = placeholderDom
    el.parentNode.replaceChild(placeholderDom, el)
}

为什么用注释节点而不是直接 display: none?

因为注释节点不会影响布局,也不会被 CSS 选择器选中。更重要的是,我们需要保留一个"锚点",方便权限变化时把原始元素恢复回去。

4. 响应式更新

权限可能会动态变化(比如用户被授权后刷新),所以指令需要同时监听 inserted 和 update 钩子:

Vue.directive('perm', {
    inserted (el, binding) {
        handlerPerm(el, binding)
    },
    update (el, binding) {
        handlerPerm(el, binding)
    }
})

恢复元素的逻辑也很简单:

function restoreElement (el) {
    if (el.placeholderDom && el.placeholderDom.parentNode) {
        el.placeholderDom.parentNode.replaceChild(el, el.placeholderDom)
    }
    el.placeholderDom = null
    return true
}

5. 支持布尔值快捷方式

有时候权限结果已经在外部计算好了,不需要再走一遍接口校验。这种场景下,支持直接传布尔值会更方便:

<button v-perm:[options]="hasPermission">操作</button>
if (typeof binding.value === 'boolean') {
    if (binding.value === false) {
        domHandler(el, binding)
    } else {
        restoreElement(el)
    }
    return
}

使用示例

基础用法

<template>
    <div>
        <!-- 无权限时隐藏 -->
        <button v-perm:[permConfig]="'SPACE.DELETE'">删除</button>
        
        <!-- 无权限时禁用并提示 -->
        <button v-perm:[permConfigWithTips]="'SPACE.EDIT'">编辑</button>
    </div>
</template>

<script>
export default {
    computed: {
        permConfig() {
            return {
                spaceId: this.currentSpaceId,
                type: 'space'
            }
        },
        permConfigWithTips() {
            return {
                spaceId: this.currentSpaceId,
                type: 'space',
                disabled: true,
                showTips: true,
                tipsText: '您没有编辑权限,请联系管理员'
            }
        }
    }
}
</script>

编程式调用

// 在执行敏感操作前校验
async handleDelete() {
    try {
        await this.$hasPerm({ spaceId: this.spaceId }, 'SPACE.DELETE')
        // 有权限,继续执行删除逻辑
        await this.doDelete()
    } catch {
        // 无权限,$hasPerm 内部已经弹出提示
    }
}

清除缓存重新加载

当权限发生变化时(比如管理员授权后),可以清除缓存重新加载:

this.$getPerm({ 
    spaceId: this.spaceId, 
    clearCache: true 
})

设计总结

特性实现方式
性能优化Promise 缓存,同一权限只请求一次
使用方式指令式 + 编程式双重支持
DOM 处理隐藏 / 禁用 / 提示三种策略
响应式inserted + update 钩子联动
灵活性支持布尔值、清除缓存、自定义提示

可以优化的点

  1. Vue 3 适配:Vue 3 的指令钩子函数名称有变化(mounted、updated),迁移时需要调整
  2. TypeScript 支持:可以为 PermissionOptions 添加完整的类型定义
  3. 批量权限查询:如果页面上有大量权限点,可以考虑合并成一次批量查询
  4. 权限预加载:在路由守卫中预加载权限数据,减少页面白屏时间

以上就是这个权限指令的完整设计思路。核心思想是:用缓存换性能,用指令换简洁。希望对你有所启发,欢迎交流讨论 🙌

完整代码

最后贴一下完整代码,可以直接拿去用,根据自己项目的接口改一下就行:

// permission.js
import { useSpaceInfoStore } from '@/store/modules/spaceInfo'
import Auth from '@/api/modules/auth'

let spaceInfoStore = null
setTimeout(() => {
    spaceInfoStore = useSpaceInfoStore()
}, 40)

/**
 * @typedef {Object} PermissionOptions
 * @property {string|number} [wikiId] - 文档id
 * @property {string|number} [spaceId] - 空间id
 * @property {string} [type] - 权限类型:空间/文档   space/wiki
 * @property {boolean} [disabled] - 是否禁用元素
 * @property {boolean} [showTips] - 是否显示提示信息
 * @property {string} [tipsText] - 提示文本内容
 * @property {boolean} [clearCache] - 是否清除缓存
 */

function install (Vue) {
    /**
     * 获取权限信息
     * @param {PermissionOptions} options - 权限选项
     * @returns {Promise<any>}
     */
    Vue.prototype.$getPerm = (options) => {
        if (!options.spaceId) return
        // 如果没有传type,则根据是否有文档id判断
        options.type = options.type || (options.wikiId ? 'wiki' : 'space')
        const argId = options.type + '-' + (options.type === 'space' ? options.spaceId : options.wikiId)
        // 清除缓存权限,可重新加载
        if (options.clearCache) {
            spaceInfoStore.permissionMap[argId] = false
        }
        if (!spaceInfoStore.permissionMap[argId]) {
            spaceInfoStore.permissionMap[argId] = new Promise((resolve, reject) => {
                if (options.type === 'space') {
                    Auth.getUserPermBySpaceId(options.spaceId).then((res) => {
                        // 组合权限生成唯一key
                        const permissions = res.reduce((acc, category) => {
                            category.permissions.forEach(permission => {
                                acc.push(`${category.value}.${permission.authorityKey}`)
                            })
                            return acc
                        }, [])
                        resolve({ permissions })
                    })
                } else {
                    Auth.getWikiPermissionDetail(options.spaceId, options.wikiId).then((res) => {
                        // 组合权限生成唯一key
                        const permissions = res.map(item => item.authorityKey)
                        resolve({ permissions })
                    })
                }
            })
        }
        return spaceInfoStore.permissionMap[argId]
    }

    /**
     * 检查是否有权限
     * @param {PermissionOptions} options - 权限选项
     * @param {string} perm - 权限码
     * @returns {Promise<boolean>}
     */
    Vue.prototype.$hasPerm = (options, perm) => {
        if (!Object.prototype.hasOwnProperty.call(options, 'showTips')) {
            options.showTips = true
        }
        return new Promise((resolve, reject) => {
            if (!options.spaceId) {
                resolve(true)
                return
            }
            const promise = Vue.prototype.$getPerm(options)
            promise.then((res) => {
                if (res.isAdmin) {
                    resolve(true)
                    return
                }
                if (res.permissions.includes(perm)) {
                    resolve(true)
                    return
                }
                if (options.showTips) {
                    Vue.prototype.$bkMessage({
                        message: options.tipsText || '没有权限',
                        theme: 'warning'
                    })
                }
                reject(new Error(''))
            })
        })
    }

    /**
     * DOM 处理函数 - 处理无权限时的元素显示
     * @param {HTMLElement} el - DOM 元素
     * @param {Object} binding - 指令绑定对象
     */
    function domHandler (el, binding) {
        let placeholderDom = null
        if (binding?.arg?.showTips || binding?.arg?.disabled) {
            placeholderDom = el.cloneNode(true)
            if (binding?.arg?.showTips) {
                placeholderDom.onclick = function () {
                    Vue.prototype.$bkMessage({
                        message: binding?.arg?.tipsText || '没有权限',
                        theme: 'warning'
                    })
                }
            }
            if (binding?.arg?.disabled) {
                placeholderDom.classList.add('disabled')
            }
        } else {
            placeholderDom = document.createComment('permission-placeholder')
        }
        if (el.parentNode) {
            el.placeholderDom = placeholderDom
            el.parentNode.replaceChild(placeholderDom, el)
        }
    }

    /**
     * 将元素恢复到原始位置
     * @param {HTMLElement} el - DOM 元素
     * @returns {boolean}
     */
    function restoreElement (el) {
        el.placeholderDom && el.placeholderDom.parentNode.replaceChild(el, el.placeholderDom)
        el.placeholderDom = null
        return true
    }

    /**
     * 权限处理函数
     * @param {HTMLElement} el - DOM 元素
     * @param {Object} binding - 指令绑定对象
     */
    function handlerPerm (el, binding) {
        // 通过直接传递boolean值,也可以进行权限校验
        if (typeof binding.value === 'boolean') {
            if (binding.value === false) {
                domHandler(el, binding)
            } else {
                restoreElement(el)
            }
            return
        }
        // 判断权限入参是否完善
        if (!binding?.arg?.spaceId || !binding?.value) return restoreElement(el)
        const promise = Vue.prototype.$getPerm({ ...binding.arg })
        promise.then((res) => {
            if (res.isAdmin) return restoreElement(el)
            if (res.permissions.includes(binding.value)) return restoreElement(el)
            domHandler(el, binding)
        })
    }

    Vue.directive('perm', {
        inserted (el, binding) {
            handlerPerm(el, binding)
        },
        update (el, binding) {
            handlerPerm(el, binding)
        }
    })
}

export default { install }

在 main.js 里注册一下就能用了:

import permission from '@/directives/permission'
Vue.use(permission)

总结 

到此这篇关于如何用一个自定义指令搞定Vue前端权限控制的文章就介绍到这了,更多相关Vue前端权限控制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • vue3  mark.js 实现文字标注功能(案例代码)

    vue3  mark.js 实现文字标注功能(案例代码)

    这篇文章主要介绍了vue3  mark.js 实现文字标注功能,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-09-09
  • Vue指令工作原理实现方法

    Vue指令工作原理实现方法

    自定义指令是 vue 中使用频率仅次于组件,其包含 bind 、 inserted 、 update 、 componentUpdated 、 unbind 五个生命周期钩子,接下来通过本文给大家详细介绍Vue指令工作原理实现方法,需要的朋友参考下吧
    2021-06-06
  • web项目开发中2个Token原因解析及示例代码

    web项目开发中2个Token原因解析及示例代码

    这篇文章主要介绍了web项目开发中会出现2个Token原因的解析以及实现的示例代码,有需要的同学可以借鉴参考下,希望可以有所帮助
    2021-09-09
  • Vue通过moment插件实现获取当前月的第一天和最后一天

    Vue通过moment插件实现获取当前月的第一天和最后一天

    这篇文章主要介绍了Vue 结合插件moment 实现获取当前月的第一天和最后一天,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2023-10-10
  • Vue状态管理工具Vuex工作原理解析

    Vue状态管理工具Vuex工作原理解析

    Vuex是一个专为Vue.js应用程序开发的状态管理模式,下面这篇文章主要给大家介绍了关于Vue中状态管理器(vuex)详解以及实际应用场景的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-02-02
  • vue3中的setup()函数基本使用详解

    vue3中的setup()函数基本使用详解

    在 Vue3 中,setup 函数是一个新引入的概念,它代替了之前版本中的 data、computed、methods 等选项,用于设置组件的初始状态和逻辑,本文将主要介绍Setup的基本用法和少量原理,感兴趣的朋友一起看看吧
    2024-02-02
  • Vue3实现Element Plus表格的多选功能与条件操作

    Vue3实现Element Plus表格的多选功能与条件操作

    Element-plus是ElementUI的升级版,是一套基于vue2与vue3的桌面端组件库,它提供了丰富的组件帮助开发人员快速构建功能强大、风格统一的页面,本文给大家介绍了Vue3实现Element Plus表格的多选功能与条件操作,需要的朋友可以参考下
    2024-08-08
  • vue el-select与el-tree实现支持可搜索树型

    vue el-select与el-tree实现支持可搜索树型

    本文主要介绍了vue el-select与el-tree实现支持可搜索树型,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • vue3 与 vue2 优点对比汇总

    vue3 与 vue2 优点对比汇总

    随着用vue3 的开发者越来越多,其必定是又她一定的有带你,接下来这篇文章小编就为大家介绍vue3 对比 vue2 有什么优点?感兴趣的小伙伴请跟小编一起阅读下文吧
    2021-09-09
  • vue离开页面时如何销毁定时器

    vue离开页面时如何销毁定时器

    这篇文章主要介绍了vue离开页面时如何销毁定时器,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-05-05

最新评论