likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 53

前言

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

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

运行结果符合预期

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

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

代码中,准备了函数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的参数传入其中。我们运行下代码看看🤣泰裤辣!这是什么操作,自动埋点,还能传参?

运行结果符合预期,很好

完整代码

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,根据注释添加埋点
转载自:https://juejin.cn/post/7254032949229764669
评论
请登录