likes
comments
collection
share

🚀芜湖,埋点还可以这么做?这也太简单了

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

前言

在项目开发中通常会有埋点的需求,然而当项目过于庞大,给每个函数添加埋点函数是不现实的,这是其一。其二,埋点和业务逻辑没有关系,混入代码中会导致维护混乱🤪。基于此,我们可以将埋点的任务用工具来做,而不是手动。这个工具就是babel下面我们来看一个小Demo,看看babel埋点是如何实现的

这篇文章适用于了解babel插件开发基础的童鞋,同时想要了解用bable埋点的基本思路的童鞋

一个埋点的Demo

安装依赖

新建一个文件夹babel-tracker, 然后在这个文件夹内按照必要的依赖

mrdir babel-tracker
cd ./babel-tracker
npm init -y
npm i -D @babel/core @babel/helper-plugin-utils

添加测试代码

创建一个测试代码src/sourceCode.js,这个代码是用来测试添加埋点函数

//sourceCode.js

import "./index.css";

//##箭头函数
const test1 = () => {};

代码中有函数表达式,箭头函数,函数声明,类方法四种函数,待会就要在这四种方法里面分别插入埋点函数,就像下面这样

import _tracker from "tracker";
import "./index.css";

//##箭头函数
const test1 = () => {
  _tracker();
};

编写入口文件

然后新建一个文件src/index.js

const { transformFileSync } = require("@babel/core");
const path = require("path");

const pathFile = path.resolve(__dirname, "./sourceCode.js");

//transform ast and generate code
const { code } = transformFileSync(pathFile, {
	plugins: [
    //plugins
	],
});

console.log(code);

这个文件做了三件事:

  1. 获取测试代码的文件路径
  2. 将测试代码用tracker 插件处理
  3. 将处理后的code打印出来

其中的第二步,是先将代码转成AST语法树,然后用插件对AST对象树做一系列的处理,最后将处理好的AST转回js代码。

我们所有的重点就是在用插件处理AST上面。下面来创建一个插件src/babel-plugin-tracker-2.js

编写插件

基本思路是,先识别出这是一个函数,然后将在函数体内部添加一个表达式_tracker()

// 导出一个 Babel 插件的函数。它接受两个参数:
// `api` 是一个 Babel 插件 API 对象,提供了一些可以在插件中使用的方法。
// `options` 是用户在 Babel 配置文件中给该插件指定的选项。
module.exports = (api, options) => {
  // 返回一个插件对象。
  return {
    // `visitor` 对象定义了我们要访问的 AST 节点类型以及对应的处理方法。
    visitor: {
      // 对于 `ArrowFunctionExpression` 类型的节点(箭头函数表达式):
      ArrowFunctionExpression: {
        // 当我们进入一个节点时:
        enter: (path, state) => {
          // `path` 是当前节点(箭头函数表达式)的路径对象,它提供了一些操作当前节点的方法。

          // 获取箭头函数的函数体的路径。
          const bodyPath = path.get("body");

          // 使用 Babel 插件 API 的 `template.statement` 方法创建一个新的 AST 节点,
          // 这个节点表示 `_tracker()` 这个语句。注意我们需要调用返回的函数(`()`)以生成 AST。
          const ast = api.template.statement('_tracker()')();

          // 将新生成的 `_tracker()` 调用语句插入到箭头函数的函数体的开头。
          bodyPath.node.body.unshift(ast);
        },
      },
    },
  };
};

我们将插件导入src/index.js文件中

//index.js

const { transformFileSync } = require("@babel/core");
const path = require("path");
const tracker = require("./babel-plugin-tracker"); //update
const pathFile = path.resolve(__dirname, "./sourceCode.js");

//transform ast and generate code
const { code } = transformFileSync(pathFile, {
	plugins: [[tracker]], //update
});

console.log(code);

运行Demo

好了,将写好的插件导入之后,就可以运行代码看看效果了

node ./src/index.js
🚀芜湖,埋点还可以这么做?这也太简单了

运行成功

可以看到埋点的函数已经被放进去了。可以有个小问题,这个文件运行起来可能会报错,因为没有_tracker函数的import,需要先import才不会报错。

接下来我们来处理这个问题

处理_tracker的import

一般在bable中处理important是在Program的AST节点中处理的,所以需要在插件中处理Program节点.。基本思路是,判断文件中是否有_tracker的import,如果没有,就添加一个导入

