Vue权限按钮别写散

Vue权限按钮别写散
Vue 项目里的权限问题,最开始通常从一个按钮开始。产品说“这个删除按钮只有管理员能看”,开发就在模板里加一段 v-if="user.role === 'admin'"。过几天又来一个导出按钮,只允许运营负责人使用;再后来菜单、路由、表格操作列都要按权限控制。项目跑着跑着,权限判断就散在几十个组件里。
散着写的好处是快。需求来了,找到按钮,加一个判断,测试一下就能上线。坏处是后面没人知道权限规则到底在哪里。某个角色为什么能看到这个按钮?某个页面为什么能进不能点?新增一个角色要改哪些地方?这些问题会越来越难回答。
这篇文章聊 Vue 里的按钮权限,但重点不是写一个 v-permission 指令就结束。按钮权限只是权限体系最容易暴露混乱的地方。真正要先拆清楚的是:路由权限解决能不能进入,菜单权限解决能不能发现,按钮权限解决能不能执行。三者相关,但不能混成一团。
这张图把权限拆成三层:路由、菜单、按钮。它们都和用户角色有关,但判断时机、失败表现和维护方式并不一样。
从一个删除按钮开始,混乱通常很安静
先看一个常见写法:
  1. <button v-if="user.role === 'admin'" @click="remove(row)">删除button>
vue
这一行代码本身没什么大问题。如果项目只有一个管理员角色、一个删除按钮,它甚至是最直接的方案。但真实项目会继续变化。角色不再只有 admin,还会有 owner、operator、auditor;删除不再只看角色,还要看数据状态;某些团队只能删除自己创建的数据;某些按钮在灰度期间只开放给部分账号。
于是判断变成这样:
  1. <button
  2. v-if="user.role === 'admin' || (user.role === 'operator' && row.status === 'draft' && row.ownerId === user.id)"
  3. @click="remove(row)"
  4. >
  5. 删除
  6. button>
vue
这时问题已经出现了。模板开始承载业务规则,组件开始知道太多权限细节。下次另一个页面也要删除同类数据,很可能复制一份类似判断。两份判断只要有一点不一致,就会出现“列表页能删,详情页不能删”或者“按钮隐藏了,但接口还能调”的问题。
所以我判断按钮权限是否需要治理,不看按钮数量,而看规则是否开始被复制。只要同一条权限规则出现在两个以上地方,就应该考虑收口。
按钮判断散在组件里,短期看是局部改动,长期会变成全局不确定。权限问题最怕的不是代码多,而是没人知道哪一处才是准的。
路由、菜单、按钮不要互相冒充
权限治理第一步,是不要让不同层级互相冒充。
路由权限回答“用户能不能进入这个页面”。如果没有权限,通常应该跳转到无权限页、登录页或上一级页面。它是页面级边界。
菜单权限回答“用户能不能看到这个入口”。菜单隐藏不代表接口安全,也不代表路由一定不能访问。它主要影响导航体验,避免用户看到自己无法使用的入口。
按钮权限回答“用户能不能执行这个动作”。它通常和具体资源、数据状态、业务规则有关。比如同样是“删除”,草稿能删,已发布不能删;自己创建的能删,别人创建的不能删。
这三层如果混在一起,会出现很奇怪的问题。比如有人用菜单权限判断路由访问,结果用户通过链接直接进入页面时没有拦住。有人用路由权限判断按钮显示,结果页面能进但某些数据行不该操作。还有人以为前端隐藏按钮就等于安全,后端接口却没有校验。
前端权限主要负责体验和减少误操作,真正的安全边界必须在后端。这个判断要先说清楚,否则前端权限很容易被赋予过高期待。
权限码比角色名更适合前端判断
很多早期项目直接用角色名判断,比如 adminmanageroperator。这种做法简单,但扩展性差。因为角色是人的集合,权限是动作的集合。一个角色拥有哪些动作,会随着业务变化而变化。
更稳的方式是后端返回权限码,前端只判断权限码。例如:
  1. {
  2. "permissions": [
  3. "article:create",
  4. "article:update",
  5. "article:delete",
  6. "article:export"
  7. ]
  8. }
