开场白
在中后台系统中,「权限控制」几乎是必不可少的一环。最常见、也看起来最优雅的方案,就是封装一个 v-permission 指令,在模板中一行搞定:
vue
<el-button v-permission="'permission_xxx'">操作</el-button>
但在一次真实项目中,这种"看似优雅"的方案,却让我踩了一个非常隐蔽、却极其致命的坑 。 本文结合真实案例,聊聊:为什么权限封装,远不只是写一个指令那么简单。
问题背景:权限明明有,按钮却时隐时现
项目背景:
- Vue 2 + Element UI
- 表格使用
el-table - 权限通过
Vuex管理 - 使用自定义
v-permission指令控制按钮显示
问题现象非常诡异:
用户明确拥有权限,但动态渲染的表格中的按钮时隐时现不稳定
刷新页面、切换路由,偶现/必现不一致
而同一权限,在非表格页面是正常显示的
原始实现:v-permission指令(简化版)
js
export default {
inserted(el, binding) {
const { value } = binding
const roles = store.getters.roles
if (value && Array.isArray(value)) {
const hasPermission = roles.some(role =>
value.includes(role)
)
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
}
}
}
设计初衷很简单:
有权限 → 保留 DOM
没权限 → 直接移除 DOM
看起来没毛病,对吧?
真正的坑:指令 + 动态表格 = 隐形雷区
问题的根源,藏在三个「底层细节」里
inserted 是"一次性生命周期"
inserted 只会在 DOM 插入时执行一次:
- 权限数据 异步加载
- 表格行 动态渲染
- 指令不会等待数据"稳定"
结果就是:指令执行时,权限数据可能还没准备好,哪怕10ms后权限就加载完成,指令也不会重新执行。
el-table 是"高度敏感组件"
el-table 的内部逻辑非常复杂:
- 行高度计算
- 虚拟渲染
- DOM复用
- slot动态插入
而v-permission做了什么?直接、粗暴地修改真实 DOM。
js
el.parentNode.removeChild(el)
这会导致:Vue的Virtual DOM与真实 DOM状态不一致,表格行高度计算被破坏,某些行"渲染过一次就不再回来"。
命令式 DOM 操作 vs 声明式渲染
这是最本质的冲突:
| 概念 / 维度 | Vue | v-permission |
|---|---|---|
| 渲染方式 | 声明式渲染 | 命令式 DOM 操作 |
| 数据驱动 | UI = f(state),由数据决定渲染结果 | 指令直接删节点 |
| 更新机制 | 批量更新,异步处理 DOM 变更 | 同步修改 DOM,立即生效 |
| 响应性 | 自动追踪依赖并更新 | 不响应权限数据变化,执行一次性操作 |
| 安全性 | Virtual DOM与真实 DOM一致 |
破坏Virtual DOM与真实 DOM对应关系 |
| 可维护性 | 状态改变自动刷新 UI | UI 状态与数据脱节,难调试 |
在普通页面里,这种冲突不明显,但在动态表格中,会被无限放大
那权限不是要"封装"吗?
1.封装成全局 mixin(Vue2)
js
// src/mixins/permissionMixin.js
export default {
computed: {
$hasPermission() {
return (code) => {
const roles = this.$store.getters.roles || []
return roles.includes(code)
}
}
}
}
// main.js
import Vue from 'vue'
import permissionMixin from '@/mixins/permissionMixin'
Vue.mixin(permissionMixin)
// 具体使用页面
<template>
<el-button v-if="$hasPermission('permission_xxx')">操作</el-button>
</template>
2.封装成组合函数(Vue3 / Composition API)
js
// src/composables/usePermission.js
import { computed } from 'vue'
import store from '@/store'
export function usePermission() {
const roles = computed(() => store.getters.roles || [])
const hasPermission = (code) => roles.value.includes(code)
return { hasPermission }
}
// 具体使用页面
<script setup>
import { usePermission } from '@/composables/usePermission'
const { hasPermission } = usePermission()
</script>
<template>
<el-button v-if="hasPermission('permission_xxx')">操作</el-button>
</template>
3.全局权限函数 / 工具方法
js
// src/utils/permission.js
export function hasPermission(code) {
const roles = store.getters.roles || []
return roles.includes(code)
}
<template>
<el-button v-if="hasPermission('permission_xxx')">操作</el-button>
</template>
4.封装权限组件<Auth>/<Permission>
js
// components/Auth.vue
// 用组件包裹需要权限控制的内容,组件内部判断权限。
<template>
<slot v-if="allowed" />
</template>
<script>
import { hasPermission } from '@/utils/permission'
export default {
props: { code: { type: String, required: true } },
computed: {
allowed() {
return hasPermission(this.code)
}
}
}
</script>
<Auth code="permission_xxx">
<el-button>操作</el-button>
</Auth>
权限封装方式详细对比表(包含原理与适用场景)
| 封装方式 | 原理 | 优点 | 适用场景 | 响应式 | 可复用性 | 维护成本 |
|---|---|---|---|---|---|---|
| 1️⃣ 全局 mixin(Vue 2) | 利用 Vue mixin 将权限判断方法注入所有组件,通过计算属性或方法访问 Vuex 中的角色信息 | 页面无需重复写 computed,权限逻辑集中,易管理 | Vue 2 中小到中型项目、按钮或表格权限 | 响应式 | 全局可用 | 低 |
| 2️⃣ 组合函数(Vue 3 / Composition API) | 利用 Composition API 的响应式特性,将角色数据封装成 computed 或函数,返回权限判断方法,可在 setup 中直接使用 |
响应式,逻辑可复用,可扩展复杂组合(OR/AND),支持动态表格和路由权限 | Vue 3 项目、大型项目、复杂权限场景 | 响应式 | 高 | 中低 |
| 3️⃣ 全局函数 / 工具方法 | 将权限判断逻辑封装成纯函数,读取 Vuex 或其他全局状态,返回 true/false | 简单直观,页面调用简洁 | 小型项目或单独组件权限 | ⚠️ 需配合 computed 才能响应式 | 高 | 低 |
4️⃣ 权限组件 <Auth> / <Permission> |
封装为组件,内部根据权限判断是否渲染 slot 内容,利用 Vue 响应式系统自动更新 DOM | 语义清晰,可嵌套组合,多权限场景可读性高,易维护 | 中大型项目,复杂权限场景,多层 slot 或动态表格 | 响应式 | 高 | 中 |
写在最后
权限控制看似是一个简单的功能点,但在实际项目中,它的实现方式直接影响系统的稳定性和可维护性。通过本次项目经验,我们可以总结出几个重要的教训:
- 权限判断必须依赖响应式状态,保证动态变化时 UI 自动更新
- 显示 / 隐藏由 Vue 控制 ,使用
v-if、v-show或权限组件实现 - 避免在指令中直接操作 DOM ,否则可能破坏 Vue
虚拟 DOM与真实 DOM的一致性
权限控制不仅仅是"功能实现",更是前端工程能力的体现。理解 Vue 的响应式和声明式渲染机制,结合合适的封装方式,才能构建稳定、可扩展的权限体系。