// 导入 `@babel/helper-module-imports` 包的 `addDefault` 函数
// 它可以向程序中添加默认导入
const { addDefault } = require("@babel/helper-module-imports");

// 导出一个 Babel 插件的函数。
module.exports = (api, options) => {
  return {
    visitor: {
      ArrowFunctionExpression: {
        enter: (path, state) => {
          //...
        },
      },

      // 对于 `Program` 类型的节点(整个程序):
      Program: {
        // 当我们进入一个节点时:
        enter: (path, state) => {
          // 从插件选项中获取 `_tracker` 函数的导入路径。
          const trackerPath = options.trackerPath;

          // 声明一个标志,初始值为 false,表示我们假设程序中没有导入 `_tracker`。
          let isHasTracker = false;

          // 遍历当前节点(整个程序)的所有子节点。
          path.traverse({
            // 对于 `ImportDeclaration` 类型的节点(导入声明):
            ImportDeclaration(path) {
              // 如果当前导入声明的来源与 `_tracker` 函数的导入路径相同:
              if (path.node.source.value === trackerPath) {
                // 将标志设置为 true,表示我们找到了 `_tracker` 的导入。
                isHasTracker = true;

                // 停止遍历,因为我们已经找到了 `_tracker` 的导入。
                path.stop();
              }
            },
          });

          // 如果我们遍历完所有导入声明后都没有找到 `_tracker` 的导入:
          if (isHasTracker === false) {
            // 使用 `addDefault` 函数向程序中添加 `_tracker` 函数的默认导入。
            // `options.trackerPath` 是 `_tracker` 函数的导入路径,
            // `{ nameHint: "_tracker" }` 是一个选项对象,用于指定导入的变量名。
            addDefault(path, options.trackerPath, { nameHint: "_tracker" });
          }
        },
      },
    },
  };
};

添加了一个Program的处理函数,在逻辑中,遍历的了整个文件的import语句,并且一一比较了import的source,如果其中的source.value_tracker,说明文件已经导入了_tracker

一个import语句,如:import a from 'a.js' 那么可以通过node.source.value,获取这个AST节点中的a.js

在判断_tracker的导入路径的时候,代码中是从options.trackerPath中获取的,而options的配置在插件的引用的地方。 并没有hard code

//transform ast and generate code
const { code } = transformFileSync(pathFile, {
	plugins: [[tracker,{ trackerPath: 'tracker'}]], //update
});

如果没有发现tracker的导入,就需要手动添加了。代码中借用的是addDefault的依赖帮忙添加的。其中{ nameHint: "_tracker" }用来设置_tracker作为埋点函数的变量名。我们来跑下代码:

node ./src/index.js
🚀芜湖,埋点还可以这么做?这也太简单了

添加成功


看起来大功告成了。我们来捋一下过程:

  1. 遍历函数,在函数中添加埋点函数
  2. 查找是否有tracker的导入,如果没有,就手动添加

过程很简单,但过于简陋,有几处可以改进的地方:

  1. 不仅给箭头函数添加,还可以给函数表达式,函数声明,类方法等函数形式添加埋点
  2. 添加tracker的导入,埋点函数变量名_tracker可能会被使用过,所以最好是随机生成埋点函数的变量名
  3. 如果文件中已经导入了tracker,我们需要获取用户定义的变量名,并且使用该变量名给函数添加埋点。例如import _tracker2 from 'tracker'; ,这时候调用埋点就要变成 _tracker2();

改进

给其他的函数类型添加埋点

visitor: {
  "ArrowFunctionExpression|FunctionDeclaration|ClassMethod|FunctionExpression": {
      // 当我们进入一个节点时:
      enter: (path, state) => {
        // `path` 是当前节点(箭头函数表达式)的路径对象,它提供了一些操作当前节点的方法。
  
        // 获取箭头函数的函数体的路径。
        const bodyPath = path.get("body");
  
        // 使用 Babel 插件 API 的 `template.statement` 方法创建一个新的 AST 节点,
        // 这个节点表示 `_tracker()` 这个语句。注意我们需要调用返回的函数(`()`)以生成 AST。
        const ast = api.template.statement('_tracker()')();
  
        // 将新生成的 `_tracker()` 调用语句插入到箭头函数的函数体的开头。
        bodyPath.node.body.unshift(ast);
      },
    },
},

