通过babel插件自动地,给相应函数插入埋点代码
效果
- 源代码
// _tracker
const test1 = ()=> '';
//_tracker
function test2() {
}
//_tracker
const test3 = function () {
}
- 编译之后
import _tracker2 from "./tracker";
// _tracker
const test1 = () => {
_tracker2();
return '';
};
//_tracker
function test2() {
_tracker2();
}
//_tracker
const test3 = function () {
_tracker2();
};
如果注释中有//_tracker,我们就给函数添加埋点。这样做避免了僵硬的给每个函数都添加埋点的情况,让埋点更加灵活
准备babel入口文件index.js
const { transformFileSync } = require('@babel/core');
const path = require('path');
const tracker = require('./babel-plugin-tracker-comment');
const pathFile = path.resolve(__dirname, './sourceCode.js');
const { code } = transformFileSync(pathFile, {
plugins: [[tracker, {trackerPath: './tracker', commentsTrack: '_tracker'}]]
})
console.log(code)
使用transformFileSyncAPI转译源代码,并将转译之后的代码打印出来过程中,将手写的插件作为参数传入plugins: [[tracker, { trackerPath: "tracker", commentsTrack: "_tracker"}]]。除此之外,还有插件的参数
- trackerPath表示埋点函数的路径,插件在插入埋点函数之前会检查是否已经引入该函数,如果没有自动引入.
- commentsTrack标识埋点,如果函数前有这个注释,表示该函数需要埋点.
准备源文件sourceCode.js
// _tracker
const test1 = ()=> '';
//_tracker
function test2() {
}
//_tracker
const test3 = function () {
}
三种不同的函数类型,并且各个函数类型都有一个加了注释
插件编写 babel-plugin-tracker-comment.js
功能一
功能实现过程中,涉及到了读取函数的注释,并且判断注释中是否有//_tracker
const leadingComments = path.get("leadingComments");
const paramCommentPath = hasTrackerComments(leadingComments, options.commentsTrack);
//函数实现
const hasTrackerComments = (leadingComments, comments) => {
if (!leadingComments) {
return null;
}
if (Array.isArray(leadingComments)) {
const res = leadingComments.filter((item) => {
return item.node.value.includes(comments);
});
return res[0] || null;
}
return null;
};
具体函数实现,接收函数前的注释,注释可能会有多个,所以需要一一判断。还接受埋点的标识。如果找到了含有注释标识的注释,就将这行注释返回。否则一律返回null,表示这个函数不需要埋点
那什么是多个注释?
这个很好理解,我们看下AST explorer就知道了

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

AST对象中,用leadingComments表示前面的注释,用trailingComments表示后面的注释。leadingComments中确实有4个注释,并且三个行注释,一个块注释,和代码对应上了。
函数要做的就是将其中含有//_tracker的comment path对象找出来
功能二
判断函数确实需要埋点之后,就要开始插入埋点函数了。但在这之前,还需要做一件事,就是检查埋点函数是否引入,如果没有引入就需要额外引入了
//函数实现
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。还用到了traverseAPI,用来遍历import语句。
如果找到了引入,就获取引入的变量。这个变量在之后埋点的时候需要。即如果引入的变量命名了tracker2,那么埋点的时候埋点函数就是tracker2了
如果没有引入,就插入引入。
addDefault就是引入path的函数,并且会返回插入引用的变量。
功能三
确定好了函数需要埋点,并且确定了埋点函数引入的变量,接下来就插入函数了。
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,还是直接返回的值--()=>''
babel-plugin-tracker-comment.js
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) {
return false;
}
if (Array.isArray(leadingComments)) {
const res = leadingComments.filter((item) => {
return item.node.value.includes(comments);
});
return res[0] || null;
}
return null;
};
//insert path
const insertTracker = (path, state, param) => {
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
import _tracker2 from "./tracker";
// _tracker
const test1 = () => {
_tracker2();
return '';
};
//_tracker
function test2() {
_tracker2();
}
//_tracker
const test3 = function () {
_tracker2();
};