写个vite插件自动处理系统权限,降低99%重复工作
?最近做一个中台系统的权限控制功能,由于路由权限和角色权限都简单,但是要做按钮权限有点麻烦,因为太多按钮了。所以我用 vite 写一个插件自动化实现。
?前言好久没有更文章咯,最近做一个中台系统的权限控制功能,由于路由权限和角色权限都简单,但是要做按钮权限有点麻烦,因为太多按钮了。其实我以前也做过这个功能,简单暴力做法就是每个按钮用自定义指令去判断是否有权限显示。但是重复代码也太多太多,并且维护性极差,代码固定难以调整。
所以这次终于忍不住了,决定抽时间做一个vite插件去自动生成对比按钮权限的代码,下面细说实现过程。
基本思路项目构建的时候vite自动帮我全局插入按钮权限的代码,并且跟接口获取存放在pinia仓库的权限列表对比是否有权限展示。
基本思路简单又明确,但需要考虑的细节还是很多的,下面一一列举分析。
1、如何识别生成独一无二的按钮编码插入的编码选择按规则自动化语义化生成的,规则如下所示。
权限编码 = 路径+后缀,这样每个按钮都能独一无二
例如路径是scr/view/index.vue的新增按钮,那么编码就是scr/view/index_create
下方表格随便列个常见的后缀规则,这些都是可以自己定义约束的
按钮名称权限后缀新增create编辑edit删除delete查看view导出export简单示例
//相对src路径constfilePath =relative(process.cwd(), id).replace(extname(id),'')constresult = code.split('\n')
//映射表constbutTextMap:Recordstring,string = {'新增':'create','编辑':'edit','删除':'delete','查看':'view','导出':'export',}
//拼接得到编码constpermCode =`${filePath}_${suffix}`2、考虑对比多种 UI 库的按钮系统可能使用了原生的button,也可能是el-button或者其它更多UI库的按钮,这些需要在识别中做针对处理即可,或者只识别button部分,因为各种库只是添加了前缀,其实都有button组成。
3、无法规则生成编码的特殊按钮处理例如除了下列常见规则外,可能还有一些不规则按钮,例如 "跳转系统" 这种高度个性化按钮
小编选择的解决方案是直接在按钮上输入编码特殊处理,在自动插入时判断是否已经有编码,有就跳过不需要去插入。
按钮名称权限后缀新增create编辑edit删除delete查看view导出export4、vite 插入时机的选择众所周知vite有很多生命周期钩子,那么我们这个需求应该选择在那个钩子执行呢?
例如resolveId、load、transform、handleHotUpdate、generateBundle等都可以用于介入构建流程,那么那个才适合呢? 但在实现当前需求时,我选择使用transform钩子。
因为这个需求要插入内容需要解析组件的模板结构,而transform钩子能帮我们拿到完整的源码,并且在生产环境开发环境都能生效。
5、具体插入方案选择在vite里面我们可以把一切文件都看作字符串,因些插入操作可以用正则去插入,但是..... 不建议
这里推荐使用walk去处理AST插入内容,我们知道vue模板编译的时候就是要转ast抽象语法树的,ast处理安全性更强、稳定性更高,而且能识别节点类型。
例如使用正则的话可以出现如下示例问题
!-- button --这是注释了的代码,但正则只会识别字符串这里就会出现问题,使用ast则不会,这只是举例其中一个小问题还有很多可能引发的问题。
6、参数传递方案我们插入权限对比编码后,正常情况是需要从vuex或者pinia里获取数据对比权限,这里我选择直接把获取vuex或者pinia的代码一起在ast中插入到页面尽最大可能减少手动写代码。
?注意:防止出现重复引入情况,插入代码时就当做判断是否存在,存在则跳过插入。
?代码实现上面把应该注意的问题都分析并给出了解决方案,下面看看最终版本的可用代码。
importtype{Plugin}from'vite';import{ relative, extname }from'path';import{ parse, walk }from'vue-eslint-parser';import{ generate }from'escodegen';
exportdefaultfunctionautoPermissionPlugin({ srcDir ='src'}: { srcDir?:string} = {}):Plugin{constfilter= (id:string) =/\.vue$/.test(id);
return{ name:'tty-auto-permission', transform(code, id) { if(!filter(id))return;
try{ constast =parse(code, { ecmaVersion:2020, sourceType:'module', loc:true,
//获取相对于scr的路径 constfilePath =relative(process.cwd(), id).replace(extname(id),'');
//按钮文案映射表 constbutTextMap:Recordstring,string = { 新增:'create', 编辑:'edit', 删除:'delete', 查看:'view', 导出:'export',
//查找模板中的按钮并注入权限指令 consttemplateAST = ast.templateBody; if(templateAST) { walk(templateAST, { enter(node) { if(node.type==='VElement'&& ['button','a-button','el-button'].includes(node.name)) { letsuffix:string|undefined=undefined;
//从按钮文字推断后缀 constbuttonText = node.children?.find((c) =c.type==='VText')?.value.trim(); if(buttonText && butTextMap[buttonText]) { suffix = butTextMap[buttonText]; }
//从@click 方法名推断 constclickHandler = node.attributes.find((attr) =attr.key.name==='@click'); if(clickHandler?.value?.expression?.callee?.name) { constfnName = clickHandler.value.expression.callee.name; if(fnName.startsWith('handle')) { suffix = fnName.charAt(6).toLowerCase() + fnName.slice(7); } }
if(suffix) { constpermCode =`${filePath}_${suffix}`;
consthasPermissionDirective = node.startTag.attributes.some( (attr) = attr.type==='VDirective'&& attr.key.name.name==='if'&& attr.value?.value?.includes('hasPerm'),
if(hasPermissionDirective) {
return; }
node.startTag.attributes.push({ type:'VDirective', key: { name: {name:'if'}, argument:null, modifiers: [], }, value: { type:'VLiteral', value:`permissionStore.hasPerm('${permCode}')`, }, } } }, }
consthasImportStore = code.includes( "import { butPermissionStore } from '@/stores/butPermission'", constwarehouseCode =` script setup import { butPermissionStore } from '@/stores/butPermission' const permissionStore = butPermissionStore() /script `.trim();
if(!code.includes('script')) { ast.body.unshift(parse(warehouseCode).body[0]); }else{
walk(ast, { enter(node) { if( node.type==='VElement'&& node.name==='script'&& node.startTag.attributes.some((attr) =attr.key.name==='setup') ) { if(!hasImportStore) {
constimportNode =parse(warehouseCode).body[0]; ast.body.splice(ast.body.indexOf(node) +1,0, importNode); } this.skip(); } },
if( !ast.body.some( (n) = n.type==='VElement'&& n.name==='script'&& n.startTag.attributes.some((a) =a.key.name==='setup'), ) ) { for(leti =0; i ast.body.length; i++) { constnode = ast.body[i]; if(node.type==='VElement'&& node.name==='script') { if(!hasImportStore) { constimportNode =parse(warehouseCode).body[0]; ast.body.splice(i +1,0, importNode); } break; } } } }
constnewCode =generate(ast); return{ code: newCode, map:null, }catch(e) { console.error(`权限注入失败:${id}`, e); return{ code,map:null } },}
项目中使用插件
import{ defineConfig }from'vite'importvuefrom'@vitejs/plugin-vue'importautoPermissionPluginfrom'./plugins/autoPermissionPlugin'
exportdefaultdefineConfig({plugins: [ vue(), autoPermissionPlugin(), ],})小结好啦,结合项目需求就实现了可用的vite权限插件,但由于针对性项目使用就没有发布到npm, 毕竟发布通用插件还要考虑很多适配因素,实在没有时间搞就算了。
这插件的实现并不难,就是要考虑的细节比较多,要不然容易出问题。这文章就先写到这了,如果发现哪里写的不对或者有更好的建议可以评论互相学习呢。
AI编程资讯AI Coding专区指南:https://aicoding.juejin.cn/aicoding
点击"阅读原文"了解详情~
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线