记录---写个vite插件自动处理系统权限,降低99%重复工作

时间:2025-09-07 17:30:02来源:互联网

下面小编就为大家分享一篇记录---写个vite插件自动处理系统权限,降低99%重复工作,具有很好的参考价值,希望对大家有所帮助。

?‍? 写在开头

点赞 + 收藏 === 学会???

前言

好久没有更文章咯,最近做一个中台系统的权限控制功能,由于路由权限和角色权限都简单,但是要做按钮权限有点麻烦,因为太多按钮了。其实我以前也做过这个功能,简单暴力做法就是每个按钮用自定义指令去判断是否有权限显示。但是重复代码也太多太多,并且维护性极差,代码固定难以调整。

所以这次终于忍不住了,决定抽时间做一个vite插件去自动生成对比按钮权限的代码,下面细说实现过程。


基本思路

项目构建的时候vite自动帮我全局插入按钮权限的代码,并且跟接口获取存放在pinia仓库的权限列表对比是否有权限展示。

基本思路简单又明确,但需要考虑的细节还是很多的,下面一一列举分析。

1、如何识别生成独一无二的按钮编码

插入的编码选择按规则自动化语义化生成的,规则如下所示。

权限编码 = 路径+后缀,这样每个按钮都能独一无二

例如路径是scr/view/index.vue的新增按钮,那么编码就是scr/view/index_create

下方表格随便列个常见的后缀规则,这些都是可以自己定义约束的

 简单示例

// 相对src路径const filePath = relative(process.cwd(), id).replace(extname(id), '')const result = code.split('n')// 映射表const butTextMap: Record<string, string> = {  '新增': 'create',  '编辑': 'edit',  '删除': 'delete',  '查看': 'view',  '导出': 'export',}//拼接得到编码const permCode = `${filePath}_${suffix}`

2、考虑对比多种UI库的按钮

系统可能使用了原生的button,也可能是el-button或者其它更多UI库的按钮,这些需要在识别中做针对处理即可,或者只识别button部分,因为各种库只是添加了前缀,其实都有button组成。

3、无法规则生成编码的特殊按钮处理

例如除了下列常见规则外,可能还有一些不规则按钮,例如"跳转系统"这种高度个性化按钮

小编选择的解决方案是直接在按钮上输入编码特殊处理,在自动插入时判断是否已经有编码,有就跳过不需要去插入。

4、vite插入时机的选择

众所周知vite有很多生命周期钩子,那么我们这个需求应该选择在那个钩子执行呢?

例如 resolveId、load、transform、handleHotUpdate、generateBundle 等都可以用于介入构建流程,那么那个才适合呢? 但在实现当前需求时,我选择使用 transform 钩子。

因为这个需求要插入内容需要解析组件的模板结构,而 transform 钩子能帮我们拿到完整的源码,并且在生产环境开发环境都能生效。

5、具体插入方案选择

vite里面我们可以把一切文件都看作字符串,因些插入操作可以用正则去插入,但是.....不建议

这里推荐使用walk去处理AST插入内容,我们知道vue模板编译的时候就是要转ast抽象语法树的,ast处理安全性更强、稳定性更高,而且能识别节点类型。

例如使用正则的话可以出现如下示例问题

<!-- <button> --> 

这是注释了的代码,但正则只会识别字符串这里就会出现问题,使用ast则不会,这只是举例其中一个小问题还有很多可能引发的问题。

6、参数传递方案

我们插入权限对比编码后,正常情况是需要从vuex或者pinia里获取数据对比权限,这里我选择直接把获取vuex或者pinia的代码一起在ast中插入到页面尽最大可能减少手动写代码。

注意:防止出现重复引入情况,插入代码时就当做判断是否存在,存在则跳过插入。

代码实现

上面把应该注意的问题都分析并给出了解决方案,下面看看最终版本的可用代码。

