30分钟开发Vue 3插入客制化脚本插件
1. 引言
嘿,大家好,我是星辰编程理财,今天要和大家聊聊一个让我颇有心得的话题:Vue组件的客制化方式之前-“埋点”。或许你也曾为埋点这个问题烦恼过,在历史的包袱下加上技术债,我们需要实现一套客制化的脚本,埋点成了我们选择的方向。
为什么要进行 Vue 组件的埋点?
曾经,我们在没有引入Vue框架是,代码里面到处都是埋点,随着业务的增长和技术迭代,需要向Vue框架下过度。于是,在这个痛点的驱使下,开始对迁移和兼容代码。通过我们统计分析发现,会有一部分公共埋点的位置。如果每个文件都去加上通用埋点代码,工作量大并和业务代码耦合。于是采用编译时插入代码的方式实现。
为何使用AST?
最初,我们尝试了简单的字符串匹配,想着解决问题应该不难。但是,特征字符串太少,简单匹配往往事倍功半,不好扩展,而且容易翻车。于是AST算是一个稳妥,虽然不太熟悉开发时间要长一些。
2. AST 技术调研
首先,我开始进行了 AST 技术的调研,由于技术上我们使用Vite作为开发及打包工具。于是直接问GTP:
Q: 开发vite插件,如何在插件中用ast分析代码?请简要回答。
A:
在Vite插件中使用AST(抽象语法树)来分析代码可以通过以下步骤完成:
- 安装依赖:首先,在你的插件项目中安装 @babel/parser 或其他适用的AST解析器。
npm install @babel/parser --save-dev
- 引入解析器:在你的插件代码中引入AST解析器。
const parser = require('@babel/parser');
- 解析代码:使用解析器来解析你感兴趣的代码文件。
const code = fs.readFileSync('path/to/your/file.js', 'utf-8');
const ast = parser.parse(code, {
sourceType: 'module',
plugins: ['jsx'] // 如果你的代码中包含JSX
});
- 遍历AST:通过遍历AST节点,你可以获取代码中的各种信息。
traverse(ast, {
// 在这里添加处理不同类型节点的逻辑
});
你可能需要使用 @babel/traverse 这个插件来实现遍历。
通过上面回答,已了解需要@babel/parser与@babel/traverse实现。我们再向文心一言侧面印证一下常规方案是不是采用@babel/parser与@babel/traverse即可。
3. 具体需求描述
既然已经确认用@babel/parser、@babel/traverse作为AST解析和处理库。接下来就是整理具体的需求描述,需求描述一定要整理好,因为我并不打算直接开发。而是直接基于需求问GTP。
Q:在开发Vite插件时,使用@babel/traverse遍历AST如何找到选项式API的mounted方法?如何找到组合式API的setup方法中直接包含的最后一个return语句,需要忽略setup方法内部的子方法的return语句。
A:
在开发Vite插件时,使用@babel/traverse遍历AST可以找到选项式API的mounted
方法和组合式API的setup
方法中直接包含的最后一个return
语句。下面是具体的步骤:
-
找到选项式API的mounted方法:
在AST遍历过程中,可以通过匹配对象方法节点中键名为mounted
的节点来找到选项式API的mounted
方法。具体步骤如下:traverse(ast, { ObjectMethod(path) { if ( path.node.key.type === 'Identifier' && path.node.key.name === 'mounted' ) { // 处理选项式API的mounted方法逻辑 // path.node 包含了该方法的完整信息 } } });
-
找到组合式API的setup方法中的最后一个return语句:
对于组合式API的setup
方法,需要在遍历过程中找到最后一个return
语句,但要忽略setup方法内部的子方法的return
语句。可以通过递归遍历ReturnStatement
节点来实现。具体步骤如下:traverse(ast, { ObjectMethod(path) { if ( path.node.key.type === 'Identifier' && path.node.key.name === 'setup' ) { let lastReturnIndex = -1; path.traverse({ Function(path) { // 忽略子方法内部的return语句 path.skip(); }, ReturnStatement(returnPath) { // 更新最后一个return语句的位置 lastReturnIndex = returnPath.node.start; } }); // 处理最后一个return语句逻辑 if (lastReturnIndex >= 0) { // 在这里可以处理最后一个return语句 } } } });
通过这些步骤,你可以在开发Vite插件时使用AST找到选项式API的mounted
方法和组合式API的setup
方法中直接包含的最后一个return
语句。
4. 整理开发
通过提需求给GTP的回答,我们大概知道了这类需求开发实现的基本套路。再结合vite插件API文档,基本可以写一个插件了。
具体我整理后的插件简单版代码如下🔍:
import * as parser from '@babel/parser';
import traverse from '@babel/traverse';
const defaultConfig = {
code: `;console.log("插入代码");`, // 具体插入代码
include: (id) => id.includes('/src/pages/') && id.includes('.vue'), // 处理时包含
exclude: (id) => id.includes('?vue&type') || id.includes('App.vue'), // 处理时不包含
};
/**
* 对Vue文件指定位置插入一段代码
* 用法:
* import vueScript from './plugins/plugin.js';
* // vite plugins
* plugins: [..., vueScript(), ...]
* @param config
* @returns {*|{transform(*, *): (*|undefined), name: string}}
*/
export default (config = defaultConfig) => {
return {
name: 'vue-insert-script',
transform(code, id) {
if ((config?.include(id) ?? true) && !config?.exclude(id)) {
try {
let index = -1;
let insertCodes = [];
const ast = parser.parse(code, {
sourceType: 'module', // 指定代码类型,例如 'module' 或 'script'
});
traverse.default(ast, {
// 进入对象方法节点
ObjectMethod(path) {
// 选项api created、mounted
if (
path.node.key.type === 'Identifier' &&
['created', 'mounted'].includes(path.node.key.name)
) {
let index = path.node.end - 1; // created、mounted方法代码最后面插入
if (path.node.key.name === 'created') {
insertCodes.push({
code: config.code,
index,
});
}
if (path.node.key.name === 'mounted') {
insertCodes.push({
code: config.code,
index,
});
}
}
// 组合式api setup
if (path.node.key.type === 'Identifier' && path.node.key.name === 'setup') {
let setupIndex = -1;
path.traverse({
Function(path) {
path.skip();
},
ReturnStatement(returnPath) {
setupIndex = returnPath.node.start - 1; // return语句前面插入
},
});
if (setupIndex >= 0) {
insertCodes.push({
code: config.code,
index: setupIndex,
});
}
}
},
});
if (insertCodes.length) {
return insertCodes
.sort((c1, c2) => c2.index - c1.index)
.reduce((code, item) => {
return code.substring(0, item.index) + item.code + code.substring(item.index);
}, code);
}
} catch (e) {
console.log('报错文件ID:', id);
throw e;
}
}
},
};
};
测试
在测试阶段,我选择了一些代表性的组件进行抽样测试,并且确保了插件在各种场景下都能正常运行。效果如下:
选项式Api
组合式Api
5. 最后
在实践中,我发现某些特殊情况下的处理可能还不够完善,后面还需要一些优化。通过这次插件开发,我体会到 AST 的优势,AST 分析能够更精准、更灵活地操作代码、提高了代码的可扩展和维护性。AST 技术应用前景应该非常广泛,它为前端开发带来了更多可能性。