30分钟开发Vue 3插入客制化脚本插件

1. 引言

嘿,大家好,我是星辰编程理财,今天要和大家聊聊一个让我颇有心得的话题:Vue组件的客制化方式之前-"埋点"。或许你也曾为埋点这个问题烦恼过,在历史的包袱下加上技术债,我们需要实现一套客制化的脚本,埋点成了我们选择的方向。

为什么要进行 Vue 组件的埋点?

曾经,我们在没有引入Vue框架是,代码里面到处都是埋点,随着业务的增长和技术迭代,需要向Vue框架下过度。于是,在这个痛点的驱使下,开始对迁移和兼容代码。通过我们统计分析发现,会有一部分公共埋点的位置。如果每个文件都去加上通用埋点代码,工作量大并和业务代码耦合。于是采用编译时插入代码的方式实现。

为何使用AST?

最初,我们尝试了简单的字符串匹配,想着解决问题应该不难。但是,特征字符串太少,简单匹配往往事倍功半,不好扩展,而且容易翻车。于是AST算是一个稳妥,虽然不太熟悉开发时间要长一些。

2. AST 技术调研

首先,我开始进行了 AST 技术的调研,由于技术上我们使用Vite作为开发及打包工具。于是直接问GTP:

Q: 开发vite插件,如何在插件中用ast分析代码?请简要回答。 A: 在Vite插件中使用AST(抽象语法树)来分析代码可以通过以下步骤完成:

  1. 安装依赖:首先,在你的插件项目中安装 @babel/parser 或其他适用的AST解析器。 npm install @babel/parser --save-dev
  2. 引入解析器:在你的插件代码中引入AST解析器。 const parser = require('@babel/parser');
  3. 解析代码:使用解析器来解析你感兴趣的代码文件。
js 复制代码
const code = fs.readFileSync('path/to/your/file.js', 'utf-8');
const ast = parser.parse(code, {
    sourceType: 'module',
    plugins: ['jsx']  // 如果你的代码中包含JSX
});
  1. 遍历AST:通过遍历AST节点,你可以获取代码中的各种信息。
js 复制代码
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语句。下面是具体的步骤:

  1. 找到选项式API的mounted方法: 在AST遍历过程中,可以通过匹配对象方法节点中键名为mounted的节点来找到选项式API的mounted方法。具体步骤如下:

    javascript 复制代码
    traverse(ast, {
        ObjectMethod(path) {
            if (
                path.node.key.type === 'Identifier' &&
                path.node.key.name === 'mounted'
            ) {
                // 处理选项式API的mounted方法逻辑
                // path.node 包含了该方法的完整信息
            }
        }
    });
  2. 找到组合式API的setup方法中的最后一个return语句: 对于组合式API的setup方法,需要在遍历过程中找到最后一个return语句,但要忽略setup方法内部的子方法的return语句。可以通过递归遍历ReturnStatement节点来实现。具体步骤如下:

    javascript 复制代码
    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文档,基本可以写一个插件了。

具体我整理后的插件简单版代码如下🔍:

js 复制代码
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 技术应用前景应该非常广泛,它为前端开发带来了更多可能性。**

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax