當前位置: 妍妍網 > 碼農

從未見過的埋點技巧:用註釋輕松搞定,簡直神了!!!

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多名前端小夥伴在等著一起學習哦 -->

  • 廣州的兄弟可以約飯哦,或者約球~我負責打鐵,你負責進球,謝謝~