1. 自定义指令的弊端
对于按钮级的权限控制,v-permission="'user:update'"
这种方式非常优雅。但当我们将这个指令应用到一个完整的业务组件上时,比如 <Table v-permission="'user:table'" />
,一个更隐蔽的问题就暴露出来了。
我们期望的是,没有权限的用户,既看不到这个表格,与这个表格相关的所有逻辑(比如数据加载 、内部暴露的方法)也不应该执行。 但 v-permission
的实现原理是‘渲染后移除’,它只作用于 DOM。这意味着,虽然表格最终从页面上消失了,但 <Table> 组件的 setup 和 onMounted 生命周期其实已经执行完毕了。
更严重的是,如果这个无权限的组件还通过 defineExpose 暴露了内部方法,父组件依然能通过 ref 调用到它,这就构成了一个潜在的安全漏洞。比如,可以看一下我这里的代码示例:
<script setup>
import { ref } from 'vue'
import Table from './Table.vue'
const tableRef = ref(null)
const handleClick = () => {
tableRef.value?.submit()
}
</script>
<template>
<button @click="handleClick">按钮</button>
<Table v-permission="'user:table'" ref="tableRef" />
</template>
<script setup>
import { ref, onMounted } from "vue";
// 表格数据
const tableData = ref([]);
// 加载状态
const loading = ref(true);
// 模拟异步请求
const fetchData = () => {
loading.value = true;
return new Promise((resolve) => {
setTimeout(() => {
console.log("异步请求");
resolve([
{ id: 1, name: "张三", age: 25 },
{ id: 2, name: "李四", age: 30 },
{ id: 3, name: "王五", age: 28 },
]);
}, 1500); // 模拟1.5秒后返回数据
});
};
// 获取数据
const getUsers = async () => {
tableData.value = await fetchData();
loading.value = false;
};
// 页面挂载时加载
onMounted(() => {
getUsers();
});
defineExpose({
submit() {
console.log('submit');
}
})
</script>
<template>
<div>
<h2>用户列表</h2>
<el-button type="primary" @click="getUsers">刷新数据</el-button>
<el-table v-loading="loading" :data="tableData" border style="width: 100%; margin-top: 16px">
<el-table-column prop="id" label="ID" width="100" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="age" label="年龄" width="120" />
</el-table>
</div>
</template>
2. 解决方法
所以,为了从根本上解决 v-permission‘只治标不治本’的问题,我意识到权限控制不能只停留在 DOM 层面,必须上升到 Vue 的组件渲染层面。
我的解决方案是,封装一个专门的 <Authority> 权限组件。它的核心思想不再是‘渲染后移除’,而是‘渲染前决策’——只有当用户权限校验通过时,才允许被它包裹的业务组件真正地被创建和渲染。 我们可以看一下具体的实现:
所以说使用自定义指令还是有一些问题的,接下来看一下组件级的权限
<template>
<slot v-if="counterStore.user.includes(permission)"></slot>
</template>
<script setup>
import { useConunterStore } from '@/store'
const counterStore = useConunterStore();
defineProps(['permission'])
</script>
<script setup>
import { ref } from 'vue'
import Table from './Table.vue'
import Authority from './Authority.vue'
const tableRef = ref(null)
</script>
<template>
<button @click="handleClick">按钮</button>
<Authority permission="user:table">
<Table ref="tableRef" />
</Authority>
</template>
这个 <Authority> 组件之所以能从根本上解决问题,关键在于它利用了 Vue 最核心的 v-if “真正”条件性渲染机制。
当权限校验不通过时,v-if 会确保被包裹的业务组件根本不会被创建。这意味着,该组件的实例、setup 函数、所有生命周期(包括 onMounted 里的 API 请求)以及 defineExpose 暴露的方法,都将彻底不存在。
这就实现了从“渲染后移除 DOM”到“渲染前阻止实例创建”的质变,不仅杜绝了不必要的 API 请求,更从逻辑层面彻底切断了越权调用方法的可能性,是一种更安全、更彻底的组件级权限控制方案。