import type { Plugin } from 'vite';import { relative, extname } from 'path';import { parse, walk } from 'vue-eslint-parser';import { generate } from 'escodegen';export default function autoPermissionPlugin({ srcDir = 'src' }: { srcDir?: string } = {}): Plugin {  const filter = (id: string) => /.vue$/.test(id);  return {    name: 'tty-auto-permission',    transform(code, id) {      if (!filter(id)) return;      try {        const ast = parse(code, {          ecmaVersion: 2020,          sourceType: 'module',          loc: true,        });        // 获取相对于 src 的路径        const filePath = relative(process.cwd(), id).replace(extname(id), '');        // 按钮文案映射表        const butTextMap: Record<string, string> = {          新增: 'create',          编辑: 'edit',          删除: 'delete',          查看: 'view',          导出: 'export',        };        // 查找模板中的按钮并注入权限指令        const templateAST = ast.templateBody;        if (templateAST) {          walk(templateAST, {            enter(node) {              if (node.type === 'VElement' && ['button', 'a-button', 'el-button'].includes(node.name)) {                let suffix: string | undefined = undefined;                // 从按钮文字推断后缀                const buttonText = node.children?.find((c) => c.type === 'VText')?.value.trim();                if (buttonText && butTextMap[buttonText]) {                  suffix = butTextMap[buttonText];                }                // 从 @click 方法名推断                const clickHandler = node.attributes.find((attr) => attr.key.name === '@click');                if (clickHandler?.value?.expression?.callee?.name) {                  const fnName = clickHandler.value.expression.callee.name;                  if (fnName.startsWith('handle')) {                    suffix = fnName.charAt(6).toLowerCase() + fnName.slice(7);                  }                }                //如果有后缀                if (suffix) {                  const permCode = `${filePath}_${suffix}`;                  // 是否已有权限判断指令                  const hasPermissionDirective = node.startTag.attributes.some(                    (attr) =>                      attr.type === 'VDirective' &&                      attr.key.name.name === 'if' &&                      attr.value?.value?.includes('hasPerm'),                  );                  if (hasPermissionDirective) {                    // 已有权限指令,跳过插入判断                    return;                  }                  // AST 注入权限指令                  node.startTag.attributes.push({                    type: 'VDirective',                    key: {                      name: { name: 'if' },                      argument: null,                      modifiers: [],                    },                    value: {                      type: 'VLiteral',                      value: `permissionStore.hasPerm('${permCode}')`,                    },                  });                }              }            },          });        }        // 是否已导入存储仓储        const hasImportStore = code.includes(          "import { butPermissionStore } from '@/stores/butPermission'",        );        const warehouseCode = `            <script setup>            import { butPermissionStore } from '@/stores/butPermission'            const permissionStore = butPermissionStore()            </script>            `.trim();        // 如没有 <script> 标签,插入新的 <script setup>        if (!code.includes('<script')) {          ast.body.unshift(parse(warehouseCode).body[0]);        } else {          // 否则查找第一个 <script> 或 <script setup> 并在其后插入 store           walk(ast, {            enter(node) {              if (                node.type === 'VElement' &&                node.name === 'script' &&                node.startTag.attributes.some((attr) => attr.key.name === 'setup')              ) {                if (!hasImportStore) {                  // 在 <script setup> 中注入 import 语句                  const importNode = parse(warehouseCode).body[0];                  ast.body.splice(ast.body.indexOf(node) + 1, 0, importNode);                }                this.skip(); // 跳过后续遍历              }            },          });          // 如果没找到 <script setup>,则在第一个 <script> 后插入          if (            !ast.body.some(              (n) =>                n.type === 'VElement' &&                n.name === 'script' &&                n.startTag.attributes.some((a) => a.key.name === 'setup'),            )          ) {            for (let i = 0; i < ast.body.length; i++) {              const node = ast.body[i];              if (node.type === 'VElement' && node.name === 'script') {                if (!hasImportStore) {                  const importNode = parse(warehouseCode).body[0];                  ast.body.splice(i + 1, 0, importNode);                }                break;              }            }          }        }                const newCode = generate(ast);        return {          code: newCode,          map: null,        };      } catch (e) {        console.error(`权限注入失败: ${id}`, e);        return { code, map: null };      }    },  };}
 
项目中使用插件
 
import { defineConfig } from 'vite'import vue from '@vitejs/plugin-vue'import autoPermissionPlugin from './plugins/autoPermissionPlugin'export default defineConfig({  plugins: [    vue(),    autoPermissionPlugin(),  ],})

小结

好啦,结合项目需求就实现了可用的vite权限插件,但由于针对性项目使用就没有发布到npm,毕竟发布通用插件还要考虑很多适配因素,实在没有时间搞就算了。

这插件的实现并不难,就是要考虑的细节比较多,要不然容易出问题。这文章就先写到这了,如果发现哪里写的不对或者有更好的建议可以评论互相学习呢。


本文转载于:https://*jueji**n.cn/post/7523712612535517210

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

本站部分内容转载自互联网,如果有网站内容侵犯了您的权益,可直接联系我们删除,感谢支持!