记录---写个vite插件自动处理系统权限,降低99%重复工作
<h1 data-id="heading-0">🧑💻 写在开头</h1><p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<div>
<div>
<h2 data-id="heading-0">前言</h2>
<blockquote>
<p>好久没有更文章咯,最近做一个中台系统的权限控制功能,由于路由权限和角色权限都简单,但是要做按钮权限有点麻烦,因为太多按钮了。其实我以前也做过这个功能,简单暴力做法就是每个按钮用自定义指令去判断是否有权限显示。但是重复代码也太多太多,并且维护性极差,代码固定难以调整。</p>
<p>所以这次终于忍不住了,决定抽时间做一个<code>vite</code>插件去自动生成对比按钮权限的代码,下面细说实现过程。</p>
</blockquote>
</div>
<br>
</div>
<div>
<div>
<h2 data-id="heading-1">基本思路</h2>
<blockquote>
<p>项目构建的时候<code>vite</code>自动帮我全局插入按钮权限的代码,并且跟接口获取存放在<code>pinia</code>仓库的权限列表对比是否有权限展示。</p>
<p>基本思路简单又明确,但需要考虑的细节还是很多的,下面一一列举分析。</p>
</blockquote>
<h4 data-id="heading-2">1、如何识别生成独一无二的按钮编码</h4>
<p>插入的编码选择按规则自动化语义化生成的,规则如下所示。</p>
<p><code>权限编码 = 路径+后缀</code>,这样每个按钮都能独一无二</p>
<p>例如路径是<code>scr/view/index.vue</code>的新增按钮,那么编码就是<code>scr/view/index_create</code></p>
<p>下方表格随便列个常见的后缀规则,这些都是可以自己定义约束的</p>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202507/2149129-20250715165717002-1462273471.png" alt="" loading="lazy"></p>
<p> 简单示例</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 相对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}`</pre>
</div>
<div>
<div>
<h4 data-id="heading-3">2、考虑对比多种UI库的按钮</h4>
<p>系统可能使用了原生的<code>button</code>,也可能是<code>el-button</code>或者其它更多<code>UI</code>库的按钮,这些需要在识别中做针对处理即可,或者只识别<code>button</code>部分,因为各种库只是添加了前缀,其实都有<code>button</code>组成。</p>
<h4 data-id="heading-4">3、无法规则生成编码的特殊按钮处理</h4>
<p>例如除了下列常见规则外,可能还有一些不规则按钮,例如"跳转系统"这种高度个性化按钮</p>
<p>小编选择的解决方案是直接在按钮上输入编码特殊处理,在自动插入时判断是否已经有编码,有就跳过不需要去插入。</p>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202507/2149129-20250715165755068-1074059696.png" alt="" loading="lazy"></p>
<div>
<div>
<h4 data-id="heading-5">4、vite插入时机的选择</h4>
<p>众所周知<code>vite</code>有很多生命周期钩子,那么我们这个需求应该选择在那个钩子执行呢?</p>
<p>例如 <code>resolveId、load、transform、handleHotUpdate、generateBundle</code> 等都可以用于介入构建流程,那么那个才适合呢? 但在实现当前需求时,我选择使用 <code>transform</code> 钩子。</p>
<p>因为这个需求要插入内容需要解析组件的模板结构,而 <code>transform</code> 钩子能帮我们拿到完整的源码,并且在生产环境开发环境都能生效。</p>
<h4 data-id="heading-6">5、具体插入方案选择</h4>
<p>在<code>vite</code>里面我们可以把一切文件都看作字符串,因些插入操作可以用正则去插入,但是.....不建议</p>
<p>这里推荐使用<code>walk</code>去处理<code>AST</code>插入内容,我们知道<code>vue</code>模板编译的时候就是要转<code>ast</code>抽象语法树的,<code>ast</code>处理安全性更强、稳定性更高,而且能识别节点类型。</p>
<p>例如使用正则的话可以出现如下示例问题</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;"><!-- <button> --> </pre>
</div>
<div>
<div>
<p>这是注释了的代码,但正则只会识别字符串这里就会出现问题,使用<code>ast</code>则不会,这只是举例其中一个小问题还有很多可能引发的问题。</p>
<h4 data-id="heading-7">6、参数传递方案</h4>
<p>我们插入权限对比编码后,正常情况是需要从<code>vuex</code>或者<code>pinia</code>里获取数据对比权限,这里我选择直接把获取<code>vuex</code>或者<code>pinia</code>的代码一起在<code>ast</code>中插入到页面尽最大可能减少手动写代码。</p>
<blockquote>
<p>注意:防止出现重复引入情况,插入代码时就当做判断是否存在,存在则跳过插入。</p>
</blockquote>
<h2 data-id="heading-8">代码实现</h2>
<p>上面把应该注意的问题都分析并给出了解决方案,下面看看最终版本的可用代码。</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">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) {
suffix = butTextMap;
}
// 从 @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);
} 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;
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;
if (node.type === 'VElement' && node.name === 'script') {
if (!hasImportStore) {
const importNode = parse(warehouseCode).body;
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 };
}
},
};
}</pre>
</div>
</div>
<div> </div>
<div>项目中使用插件<br>
</div>
<div> </div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import autoPermissionPlugin from './plugins/autoPermissionPlugin'
export default defineConfig({
plugins: [
vue(),
autoPermissionPlugin(),
],
})</pre>
</div>
<div>
<h2 data-id="heading-9">小结</h2>
<blockquote>
<p>好啦,结合项目需求就实现了可用的<code>vite</code>权限插件,但由于针对性项目使用就没有发布到<code>npm</code>,毕竟发布通用插件还要考虑很多适配因素,实在没有时间搞就算了。</p>
<p>这插件的实现并不难,就是要考虑的细节比较多,要不然容易出问题。这文章就先写到这了,如果发现哪里写的不对或者有更好的建议可以评论互相学习呢。</p>
</blockquote>
</div>
</div>
<br>
</div>
<div>
<h2>本文转载于:https://juejin.cn/post/7523712612535517210</h2>
</div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></p><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/18985926
頁:
[1]