当前位置: 欣欣网 > 码农

从未见过的埋点技巧:用注释轻松搞定,简直神了!!!

2024-03-08码农

前言

大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~

这篇文章主要讲如何根据注释,通过babel插件自动地,给相应函数插入埋点代码,在实现埋点逻辑和业务逻辑分离的基础上,配置更加灵活

这篇文章想要达到的效果:
源代码:

//##箭头函数
//_tracker
const test1 = () => {};

const test1_2 = () => {};

转译之后:

import _tracker from"tracker";
//##箭头函数
//_tracker
const test1 = () => {
_tracker();
};
const test1_2 = () => {};

代码中有两个函数,其中一个 //_tracker 的注释,另一个没有。转译之后只给有注释的函数添加埋点函数。
要达到这个效果就需要读取函数上面的注释,如果注释中有 //_tracker ,我们就给函数添加埋点。这样做避免了僵硬的给每个函数都添加埋点的情况,让埋点更加灵活。

下面让我们来看看怎么做

开始

准备babel入口文件 index.js

const { transformFileSync } = require("@babel/core");
const path = require("path");
const tracker = require("./babel-plugin-tracker-comment.js");
const pathFile = path.resolve(__dirname, "./sourceCode.js");
//transform ast and generate code
const { code } = transformFileSync(pathFile, {
plugins: [[tracker, { trackerPath"tracker"commentsTrack"_tracker" }]],
});
console.log(code);

