前言
大家好,我是林三心,用最通俗易懂的話講最難的知識點是我的座右銘,基礎是進階的前提是我的初心~
這篇文章主要講如何根據註釋,透過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多名前端小夥伴在等著一起學習哦 -->
廣州的兄弟可以約飯哦,或者約球~我負責打鐵,你負責進球,謝謝~