通过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就知道了

image.png

a函数,前面有4个注释,三个行注释,一个块注释。

其对应的AST解析是这样的:


image.png

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();
};


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 227,572评论 6 531
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,071评论 3 414
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 175,409评论 0 373
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,569评论 1 307
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,360评论 6 404
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,895评论 1 321
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,979评论 3 440
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,123评论 0 286
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,643评论 1 333
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,559评论 3 354
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,742评论 1 369
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,250评论 5 356
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,981评论 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,363评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,622评论 1 280
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,354评论 3 390
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,707评论 2 370

推荐阅读更多精彩内容