目录
开始
插件编写
功能一
功能二
功能三
合并功能
运行代码
总结
这篇文章主要讲如何根据注释,通过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
function test3() {}function test3_1() {}
这里准备了三种不同的函数类型,并且各个函数类型都有一个加了注释,另一个没加作为参照对象
就差插件了,下面写插件代码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[2]就知道了
a函数
,前面有4个注释,三个行注释,一个块注释。
其对应的AST解析是这样的:
AST对象中,用leadingComments
表示前面的注释,用trailingComments
表示后面的注释。leadingComments
中确实有4个注释,并且三个行注释,一个块注释,和代码对应上了。
函数要做的就是将其中含有//_tracker
的comment path对象找出来
功能二
判断函数确实需要埋点之后,就要开始插入埋点函数了。但在这之前,还需要做一件事,就是检查埋点函数是否引入,如果没有引入就需要额外引入了
const { addDefault } = require("@babel/helper-module-imports");if (paramCommentPath) {//add Importconst 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 Importconst 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) {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, 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;}// 获取leadingCommentsconst leadingComments = nodeComments.get("leadingComments");const paramCommentPath = hasTrackerComments(leadingComments,options.commentsTrack);//查看作用域中是否有——trackerParam// 如果有注释,就插入函数if (paramCommentPath) {//add Importconst 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
得到输出:
总结
这篇文章写了如何根据函数上方的注释,选择性的给函数埋点。过程详尽,例子通俗易懂,真是不可多得的一篇好文章啊
有任何问题,欢迎金友们留言评论哦~
相关文章:
通过工具babel,给函数都添加埋点[4]
通过工具babel,给埋点函数传递参数[5]