Vue3 里咋给按钮加权限控制?从场景到实践一次讲透
为啥 Vue3 项目要做按钮权限控制?
咱先聊聊为啥得做按钮权限控制~ 举个实际例子,比如公司后台系统,普通客服只能看订单,不能删订单;运营能删但不能改价格;管理员啥都能操作,要是不做权限控制,客服误点删除按钮,数据就没了,这损失谁扛得住?
从业务安全角度说,权限控制是防止「越权操作」的第一道防线,尤其是涉及资金、用户隐私的功能,必须严格限制谁能点。
另外像金融、医疗这些行业,政策法规对数据操作权限有明确要求,没做权限控制可能合规性都过不了。
还有体验优化层面,要是所有按钮不管有没有权限都堆在界面上,用户找功能时一堆用不了的按钮,体验稀碎,所以按钮权限控制不是可选,是中后台项目刚需~
Vue3 按钮权限常见实现思路有哪些?
Vue3 里常见的实现思路有三类,各有各的适用场景,咱一个个说~
自定义指令(v - permission)
写个 v - permission 指令,好处是模板里写法特简洁,<button v - permission="'btn:add'">添加</button> 一行就搞定,不用在组件里写一堆逻辑,适合项目里权限逻辑比较统一的情况,比如所有按钮权限都通过“是否包含权限码”判断。
组合式函数(usePermission)
写个 usePermission 函数,返回 hasPermission 方法,组件里调用 hasPermission('btn:add') 来控制 v - if,这种方式灵活,能处理复杂逻辑,需要同时有 btn:add 和 btn:edit 权限才能显示」,函数里写条件判断就行。
权限组件封装(PermissionButton)
写个 PermissionButton 组件,传个 code 属性,组件内部判断权限后决定是否渲染按钮,这种适合项目里按钮样式、交互逻辑需要统一管理的情况,比如所有按钮要加 loading 态、统一的 hover 样式,改组件一处就全改了~
用自定义指令实现按钮权限,步骤是啥?
接下来详细说自定义指令咋实现,步骤很清晰:
步骤 1:创建自定义指令文件
新建 directives/permission.js,写指令逻辑:
// directives/permission.js
export const permissionDirective = {
mounted(el, binding) {
// 获取指令绑定的权限码,v - permission="'btn:add'" 里的 'btn:add'
const permissionCode = binding.value;
// 假设用 Pinia 存用户权限,先引入状态管理
const userStore = useUserStore();
const userPermissions = userStore.permissions;
if (permissionCode && !userPermissions.includes(permissionCode)) {
// 没有权限,把按钮从 DOM 里移除
el.parentNode?.removeChild(el);
}
}
};
这里有个小细节:指令的 mounted 钩子是 DOM 渲染后执行,所以能操作 el 的父节点,但如果权限是异步获取的(比如登录后才拿到),指令可能在权限加载前就执行了,这时候得处理异步情况,可以加个 watch 或者在权限加载完成后重新渲染组件~
步骤 2:全局注册指令
在 main.js 里把指令注册到 App 上:
import { createApp } from 'vue';
import App from './App.vue';
import { permissionDirective } from './directives/permission';
const app = createApp(App);
app.directive('permission', permissionDirective);
app.mount('#app');
步骤 3:模板中使用指令
现在任何组件里,给按钮加 v - permission 绑定权限码:
<template> <button v - permission="'btn:add'">添加订单</button> <button v - permission="'btn:delete'">删除订单</button> </template>
扩展:处理禁用而不是隐藏
有时候产品希望保留按钮但置灰,这时候指令里可以改逻辑:
export const permissionDirective = {
mounted(el, binding) {
const { value: code, arg: action = 'hide' } = binding; // arg 用来传操作类型,v - permission:disable="'btn:add'"
const userPermissions = useUserStore().permissions;
if (code && !userPermissions.includes(code)) {
if (action === 'disable') {
el.disabled = true;
el.classList.add('disabled - style'); // 加个禁用样式
} else {
el.parentNode?.removeChild(el);
}
}
}
};
用的时候:
<button v - permission:disable="'btn:add'">添加</button> 这样按钮就会被禁用而不是删除~
如果需要「或权限」(有 A 或 B 权限就能显示),可以让指令支持数组,v - permission="['btn:add', 'btn:edit']",然后在指令里判断是否有一个权限满足:
mounted(el, binding) {
const permissionList = binding.value;
const userPermissions = useUserStore().permissions;
const hasPermission = permissionList.some(code => userPermissions.includes(code));
if (!hasPermission) {
// 处理隐藏或禁用
}
}
这样指令就更灵活啦~
组合式函数方案咋落地?
咱再看组合式函数方案咋落地~
写 usePermission 函数,封装权限判断逻辑
新建 composables/usePermission.js:
// composables/usePermission.js
export function usePermission() {
const userStore = useUserStore();
const hasPermission = (code) => {
return userStore.permissions.includes(code);
};
return { hasPermission };
}
组件内调用函数,控制按钮显示
在需要权限控制的组件里:
<template>
<button v - if="hasPermission('btn:add')">添加订单</button>
<button v - if="hasPermission('btn:delete')">删除订单</button>
</template>
<script setup>
import { usePermission } from '../composables/usePermission';
const { hasPermission } = usePermission();
</script>
优势:灵活处理多权限逻辑
比如业务要求「同时有 btn:add 和 btn:edit 权限才能显示按钮」,函数里加逻辑:
// composables/usePermission.js 扩展
export function usePermission() {
const userStore = useUserStore();
const hasPermission = (codeList) => {
// 如果传的是数组,判断是否所有权限都有;传字符串则判断单个
if (typeof codeList === 'string') {
return userStore.permissions.includes(codeList);
}
return codeList.every(code => userStore.permissions.includes(code));
};
return { hasPermission };
}
组件里用:
<button v - if="hasPermission(['btn:add', 'btn:edit'])">批量添加</button>
封装权限组件有啥好处?咋实现?
封装 PermissionButton 组件,能统一管理按钮的权限逻辑和样式,适合团队协作或大型项目~
好处:一处修改,处处生效
比如所有按钮要加统一的 loading 态、hover 样式,或者权限逻辑要新增“超时隐藏”,只需要改 PermissionButton 组件,不用每个按钮都改。
实现步骤:接收权限码,内部判断权限
新建 components/PermissionButton.vue:
<template>
<button v - if="hasPermission" v - bind="$attrs">{{ defaultSlot }}</button>
</template>
<script setup>
import { useUserStore } from '../stores/user';
import { useSlots } from 'vue';
const props = defineProps({
code: String // 接收权限码
});
const userStore = useUserStore();
const hasPermission = userStore.permissions.includes(props.code);
const slots = useSlots();
const defaultSlot = slots.default?.()[0].children; // 获取插槽内容
</script>
使用方式:传 code 属性,插槽写按钮文案
在其他组件里:
<template> <PermissionButton code="btn:add">添加订单</PermissionButton> <PermissionButton code="btn:delete">删除订单</PermissionButton> </template> <script setup> import PermissionButton from '../components/PermissionButton.vue'; </script>
扩展:支持自定义样式、加载态
如果要给按钮加加载态,组件里扩展:
<template>
<button
v - if="hasPermission"
v - bind="$attrs"
:loading="loading"
>
{{ loading ? '加载中...' : defaultSlot }}
</button>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// ... 之前的逻辑
const loading = ref(false);
onMounted(() => {
// 模拟接口请求加载态
loading.value = true;
setTimeout(() => {
loading.value = false;
}, 1000);
});
</script>
权限数据从哪来?咋和后端配合?
权限数据从哪来?这得前后端配合~ 咱分场景说:
登录时获取基础权限
用户登录成功后,后端接口(/user/info)返回的用户信息里,通常包含 roles(角色列表)和 permissions(按钮权限码列表),前端把这些数据存到状态管理(Pinia 的 userStore 里):
// stores/user.js
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
permissions: []
}),
actions: {
async fetchUserInfo() {
const res = await api.login(); // 假设登录接口返回用户信息
this.permissions = res.data.permissions;
}
}
});
登录流程里调用 fetchUserInfo,把权限存起来,后续所有权限判断都基于这个数组~
细粒度权限:后端配置系统
如果是更细的按钮权限,比如每个按钮对应唯一的 permissionCode(如 btn:order:delete),后端需要有「权限配置系统」,管理员在系统里给角色分配可操作的按钮,用户登录时,后端根据角色返回对应的 permissions 列表。
权限更新的处理
如果用户在系统内切换角色(比如从普通员工切到管理员),或者管理员修改了用户权限,这时候要触发权限重新加载,可以写个 refreshPermissions 方法:
// stores/user.js
actions: {
async refreshPermissions() {
const res = await api.getLatestPermissions();
this.permissions = res.data.permissions;
}
}
然后在角色切换的组件里调用 refreshPermissions,这样所有用到权限的按钮会自动更新(因为 Pinia 的 state 是响应式的,指令或函数里用了这个 state,变化后会触发重新渲染)~
复杂场景(动态权限、多角色)咋处理?
实际项目里总有复杂场景,比如动态权限、多角色咋处理?
场景 1:动态权限(权限中途变化)
用户操作过程中,权限可能被后台修改(比如管理员实时更新了权限),这时候要让按钮权限「实时生效」,做法是让权限数据保持响应式,并在权限变化时触发视图更新。
比如用 Pinia 存权限,因为 Pinia 的 state 是响应式的,当 permissions 数组变化时,所有用到它的地方(指令、函数、组件)都会自动更新。
如果权限是存在普通变量里,没响应式,那修改后视图不会变,这就坑了,所以一定要把权限存在响应式的状态管理里~
场景 2:多角色权限叠加
如果用户有多个角色(比如既是「运营」又是「客服」),权限是取角色权限的并集还是交集?这得看业务规则。
比如后端返回每个角色的 permissions,前端合并去重:
// 登录后处理多角色权限
const roles = res.data.roles; // 假设是 ['operator', 'customer - service']
const allPermissions = [];
for (const role of roles) {
const rolePermissions = await api.getRolePermissions(role);
allPermissions.push(...rolePermissions);
}
userStore.permissions = [...new Set(allPermissions)]; // 去重后存起来
这样用户就有所有角色的权限总和~
场景 3:权限缓存与失效
为了减少接口请求,可以把权限数据存在 localStorage/sessionStorage 里,但要注意缓存失效问题,比如用户权限被修改后,缓存没更新。
解决方案是:登录时先读缓存,若缓存存在且未过期,用缓存;否则请求新权限,当用户主动触发权限更新(比如点「刷新权限」按钮),清除缓存并重新请求~
// 简化的缓存逻辑
const CACHE_KEY = 'user_permissions';
const CACHE_EXPIRE = 60 * 60 * 1000; // 1 小时
async function getPermissions() {
const cache = localStorage.getItem(CACHE_KEY);
if (cache) {
const { data, expire } = JSON.parse(cache);
if (Date.now() < expire) {
return data;
}
}
const res = await api.getPermissions();
const newCache = {
data: res.data,
expire: Date.now() + CACHE_EXPIRE
};
localStorage.setItem(CACHE_KEY, JSON.stringify(newCache));
return res.data;
}
这样平衡了性能和数据准确性~
实际项目里容易踩的坑有哪些?咋避?
实际开发中,这些坑容易踩,咱提前避避~
坑 1:权限码命名混乱
比如有的按钮权限码是 add_order,有的是 order_add,后期维护根本分不清哪个对应哪个。解决方案:定统一规范,模块:操作:资源,像 order:add:btn,这样看权限码就知道是订单模块的添加按钮~
坑 2:指令逻辑没考虑异步权限
如果权限是登录后异步获取的,指令的 mounted 钩子可能在权限加载前就执行了,导致判断错误。解决方案:在权限加载完成前,按钮默认隐藏或禁用,等权限到了再渲染,可以用 v - if 控制整个按钮区域的加载状态:
<template>
<div v - if="permissionsLoaded">
<button v - permission="'btn:add'">添加</button>
</div>
<div v - else>加载中...</div>
</template>
<script setup>
const userStore = useUserStore();
const permissionsLoaded = computed(() => userStore.permissions.length > 0);
onMounted(() => {
userStore.fetchUserInfo(); // 登录后获取权限
});
</script>
坑 3:隐藏按钮导致布局错乱
如果按钮是行内元素,隐藏后旁边元素会移位。解决方案:用 visibility: hidden 代替 removeChild,这样元素占位置但不可见;或者给按钮父元素定宽高,用 flex 布局等,保证布局稳定~
坑 4:响应式丢失
如果把权限存在普通变量里(不是 Pinia/Vuex 的 state),修改后视图不会更新。解决方案:所有权限数据必须存在响应式对象里,利用 Vue 的响应式系统自动更新视图~
怎么给按钮权限写测试?
最后聊聊咋给按钮权限写测试,毕竟逻辑不测试容易出 bug~
自定义指令的单元测试
用 Vitest 测试指令在有权限和无权限时的 DOM 变化:
import { mount } from '@vue/test - utils';
import { createTestingPinia } from '@pinia/testing';
import { permissionDirective } from '../directives/permission';
describe('permissionDirective', () => {
it('有权限时显示按钮', () => {
const wrapper = mount({
template: '<button v - permission="\'btn:add\'">添加</button>'
}, {
global: {
directives: { permission: permissionDirective },
plugins: [createTestingPinia({
initialState: { user: { permissions: ['btn:add'] } }
})]
}
});
expect(wrapper.find('button').exists()).toBe(true);
});
it('无权限时隐藏按钮', () => {
const wrapper = mount({
template: '<button v - permission="\'btn:add\'">添加</button>'
}, {
global: {
directives: { permission: permissionDirective },
plugins: [createTestingPinia({
initialState: { user: { permissions: [] } }
})]
}
});
expect(wrapper.find('button').exists()).toBe(false);
});
}); 版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网



