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

文章介绍了如何通过Babel插件自动化实现函数埋点,避免手动添加,保持代码整洁。首先创建一个Babel插件,识别并处理不同类型的函数,插入Tracker调用。接着处理Tracker的import,确保文件正确引入。最后讨论了插件的改进,包括支持更多函数类型和处理已有Tracker导入的情况。

目录

前言

一个埋点的Demo

安装依赖

添加测试代码

编写入口文件

编写插件

运行Demo

处理_tracker的import

改进

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

处理埋点函数变量名

总结:


前言

        在项目开发中通常会有埋点的需求,然而当项目过于庞大,给每个函数添加埋点函数是不现实的,这是其一。其二,埋点和业务逻辑没有关系,混入代码中会导致维护混乱🤪。
        基于此,我们可以将埋点的任务用工具来做,而不是手动。这个工具就是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 中处理 import 是在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的时候,就要保证插全局变量名的唯一性

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chengbo_eva

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值