babel提供这样的功能,字符串拼接的方法来表示遍历多种类型的AST,这样就完成了多种函数类型都可以差入埋点函数了我们修改测试代码,并且运行看看

import "./index.css";

//##箭头函数
const test1 = () => {};

//函数表达式
const test2 = function () {};

// 函数声明
function test3() {}

// 类方法
class test4 {
    test4_0() {}

    test4_1 = () => {};

    test4_2 = function () {};
}
node ./src/index.js
🚀芜湖,埋点还可以这么做?这也太简单了

每个函数都有埋点

不过有一个点,如果箭头函数直接返回结果,现有的代码是不支持的,形如const test_5 = ()=>0,函数体只是一个statement,而不是一个数组,所以强行执行unshift操作会报错。🚀芜湖,埋点还可以这么做?这也太简单了需要对代码做些修改

visitor: {
  "ArrowFunctionExpression|FunctionDeclaration|ClassMethod|FunctionExpression": {
    // 当我们进入一个节点时:
    enter: (path, state) => {
      // `path` 是当前节点(箭头函数表达式)的路径对象,它提供了一些操作当前节点的方法。

      // 获取箭头函数的函数体的路径。
      const bodyPath = path.get("body");

      // 使用 Babel 插件 API 的 `template.statement` 方法创建一个新的 AST 节点,
      // 这个节点表示 `_tracker()` 这个语句。注意我们需要调用返回的函数(`()`)以生成 AST。
      const ast = api.template.statement('_tracker()')();
      if (bodyPath.isBlockStatement()) {
        bodyPath.node.body.unshift(ast);
      } else {
        const ast2 = api.template.statement(`{
            _tracker();
            return BODY;
        }`)({ BODY: bodyPath.node });

        bodyPath.replaceWith(ast2);
      }
    }
  }
}

在代码中,做了一个对函数节点body属性值类型的判断,如果是isBlockStatement,那就可以执行unshift,如果不是,说明函数单纯返回了一个值,这时候就需要将函数体变成blockStatement,并且函数的返回值依然是原来的值。形如const test_5 = ()=>0变成const test_5 = ()=>{ return 0; }。这样就可以添加埋点函数了运行看看:🚀芜湖,埋点还可以这么做?这也太简单了

搞定

处理埋点函数变量名

visitor: {
  "ArrowFunctionExpression|FunctionDeclaration|ClassMethod|FunctionExpression": {
    enter: (path, state) => {
      const types = api.types;
      const bodyPath = path.get("body");
      const ast = state.trackerAst;
      if (types.isBlockStatement(bodyPath.node)) {
        bodyPath.node.body.unshift(ast);
      } else {
        const ast2 = api.template.statement(`{
              ${state.importTrackerId}();
              return BODY;
            }`)({ BODY: bodyPath.node });

        bodyPath.replaceWith(ast2);
      }
    },
  },

  Program: {
    enter: (path, state) => {
      const trackerPath = options.trackerPath;
      path.traverse({
        ImportDeclaration(path) {
          if (path.node.source.value === trackerPath) {
            const specifiers = path.get("specifiers.0");
            state.importTrackerId = specifiers.get("local").toString();
            path.stop();
          }
        },
      });

      if (!state.importTrackerId) {
        state.importTrackerId = addDefault(path, options.trackerPath, {
          nameHint: path.scope.generateUid("tracker"),
        }).name;
      }

      state.trackerAst = api.template.statement(`${state.importTrackerId}();`)();
    },
  },
},
  1. 使用了path.scope.generateUid("tracker")来生成当前作用域内唯一的变量。
  2. 借助state,来传递生成的变量,或者是已经定义的变量
  3. 在插入埋点函数的时候,就可以读取state中的变量了

总结:

这篇文章较为基础,讲了如何在函数中添加埋点函数,以及如何处理埋点函数的import。在埋点的时候,需要注意一下几个问题:

  1. 函数形态的多样性
  2. 埋点函数的变量是否已经定义,如果已经定义,插入埋点的时候,就要使用已经定义的变量名;如果没有定义,插入import的时候,就要保证插全局变量名的唯一性

对每个函数都执行插入埋点操作还是有问题,实际情况并不需要这么做。下篇文章讲讲如何根据注释来添加埋点。

相关文章:

  1. 通过工具babel,根据注释来埋点
  2. 通过工具babel,向埋点函数传参
转载自:https://juejin.cn/post/7238431954126929981
评论
请登录