json
组件里不再问“用户是不是管理员”,而是问“用户有没有 article:delete”。这样角色调整时,前端不需要知道 admin 还是 operator,只关心动作是否被允许。
当然,权限码也不能乱起。建议用“资源:动作”的形式,必要时再加范围,比如 article:delete:any article:delete:own。不要今天写 deleteArticle,明天写 article_remove,后天写 ARTICLE_DELETE。命名不统一,后续维护会很痛。
权限码适合表达静态动作,但不一定能覆盖所有数据级规则。比如“只能删除自己创建的草稿”,前端可以先判断有没有 article:delete:own,再结合 row.ownerId row.status 做二次判断。这种二次判断最好封装成函数,不要直接散在模板里。
用函数收口业务判断
Vue 项目里,我更建议先用普通函数收口权限判断,而不是一上来就写复杂插件。比如:
  1. export function canDeleteArticle(user, article) {
  2. if (!user || !article) return false;

  3. if (user.permissions.includes('article:delete:any')) {
  4. return article.status === 'draft' || article.status === 'rejected';
  5. }

  6. if (user.permissions.includes('article:delete:own')) {
  7. return article.ownerId === user.id && article.status === 'draft';
  8. }

  9. return false;
  10. }
js
组件里就变成:
  1. <button v-if="canDeleteArticle(user, row)" @click="remove(row)">删除button>
vue
这看起来只是把判断挪了个地方,但意义不小。第一,规则有了名字。第二,列表页和详情页可以复用同一判断。第三,测试可以直接覆盖这个函数,而不是通过页面点击间接验证。
如果规则继续变复杂,可以再把权限函数按业务域拆分,比如 article、order、user。不要把所有权限都塞进一个巨大文件。巨大文件一开始方便查找,后面会变成谁都不愿意改。
收口不是为了写更抽象的代码,而是为了让同一条规则只有一个解释来源。组件负责展示,权限函数负责判断,后端负责最终安全校验。
指令适合处理简单按钮,不适合吃掉所有规则
Vue 里常见做法是写一个 v-permission 指令:
  1. <button v-permission="'article:export'">导出button>
vue
这个方案适合静态权限按钮,比如“导出”“新建”“批量操作”。它让模板很清爽,也方便统一处理无权限时隐藏或禁用。
但我不建议让指令承载复杂业务规则。比如删除按钮要看权限码、数据状态、归属关系、审核阶段,如果硬塞进指令,最后会变成:
  1. <button v-permission="{ code: 'article:delete:own', ownerId: row.ownerId, status: row.status }">删除button>
vue
这时指令已经开始理解业务对象,反而不如显式调用 canDeleteArticle(user, row) 清楚。
我的取舍是:静态动作交给指令,数据相关动作交给函数。指令处理“有没有这个动作权限”,函数处理“在这条数据上能不能执行”。这条边界简单,但能避免很多后期复杂度。
无权限时隐藏,还是禁用
按钮权限还有一个产品层面的选择:无权限时,按钮是隐藏,还是展示但禁用?这不是纯前端问题。
隐藏适合普通用户不需要知道的能力。比如普通员工没有系统配置入口,隐藏可以减少干扰。禁用适合用户知道这个能力存在,但当前条件不满足。比如文章已发布后不能删除,展示禁用按钮并提示原因,比直接隐藏更容易理解。
还有一种情况是权限申请流程。如果用户能申请某个权限,禁用按钮旁边可以给出“申请权限”入口。这样权限不只是拦截,也成为流程的一部分。
所以不要在技术层统一规定“无权限一律隐藏”。更好的做法是让权限判断返回原因:没有权限、状态不允许、数据不归属、审核中不可操作。组件根据原因决定隐藏、禁用还是提示。
后端校验不能省
前端按钮权限再完整,也不能替代后端校验。用户可以改前端代码,可以直接请求接口,也可以拿到旧页面里的入口。真正决定动作能不能执行的地方,必须是后端。
前端和后端最好共享同一套权限语义。前端拿到 article:delete:own,后端也按同样的权限码理解,而不是前端一套角色名、后端一套枚举。否则排查问题时会出现“前端认为能删,后端认为不能删”的争议。
接口返回也要友好。后端拒绝操作时,不要只返回 403。最好能返回可展示的错误码或原因,比如 ARTICLE_STATUS_NOT_DELETABLEPERMISSION_DENIED。前端才能给用户一个明确反馈,而不是统一弹“操作失败”。
一条可落地的改造路线
如果一个 Vue 项目权限已经写散了,我不建议立刻全量重构。可以按风险和重复度逐步收口。
先盘点出现两次以上的权限判断,尤其是删除、导出、审核、上下架这类高风险动作。
把这些判断抽成业务函数,优先保证列表页和详情页规则一致。
为静态按钮补一个简单的权限指令,减少模板里的重复权限码判断。
和后端确认权限码命名,避免继续使用角色名写新逻辑。
为无权限原因设计展示规则:隐藏、禁用、提示或引导申请。
最后再考虑菜单、路由、按钮的统一配置,不要一开始就追求大而全。
Vue 权限按钮别写散,本质上不是组件写法问题,而是权限语义要有固定位置。组件可以变化,UI 可以变化,角色也可以变化,但“谁在什么条件下能执行什么动作”这件事,不能散落在模板里靠记忆维护。