babel 埋点插件

我们通常对 babel 的理解就是它可以帮助我们去处理兼容性,也就是有些 JavaScript 的新特性,可能我们想去使用,但对于某些浏览器来说还并未支持,此时我们就可以通过 babel 将我们的代码降级处理为浏览器兼容的执行版本,以便能够运行在当前和旧版本的浏览器或其他环境中。

Babel 插件就是作用于抽象语法树。Babel 三个主要的处理步骤就是解析(parse),转换(transform),生成(generate)。

  • parse:通过 parser 把源码转成抽象语法树(AST)
  • transform:遍历 AST,调用各种 transform 插件对对抽象语法树(AST)进行变换操作,返回的结果就是我们处理后的 AST。
  • generate:把转换后的 AST 打印成目标代码,并生成 sourcemap

即:code -- (1) - > ast -- (2) - > ast2 -- (3) - > code2

插件实际上就是在处理 AST 抽象语法树,所以需要做到三点:

  • 确认我们要修改的节点类型
  • 找到 AST 中需要修改的属性
  • 将 AST 中需要修改的属性用新生成的属性对象替换

visitor模式

访问者模式是一种将数据操作和数据结构分离的设计模式。对应到 babel traverse 的实现,就是 AST 和 visitor 分离,在 traverse(遍历)AST 的时候,调用注册的 visitor 来对其进行处理。

Visitor 中的每个函数接收 2 个参数:path 和 state

visitor: {
  Program: {
    enter(path, state) {
      ...
    }
    },
  }

埋点

埋点就是在应用中特定的流程收集一些信息,用来跟踪应用使用的状况。

目的是:获取用户行为以及跟踪产品在用户端的使用情况,并以监控数据为基础,指明产品优化的方向

通过在web页面中,添加js脚本,实现对应用的数据监控、性能监控和异常监控(如加载时间、服务器响应时间),最后将统计结果发送到服务器, 后台服务会对统计数据进行分析,进而通过数据对服务器性能有个可靠的了解后续用来进一步优化产品或是提供运营的数据支撑。

埋点只是在函数里面插入了一段代码,这段代码不影响其余逻辑,这种函数插入不影响逻辑的代码的手段叫作函数插桩。

我们能够基于 babel 来实现自动的函数插桩,就是自动埋点。

思路分析

import example1 from 'example1';
import * as example2 from 'example2';
import { example3 } from 'example3';
import 'example4';

function example5() {
  console.log('example5');
}

class Example6 {
  example2() {
    return 'example6';
  }
}

const example7 = () => 'example7';

const example8 = function () {
  console.log('example8');
}

实现埋点要转成这样:

import _tracker2 from 'tracker';
import example1 from 'example1';
import * as example2 from 'example2';
import { example3 } from 'example3';
import 'example4';

function example5() {
  _tracker2();
  console.log('example5');
}

class Example6 {
  example2() {
    _tracker2();
    return 'example6';
  }
}

const example7 = () => 'example7';

const example8 = function () {
  _tracker2();
  console.log('example8');
}

开发个最简单的埋点插件
有两个事情要做:

  • 引入 tracker 模块。若是已经引入过就不引入,没有的话就引入,而且生成个惟一 id 做为标识符
  • 对全部函数在函数体开始插入 tracker 的代码

代码实现

小册《babel插件通关秘籍》中有具体api的详细介绍

模块引入

引入模块这种功能很多插件都需要,这种插件之间的公共函数会放在 helper,这里我们使用 \@babel/helper-module-imports。

const importModule = require('@babel/helper-module-imports');

// 省略一些代码
importModule.addDefault(path, 'tracker',{
  nameHint: path.scope.generateUid('tracker')
})

首先要判断是否被引入过:在 Program 根结点里,通过 enter 的时候判断 state 上是否存在 trackerImportId,即是否已经导入,没有就引入 tracker 模块,用 generateUid 生成唯一 id,然后放到 state。

visitor: {
    Program: {
        enter (path, state) {
          if (!state.trackerImportId) {
              // tracker 模块的 id
              state.trackerImportId  = importModule.addDefault(path, 'tracker',{
                  nameHint: path.scope.generateUid('tracker')
              }).name; 
              // 埋点代码的 AST
              state.trackerAST = api.template.statement(`${state.trackerImportId}()`)();
          }
      }
   }
}

我们在记录 tracker 模块的 id 的时候,也生成调用 tracker 模块的 AST,使用 template.statement.

函数插桩

埋点代码导入之后,下一步就是向函数体中插入代码。

找到对应的函数,这里要处理的有:ClassMethod、ArrowFunctionExpression、FunctionExpression、FunctionDeclaration 这些节点。

有的函数没有函数体,这种要包装一下,而后修改下 return 值。若是有函数体,就直接在开始插入就好了。

