当前位置:首页 > 技术文章 > 正文内容

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

zonemu1个月前 (07-26)技术文章15

最近做一个中台系统的权限控制功能,由于路由权限和角色权限都简单,但是要做按钮权限有点麻烦,因为太多按钮了。所以我用 vite 写一个插件自动化实现。

前言

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

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

基本思路

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

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

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

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

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

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

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

按钮名称
权限后缀
新增
create
编辑
edit
删除
delete
查看
view
导出
export

简单示例

//相对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、无法规则生成编码的特殊按钮处理

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

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

按钮名称
权限后缀
新增
create
编辑
edit
删除
delete
查看
view
导出
export

4、vite 插入时机的选择

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

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

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

5、具体插入方案选择

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

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

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

 --> 

这是注释了的代码,但正则只会识别字符串这里就会出现问题,使用 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, });
//获取相对于scr的路径 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; }

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 = ` import { butPermissionStore } from '@/stores/butPermission' const permissionStore = butPermissionStore() `.trim();

if (!code.includes(')) { 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) {
const importNode = 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 (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 , 毕竟发布通用插件还要考虑很多适配因素,实在没有时间搞就算了。

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

AI编程资讯AI Coding专区指南:
https://aicoding.juejin.cn/aicoding

" " ~

相关文章

宽带客户收费管理系统--维修版(宽带售后服务)

宽带客户收费管理系统--维修版headerfooter《宽带客户收费管理系统——维修版》是一款适合宽带运营商使用的管理系统。软件主要包括以下功能:1.主要功能包括用户开户、收费录入、工单登记、故障处理...

2023 年 10 个最佳 Linux 桌面发行版

Linux 操作系统在桌面领域的发展已经不再被忽视,越来越多的用户正在考虑切换到 Linux 上。在 2023 年,我们可以期待更多的 Linux 桌面发行版的推出和发展。这里列举了 10 个最佳的...

git的几种分支模式(git分支的概念)

编写代码,是软件开发交付过程的起点,发布上线,是开发工作完成的终点。代码分支模式贯穿了开发、集成和发布的整个过程,是工程师们最亲切的小伙伴。那如何根据自身的业务特点和团队规模来选择适合的分支模式呢?本...

Gitlab 的使用和代码审查流程介绍

1、先简洁介绍下项目常用的信息-面板统计页面2、用户信息面板3、服务器信息4、项目信息5、重点介绍代码提交审核机制和授权合并机制开发人员推送代码的时候不能直接推送到master,否则就会报错。此时开发...

(一)熟练HTML5+CSS3,每天复习一遍

前言学习网页的概念和分类,了解静态网页和动态网页的不同;了解网页浏览器的工作原理。了解HTML,XHTML,HTML5的概念,制作简单的HTML页面的开发。什么是网页可以在internet上通过网页浏...

HTML5学习笔记三:HTML5语法规则(html5语法详解)

1.标签要小写2.属性值可加可不加””或”3.可以省略某些标签 html body head tbody4.可以省略某些结束标签 tr td li例:显示效果:5.单标签不用加结束标签img inpu...