Vite插件开发

需求背景

前端后台类项目,列表页面常带搜索框类表单,用于筛选列表数据。用户操作习惯性输入或选择表单项后点击”×”按钮清空输入或者是选择项。因此针对常见的 input 表单 select 表单及 dateSelect 等表单,需要前端手动添加 clearable 属性。

问题聚焦

由于手动设置 clearable 属性太过依赖人为因素,假设有部分表单漏加属性,会导致无法清空输入或选择条件的用户体验问题,另外会增加测试同事的测试压力,因此,如何降低前端开发的编码关注度,杜绝 clearable 属性的漏加,减少测试工作量问题。是本文讨论的要点及问题突破的主要方向。

解题思路

针对以上背景及问题的产生,经一段时间的调研,发现目前关于此类需求前端社区并无成熟解决方案。更多的还是依赖代码规约人为控制问题产生。在寻找解决方案的过程之中,考虑了多个方案,其中个人感觉可以尝试的有以下几种:

  • 将所有的搜索表单封装成搜索组件
  • 全局覆盖 element-plus 中 form 表单的属性
  • 通过 vite 插件的形式修改项目运行时代码

方案对比

方案 问题 实现难度(5 星) 推荐指数(5 星)
封装组件 需要针对不同场景,封装大量组件 ⭐️⭐️⭐️ ⭐️
覆盖属性 会覆盖非搜索表单的属性,破坏性强 ⭐️ ⭐️
vite 插件 无破坏性,无需关注编码 ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️

方案实现

因为方案 1 方案 2 的技术实现基本无难度,且方案本身缺点较多或者是带来的附加工作量较多,因此本文仅针对 vite 插件解决方案进行探究、实现、验证。

AST 语法树
  1. 官方定义:抽象语法树 (Abstract Syntax Tree,AST),是源代码语法结构的一种抽象表示。以树状的形式表现编程语言的语法结构,每个节点都表示源代码中的一种结构。
AST语法树
  1. AST 用途:代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等

    • 如 JSLint、JSHint 对代码错误或风格的检查,发现一些潜在的错误
    • IDE 的错误提示、格式化、高亮、自动补全等等
    • 代码混淆压缩
    • UglifyJS2 等
    • 代码打包工具 webpack、rollup 等等
    • CommonJS、AMD、CMD、UMD 等代码规范之间的转化
    • CoffeeScript、TypeScript、JSX 等转化为原生 Javascript

Vite 插件开发

  • vite 插件钩子函数

    钩子函数可以理解为 vite 在运行代码到某一个阶段暴露出来可以提供给第三方开发者修改构建结果的入口。可以类比 git 提交中的钩子,或者是类比 vue 中的生命周期函数。

    1. 通用钩子
      • options
      • buildStart
      • resolveId
      • load
      • transform
      • buildEnd
      • closeBundle
    2. vite 特有钩子
      • config
      • configResolved
      • configureServer
      • configurePreviewServer
      • transformIndexHtml
      • handleHotUpdate
  • 插件调用的顺序

    使用enforce属性可以自定义插件调用顺序。enforce可取 **[‘pre’,’post’]**插件调用顺序如下:

    1. Alias(路径别名)
    2. 带有 enforce:pre 的用户插件
    3. Vite 的核心插件
    4. 没有enforce属性的用户插件
    5. Vite 的构建插件
    6. 带有enforce:post的用户插件
    7. Vite 后置构建插件(最小化,manifest,报告等)
  • vite 构建流程

vite构建流程
  • vite 编译 vue 流程(对应构建请求响应及热更新流程)
vite编译流程
  • 插件实现原理
插件实现原理
  1. 获取 vue 文件源码
  2. 利用@vue/compiler-sfc loader 将 vue 代码转为 AST 语法树
  3. 使用递归算法遍历 AST 语法树匹配到对应的 DOM 节点并修改节点内容(重点)
  4. 使用@padcom/vue-ast-serializer 将 AST 转为 vue 源码(重点)
  5. 交付 vue 核心编译器及后续插件处理

伪代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import { parse } from "vue/compiler-sfc";
import { stringify } from "@padcom/vue-ast-serializer";

//AST修改辅助函数
const travers = (node) => {
if (!node.props.some((prop) => prop.name === "clearable")) {
node.props.push({
type: 6 /* ATTRIBUTE */,
name: ":clearable",
value: {
type: 2 /* TEXT */,
content: "true",
loc: node.loc,
},
loc: node.loc,
});
}
};
// 插件导出
export const gogo = () => {
return {
name: "vite-plugin-go",
enforce: "pre",
transform(src, id) {
if (!id.endsWith("App.vue")) return;
const { descriptor } = parse(src);
if (descriptor.template.type !== "template") return;
//AST树递归遍历(TODO:算法优化)
function walkNode(nodeList = []) {
if (!Array.isArray(nodeList)) return;
nodeList.forEach((node) => {
const { props = [] } = node;
if (!props.length) return;
const target = props.find((item) => item.name == "class") || {};
const targetClass = target?.value?.content ?? "";
if (node.type === 1 && node.tag.includes("el-")) {
travers(node);
}
if (
node.children &&
node.children.length &&
node.tagType == 0 &&
targetClass == "action-box"
) {
walkNode(node.children);
}
});
}
walkNode(descriptor.template.ast.children);
const code = stringify({ descriptor });

return {
code,
map: null, // 如果需要source map,这里需要正确处理
};
},
};
};

补充

辅助插件: vite-plugin-inspect 用于检测 vite 构建过程中所有插件对代码的处理结果

1
npm i -D vite-plugin-inspect
1
2
3
4
5
6
//vite.config.js
import Inspect from "vite-plugin-inspect";

export default {
plugins: [Inspect()],
};
项目参考