'ClassMethod|ArrowFunctionExpression|FunctionExpression|FunctionDeclaration'(path, state) {
  const bodyPath = path.get('body');
  
  //有函数体就在开始插入埋点代码
  if (bodyPath.isBlockStatement()) { 
    console.log('插入埋点代码');
  } else { //没有函数体要包装一下,处理下返回值
    
    //使用template.statement,生成调用tracker模块的AST
    const ast = api.template.statement(`{return PREV_BODY;}`)({ PREV_BODY: bodyPath.node });
    bodyPath.replaceWith(ast);
    console.log('插入埋点代码');
  }
}

这样就实现了自动埋点。

效果演示

示例代码 sourceCode.js

import example1 from 'example1';
import * as example2 from 'example2';
import { example3 } from 'example3';
import 'example4';

function example5() {
  console.log('example5');
}

class Example6 {
  example2() {
    return 'example6';
  }
}

const example7 = () => 'example7';

const example8 = function () {
  console.log('example8');
}

埋点插件 auto-track-plugin.js

这个文件主要就是对我们源代码生成 AST 后的 AST 进行处理,主要在此模块中导入我们的埋点函数

// @babel/helper-plugin-utils  辅助插件开发
// @babel/babel-plugin-import 实现按需加载

const pluginUtils= require('@babel/helper-plugin-utils');
const importModule = require('@babel/helper-module-imports');

const autoTrackPlugin = pluginUtils.declare((api, options, dirname) => {
  api.assertVersion(7);
  
  return {
    name:'plugin-demo',
    visitor: {
        Program: {
            enter (path, state) {
              if (!state.trackerImportId) {
                  // tracker 模块的 id
                  state.trackerImportId  = importModule.addDefault(path, 'tracker',{
                      nameHint: path.scope.generateUid('tracker')
                  }).name; 
                  // 埋点代码的 AST
                  state.trackerAST = api.template.statement(`${state.trackerImportId}()`)();
              }
          }
       }
      'ClassMethod|ArrowFunctionExpression|FunctionExpression|FunctionDeclaration'(path, state) {
        const bodyPath = path.get('body');
        if (bodyPath.isBlockStatement()) {
          bodyPath.node.body.unshift(state.trackerAST);
        } else {
          const ast = api.template.statement(`{${state.trackerImportId}();return PREV_BODY;}`)({ PREV_BODY: bodyPath.node });
          bodyPath.replaceWith(ast);
        }
      }
    }
  }
});
module.exports = autoTrackPlugin;

入口文件 ,index.js

通过 fs 读取文件内容,通过 @babel/parser 中 parser 解析 code,变为 AST,transformFromAstSync 函数配置 plugin。

它的主要作用就是,转化 AST ,然后交给 tracker 函数去处理 AST,拿到处理后的 AST 并且生成新的代码

const { transformFromAstSync } = require('@babel/core');
const parser = require('@babel/parser');
const autoTrackPlugin = require('./plugin/auto-track-plugin');
const fs = require('fs');
const fse = require('fs-extra');
const path = require('path');

const sourceCode = fs.readFileSync(path.join(__dirname, './sourceCode.js'), {
  encoding: 'utf-8'
});

const ast = parser.parse(sourceCode, {
  sourceType: 'unambiguous'
});

const outputDir = path.resolve(__dirname, './lib');
 
  /*
  * 调用函数转化
  *  传入ast 内容
  * */
const { code } = transformFromAstSync(ast, sourceCode, {
  plugins: [[autoTrackPlugin, {
    trackerPath: 'tracker'
  }]]
});

fse.ensureDirSync(outputDir);
fse.writeFileSync(path.join(outputDir, 'lib.js'), code);

将埋点之后的代码,输出到另一个文件夹中,lib/index.js

本地测试可以通过 node ./src/index.js 指令去运行这段代码

import _tracker2 from "tracker";
import example1 from 'example1';
import * as example2 from 'example2';
import { example3 } from 'example3';
import 'example4';

function example5() {
  _tracker2();
  
  console.log('example5');
}

class Example6 {
  example2() {
    _tracker2();
    
    return 'example6';
  }
  
}

const example7 = () => {
  _tracker2();
  
  return 'example7';
};

const example8 = function () {
  _tracker2();
  
  console.log('example8');
};

最后 通过运行我们可以看到输出后的结果以及和源代码的对比结果。

总结

函数插桩是在函数中插入一段逻辑但不影响函数本来逻辑,埋点就是一种常见的函数插桩,能够用 babel 来动作。

实现思路分为引入 tracker 模块和函数插桩两部分:

引入 tracker 模块须要判断 ImportDeclaration 是否包含了 tracker 模块,没有的话就用 @babel/helper-module-import 来引入。

函数插桩就是在函数体开始插入一段代码,若是没有函数体,须要包装一层,而且处理下返回值。

参考:

babel官网 Babel 是什么? · Babel 中文网

Babel 插件通关秘籍 掘金课程

基于 babel 的埋点工具简单实现及思考 基于babel的埋点工具简单实现及思考_CRMEB_InfoQ写作社区

用 babel-plugin 实现按需加载 登录 袋鼠云K2 · 袋鼠云K2

埋点相关:

前端监控和前端埋点方案设计 前端监控和前端埋点方案设计 - 掘金

数据采集与埋点 数据采集与埋点

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值