这里我们使用 transformFileSync API转译源代码,并将转译之后的代码打印出来。过程中,将手写的插件作为参数传入 plugins: [[tracker, { trackerPath: "tracker", commentsTrack: "_tracker"}]] 。除此之外,还有插件的参数

  • trackerPath 表示埋点函数的路径,插件在插入埋点函数之前会检查是否已经引入了该函数,如果没有引入就需要额外引入。

  • commentsTrack 标识埋点,如果函数前的注释有这个,就说明函数需要埋点。判断的标识是动态传入的,这样比较灵活

  • 入口文件准备好了,接下来准备下源代码文件 sourceCode.js

    import"./index.css";
    //##箭头函数
    //_tracker
    const test1 = () => {};
    const test1_2 = () => {};
    //函数表达式
    //_tracker
    const test2 = function () {};
    const test2_1 = function () {};
    // 函数声明
    //_tracker
    functiontest3() {}
    functiontest3_1() {}




    这里准备了三种不同的函数类型,并且各个函数类型都有一个加了注释,另一个没加作为参照对象
    就差插件了,下面写插件代码 babel-plugin-tracker-comment.js

    插件编写

    功能一

    功能实现过程中,涉及到了读取函数的注释,并且判断注释中是否有 //_tracker

    const leadingComments = path.get("leadingComments");
    const paramCommentPath = hasTrackerComments(leadingComments, options.commentsTrack);
    //函数实现
    const hasTrackerComments = (leadingComments, comments) => {
    if (!leadingComments) {
    returnnull;
     }
    if (Array.isArray(leadingComments)) {
    const res = leadingComments.filter((item) => {
    return item.node.value.includes(comments);
    });
    return res[0] || null;
     }
    returnnull;
    };

    具体函数实现,接收函数前的注释,注释可能会有多个,所以需要一一判断。还接受埋点的标识。如果找到了含有注释标识的注释,就将这行注释返回。否则一律返回 null ,表示这个函数不需要埋点

    那什么是多个注释?

    这个很好理解,我们看下 AST explorer 就知道了


    a函数 ,前面有4个注释,三个行注释,一个块注释。
    其对应的AST解析是这样的:


    AST对象中,用 leadingComments 表示前面的注释,用 trailingComments 表示后面的注释。 leadingComments 中确实有4个注释,并且三个行注释,一个块注释,和代码对应上了。
    函数要做的就是将其中含有 //_tracker 的comment path对象找出来

    功能二

    判断函数确实需要埋点之后,就要开始插入埋点函数了。但在这之前,还需要做一件事,就是检查埋点函数是否引入,如果没有引入就需要额外引入了

    const { addDefault } = require("@babel/helper-module-imports");

    if (paramCommentPath) {
    //add Import
    const programPath = path.hub.file.path;
    const importId = checkImport(programPath, options.trackerPath);
    state.importTackerId = importId;
    }
    //函数实现
    const checkImport = (programPath, trackPath) => {
    let importTrackerId = "";
    programPath.traverse({
    ImportDeclaration(path) {
    const sourceValue = path.get("source").node.value;
    if (sourceValue === trackPath) {
    const specifiers = path.get("specifiers.0");
    importTrackerId = specifiers.get("local").toString();
    path.stop();
    }
    },
    });
    if (!importTrackerId) {
    importTrackerId = addDefault(programPath, trackPath, {
    nameHint: programPath.scope.generateUid("tracker"),
    }).name;
    }
    return importTrackerId;
    };


    拿到 import语句 需要program节点。 checkImport 函数的实现就是在当前文件中,找出埋点函数的引入。寻找的过程中,用到了引入插件时传入的参数 trackerPath 。还用到了 traverse API,用来遍历 import语句
    如果找到了引入,就获取引入的变量。这个变量在之后埋点的时候需要。即如果引入的变量命名了 tracker2 ,那么埋点的时候埋点函数就是 tracker2
    如果没有引入,就插入引入。

    addDefault 就是引入path的函数,并且会返回插入引用的变量。

    功能三

    确定好了函数需要埋点,并且确定了埋点函数引入的变量,接下来就插入函数了。

    if (paramCommentPath) {
    //add Import
    const programPath = path.hub.file.path;
    const importId = checkImport(programPath, options.trackerPath);
    state.importTackerId = importId;
    insertTracker(path, state);
    }
    const insertTracker = (path, state) => {
    const bodyPath = path.get("body");
    if (bodyPath.isBlockStatement()) {
    const ast = template.statement(`${state.importTackerId}();`)();
    bodyPath.node.body.unshift(ast);
    else {
    const ast = template.statement(`{
    ${state.importTackerId}();
    return BODY;
    }`
    )({ BODY: bodyPath.node });
    bodyPath.replaceWith(ast);
    }
    };

    在生成埋点函数的时候,就用到了之前获取到的埋点函数的变量 importTackerId 。还有在实际插入的时候,要区分函数体是一个 Block ,还是直接返回的值-- ()=>''

    合并功能

    三个功能都写好了,接下来将三个功能合起来,就是我们的插件代码了

    const { declare } = require("@babel/helper-plugin-utils");
    const { addDefault } = require("@babel/helper-module-imports");
    const { template } = require("@babel/core");
    //get comments path from leadingComments
    const hasTrackerComments = (leadingComments, comments) => {
    if (!leadingComments) {
    returnfalse;
    }
    if (Array.isArray(leadingComments)) {
    const res = leadingComments.filter((item) => {
    return item.node.value.includes(comments);
    });
    return res[0] || null;
    }
    returnnull;
    };
    //insert path
    const insertTracker = (path, param, state) => {
    const bodyPath = path.get("body");
    if (bodyPath.isBlockStatement()) {
    const ast = template.statement(`${state.importTackerId}(${param});`)();
    bodyPath.node.body.unshift(ast);
    else {
    const ast = template.statement(`{
    ${state.importTackerId}(${param});
    return BODY;
    }`
    )({ BODY: bodyPath.node });
    bodyPath.replaceWith(ast);
    }
    };
    //check if tacker func was imported
    const checkImport = (programPath, trackPath) => {
    let importTrackerId = "";
    programPath.traverse({
    ImportDeclaration(path) {
    const sourceValue = path.get("source").node.value;
    if (sourceValue === trackPath) {
    const specifiers = path.get("specifiers.0");
    importTrackerId = specifiers.get("local").toString();
    path.stop();
    }
    },
    });
    if (!importTrackerId) {
    importTrackerId = addDefault(programPath, trackPath, {
    nameHint: programPath.scope.generateUid("tracker"),
    }).name;
    }
    return importTrackerId;
    };
    module.exports = declare((api, options) => {
    console.log("babel-plugin-tracker-comment");
    return {
    visitor: {
    "ArrowFunctionExpression|FunctionDeclaration|FunctionExpression": {
    enter(path, state) {
    let nodeComments = path;
    if (path.isExpression()) {
    nodeComments = path.parentPath.parentPath;
    }
    // 获取leadingComments
    const leadingComments = nodeComments.get("leadingComments");
    const paramCommentPath = hasTrackerComments(leadingComments,options.commentsTrack);
    //查看作用域中是否有——trackerParam
    // 如果有注释,就插入函数
    if (paramCommentPath) {
    //add Import
    const programPath = path.hub.file.path;
    const importId = checkImport(programPath, options.trackerPath);
    state.importTackerId = importId;
    insertTracker(path, state);
    }
    },
    },
    },
    };
    });







    在获取注释的时候,代码中并不是直接获取到 path leadingComments ,这是为什么?
    比如这串代码:

    //_tracker
    const test1 = () => {};

    我们在函数中遍历得到的path是 ()=>{} ast的path,这个path的 leadingComments 其实是 null ,而想要获取 //_tracker ,我们真正需要拿到的path,是注释下面的 变量声明语句 。所以在代码中有判断是否为表达式,如果是,那就需要先 parentPath ,得到 赋值表达式 path ,然后在 parentPath ,才能拿到 变量声明语句

    运行代码

    node index.js

    得到输出:

    总结

    这篇文章写了如何根据函数上方的注释,选择性的给函数埋点。过程详尽,例子通俗易懂,真是不可多得的一篇好文章啊

    作者:慢功夫 https://juejin.cn/post/7253744712409088057

    结语

    我是林三心

  • 一个待过 小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司 的作死型前端选手;

  • 一个偏前端的全干工程师;

  • 一个不正经的掘金作者;

  • 逗比的B站up主;

  • 不帅的小红书博主;

  • 喜欢打铁的篮球菜鸟;

  • 喜欢历史的乏味少年;

  • 喜欢rap的五音不全弱鸡如果你想一起学习前端,一起摸鱼,一起研究简历优化,一起研究面试进步,一起交流历史音乐篮球rap,可以来俺的摸鱼学习群哈哈,点这个,有7000多名前端小伙伴在等着一起学习哦 -->

  • 广州的兄弟可以约饭哦,或者约球~我负责打铁,你负责进球,谢谢~