目录
前言
参数放在注释中
准备入口文件
编写插件
运行代码
完整代码
参数放在局部作用域中
准备源代码
编写插件
运行代码
完整代码
总结
前言
在上篇文章讲了如何通过手写babel插件
自动给函数埋点之后,就有同学问我,自动插入埋点的函数怎么给它传参呢?这篇文章就来解决这个问题
我讲了通过babel来实现自动化埋点,也讲过读取注释给特定函数插入埋点代码,感兴趣的同学可以来主页。
效果是这样的
源代码:
//##箭头函数
//_tracker
const test1 = () => {};
const test1_2 = () => {};
转译之后:
import _tracker from "tracker";
//##箭头函数
//_tracker
const test1 = () => { _tracker(); };
const test1_2 = () => {};
代码中有两个函数,其中一个//_tracker
的注释,另一个没有。转译之后只给有注释的函数添加埋点函数。
要达到这个效果就需要读取函数上面的注释,如果注释中有//_tracker
,我们就给函数添加埋点。这样做避免了僵硬的给每个函数都添加埋点的情况,让埋点更加灵活。
那想要给插入的埋点函数传入参数应该怎么做呢?传入参数可以有两个思路,
- 一个是将参数也放在注释里面,在babel插入代码的时候读取下注释里的内容就好了;
- 另一个是将参数以局部变量的形式放在当前作用域中,在babel插入代码时读取下当前作用域的变量就好;
下面我们来实现这两个思路,大家挑个自己喜欢的方法就好
参数放在注释中
整理下源代码
import "./index.css";
//##箭头函数
//_tracker,_trackerParam={name:'gongfu', age:18}
const test1 = () => {};
//_tracker
const test1_2 = () => {};
代码中,有两个函数,每个函数上都有_tracker
的注释,其中一个注释携带了埋点函数的参数,待会我们就要将这个参数放到埋点函数里
关于如何读取函数上方的注释,大家可以这篇文章:,我就不赘述了
准备入口文件
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",
commentParam: "_trackerParam"
}]
],
});
console.log(code);
和上篇文章的入口文件类似,使用了transformFileSync
API转译源代码,并将转译之后的代码打印出来。过程中,将手写的插件作为参数传入
plugins: [[tracker, { trackerPath: "tracker", commentsTrack: "_tracker"}]]
。
除此之外,还有插件的参数。
trackerPath
表示埋点函数的路径,插件在插入埋点函数之前会检查是否已经引入了该函数,如果没有引入就需要额外引入。commentsTrack
标识埋点,如果函数前的注释有这个,就说明函数需要埋点。判断的标识是动态传入的,这样比较灵活commentParam
标识埋点函数的参数,如果注释中有这个字符串,那后面跟着的就是参数了。就像上面源代码所写的那样。这个标识不是固定的,是可以配置化的,所以放在插件参数的位置上传进去
编写插件
插件的功能有:
- 查看埋点函数是否已经引入
- 查看函数的注释是否含有
_tracker
- 将埋点函数插入函数中
- 读取注释中的参数
前三个功能在上篇文章中已经实现了,下面实现第四个功能
const paramCommentPath = hasTrackerComments(leadingComments, options.commentsTrack);
if (paramCommentPath) {
const param = getParamsFromComment(paramCommentPath, options);
insertTracker(path, param, state);
}
//函数实现
const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}
try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};
const insertTracker = (path, param, state) => {
const bodyPath = path.get("body");
if (bodyPath.isBlockStatement()) {
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
bodyPath.node.body.unshift(ast);
} else {
const ast = template.statement(`{
${state.importTackerId}(${param});
return BODY;
}`)({ BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};
上述代码的逻辑是检查代码是否含有注释_tracker
,如果有的话,再检查这一行注释中是否含有参数,最后再将埋点插入函数。
在检查是否含有参数的过程中,用到了插件参数commentParam
。表示如果注释中含有该字符串,那后面的内容就是参数了。获取参数的方法也是简单的字符串切割。如果没有获取到参数,就一律返回null
获取参数的复杂程度,取决于。先前是否就有一个规范,并且编写代码时严格按照规范执行。 像我这里的规范是埋点参数
commentParam
和埋点标识符_tracker
必须放在一行,并且参数需要是对象的形式。即然是对象的形式,那这一行注释中就不允许就其他的大括号符号"{}"
。 遵从了这个规范,获取参数的过程就变的很简单了。当然你也可以有自己的规范
在执行插入逻辑的函数中,会校验参数param
是否为null,如果是null,生成ast的时候,就不传入param了。
当然你也可以一股脑地传入param,不会影响结果,顶多是生成的埋点函数会收到一个
null
的参数,像这样_tracker(null)
第四个功能也实现了,来看下完整代码
const { declare } = require("@babel/helper-plugin-utils");
const { addDefault } = require("@babel/helper-module-imports");
const { template } = require("@babel/core");
//judge if there are trackComments in 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;
};
const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}
try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};
const insertTracker = (path, param, state) => {
const bodyPath = path.get("body");
if (bodyPath.isBlockStatement()) {
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
bodyPath.node.body.unshift(ast);
} else {
const ast = template.statement(`{
${state.importTackerId}(${param});
return BODY;
}`)({ BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};
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) => {
return {
visitor: {
"ArrowFunctionExpression|FunctionDeclaration|FunctionExpression|ClassMethod": {
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);
// 如果有注释,就插入函数
if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;
const param = getParamsFromComment(paramCommentPath, options);
insertTracker(path, param, state);
}
},
},
},
};
});
运行代码
现在可以用入口文件来使用这个插件代码了
node index.js
执行结果
运行结果符合预期
可以看到我们设置的埋点参数确实被放到函数里面了,而且注释里面写了什么,函数的参数就会放什么,那么既然如此,可以传递变量吗?我们来试试看
import "./index.css";
//##箭头函数
//_tracker,_trackerParam={name, age:18}
const test1 = () => {
const name = "gongfu2";
};
const test1_2 = () => {};
在需要插入的代码中,声明了一个变量,然后注释的参数刚好用到了这个变量。
运行代码看看效果
可以看到,插入的参数确实用了变量,但是引用变量却在变量声明之前,这肯定不行。得改改。
需要将埋点函数插入到函数体的后面,并且是returnStatement
的前面,这样就不会有问题了
const insertTrackerBeforeReturn = (path, param, state) => {
//blockStatement
const bodyPath = path.get("body");
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
if (bodyPath.isBlockStatement()) {
//get returnStatement, by body of blockStatement
const returnPath = bodyPath.get("body").slice(-1)[0];
if (returnPath && returnPath.isReturnStatement()) {
returnPath.insertBefore(ast);
} else {
bodyPath.node.body.push(ast);
}
} else {
ast = template.statement(`{
${state.importTackerId}(${param});
return BODY;
}`)({ BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};
这里将insertTracker
改成了insertTrackerBeforeReturn
。其中关键的逻辑是判断是否是一个函数体,
- 如果是一个函数体,就判断有没有
return
语句,- 如果有
return
,就放在return
前面 - 如果没有
return
,就放在整个函数体的后面
- 如果有
- 如果不是一个函数体,就直接生成一个函数体,然后将埋点函数放在
return
的前面
再来运行插件:
很棒,这就是我们要的效果😃
完整代码
const { declare } = require("@babel/helper-plugin-utils");
const { addDefault } = require("@babel/helper-module-imports");
const { template } = require("@babel/core");
//judge if there are trackComments in 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;
};
const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}
try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};
const insertTrackerBeforeReturn = (path, param, state) => {
//blockStatement
const bodyPath = path.get("body");
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
if (bodyPath.isBlockStatement()) {
//get returnStatement, by body of blockStatement
const returnPath = bodyPath.get("body").slice(-1)[0];
if (returnPath && returnPath.isReturnStatement()) {
returnPath.insertBefore(ast);
} else {
bodyPath.node.body.push(ast);
}
} else {
ast = template.statement(`{ ${state.importTackerId}(${param}); return BODY; }`)({BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};
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) => {
return {
visitor: {
"ArrowFunctionExpression|FunctionDeclaration|FunctionExpression|ClassMethod": {
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);
// 如果有注释,就插入函数
if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;
const param = getParamsFromComment(paramCommentPath, options);
insertTrackerBeforeReturn(path, param, state);
}
},
},
},
};
});
参数放在局部作用域中
这个功能的关键就是读取当前作用域中的变量。在写代码之前,来定一个前提:当前作用域的变量名也和注释中参数标识符一致,也是_trackerParam
准备源代码
import "./index.css";
//##箭头函数
//_tracker,_trackerParam={name, age:18}
const test1 = () => {
const name = "gongfu2";
};
const test1_2 = () => {};
//函数表达式
//_tracker
const test2 = function () {
const age = 1;
_trackerParam = {
name: "gongfu3",
age,
};
};
const test2_1 = function () {
const age = 2;
_trackerParam = {
name: "gongfu4",
age,
};
};
代码中,准备了函数test2
和test2_1
。其中都有_trackerParam
作为局部变量,但test2_1
没有注释//_tracker
编写插件
if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;
//check if have tackerParam
const hasTrackParam = path.scope.hasBinding(options.commentParam);
if (hasTrackParam) {
insertTrackerBeforeReturn(path, options.commentParam, state);
return;
}
const param = getParamsFromComment(paramCommentPath, options);
insertTrackerBeforeReturn(path, param, state);
}
这个函数的逻辑是先判断当前作用域中是否有变量_trackerParam
,有的话,就获取该声明变量的初始值。然后将该变量名作为insertTrackerBeforeReturn
的参数传入其中。
运行下代码
我们运行下代码看看
运行结果符合预期,很好
完整代码
const { declare } = require("@babel/helper-plugin-utils");
const { addDefault } = require("@babel/helper-module-imports");
const { template } = require("@babel/core");
//judge if there are trackComments in 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;
};
const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}
try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};
const insertTrackerBeforeReturn = (path, param, state) => {
//blockStatement
const bodyPath = path.get("body");
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
if (bodyPath.isBlockStatement()) {
//get returnStatement, by body of blockStatement
const returnPath = bodyPath.get("body").slice(-1)[0];
if (returnPath && returnPath.isReturnStatement()) {
returnPath.insertBefore(ast);
} else {
bodyPath.node.body.push(ast);
}
} else {
ast = template.statement(`{${state.importTackerId}(${param}); return BODY;}`)({BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};
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) => {
return {
visitor: {
"ArrowFunctionExpression|FunctionDeclaration|FunctionExpression|ClassMethod": {
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);
// 如果有注释,就插入函数
if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;
//check if have tackerParam
const hasTrackParam = path.scope.hasBinding(options.commentParam);
if (hasTrackParam) {
insertTrackerBeforeReturn(path, options.commentParam, state);
return;
}
const param = getParamsFromComment(paramCommentPath, options);
insertTrackerBeforeReturn(path, param, state);
}
},
},
},
};
});
总结
这篇文章讲了如何给埋点的函数添加参数,参数可以写在注释里,也可以写在布局作用域中。支持动态传递,非常灵活。感兴趣的同学们可以拷一份代码下来跑一跑,相信你们会很有成就感的。
下篇文章来讲讲如何在create-reate-app中使用我们手写的babel插件。