注册
web

🤣泰裤辣!这是什么操作,自动埋点,还能传参?


前言


在上篇文章讲了如何通过手写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);



和上篇文章的入口文件类似,使用了transformFileSyncAPI转译源代码,并将转译之后的代码打印出来。过程中,将手写的插件作为参数传入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

执行结果
image.png



运行结果符合预期



可以看到我们设置的埋点参数确实被放到函数里面了,而且注释里面写了什么,函数的参数就会放什么,那么既然如此,可以传递变量吗?我们来试试看


import "./index.css";

//##箭头函数
//_tracker,_trackerParam={name, age:18}
const test1 = () => {
const name = "gongfu2";
};

const test1_2 = () => {};

在需要插入的代码中,声明了一个变量,然后注释的参数刚好用到了这个变量。
运行代码看看效果
image.png
可以看到,插入的参数确实用了变量,但是引用变量却在变量声明之前,这肯定不行🙅。得改改。
需要将埋点函数插入到函数体的后面,并且是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的前面

再来运行插件:
image.png



很棒,这就是我们要的效果😃




完整代码


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,
};
};

代码中,准备了函数test2test2_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的参数传入其中。
我们运行下代码看看
image.png



运行结果符合预期,很好




完整代码


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插件。



相关文章:



  1. 通过工具babel,给函数都添加埋点
  2. 通过工具babel,根据注释添加埋点

作者:慢功夫
来源:juejin.cn/post/7254032949229764669

0 个评论

要回复文章请先登录注册