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

news2024/9/21 14:30:00

目录

前言

一个埋点的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的时候,就要保证插全局变量名的唯一性

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/759468.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

聚焦型光场相机基于立体视差的深度估计原理

聚焦型光场相机可以看作是主透镜将物面成了一个放大或者缩小的虚像,然后每个微透镜阵列对这个经过放大或者缩小的虚像进行二次成像后投影在了ccd平面,其中二次成像的过程可以比拟为一个虚拟阵列相机,利用MLA和主透镜的相关参数就可以以立体视…

Java开发基础系列(三):数据操作

😊 作者: 一恍过去 💖 主页: https://blog.csdn.net/zhuocailing3390 🎊 社区: Java技术栈交流 🎉 主题: Java开发基础系列(三):数据操作 ⏱️ 创作时间: 2023年07月…

Java线程池实现原理

随着计算机行业的飞速发展,摩尔定律逐渐失效,多核CPU成为主流。使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器。J.U.C提供的线程池:ThreadPoolExecutor类,帮助开发人员管理线程并方便地执行并行任务。了解并合理使…

x86架构ubuntu22下运行3DS模拟器Citra

0. 环境 i5 ubuntu22(安装系统时候选择 自动上网下载第三方驱动软件,主要是显卡驱动opengl) 1. apt安装依赖 1.1 SDL2 sudo apt install libsdl2-dev 1.2 OpenSSL (optional) sudo apt install libssl-dev 1.3 Qt 6.2 sudo apt install …

零基础学Python-必备工具安装

文章目录 1. Python 安装与卸载Python 安装包下载安装Python如何验证Python 安装成功扩展 电脑中其实可以装多个不同版本的python 卸载Python 2. Python 开发有哪些常用的IDEPyCharm 安装PyCharm 安装包下载安装PyCharm PyCharm 使用VScode 安装VScode 安装包下载安装VScodeVsc…

libbpf-bootstrap开发指南:网络包监测-tc

目录 前置知识 代码分析 BPF部分 功能说明 struct __sk_buff 说明 bpf_htons & bpf_ntohs 为什么有l2 1、l31 data 数据的排布 用户部分 功能说明 DECLARE_LIBBPF_OPTS 执行效果 前置知识 IP数据包的总长度指的是整个IP数据包的长度,包括IP头部和…

React(2)

题外话&#xff1a;vscode有个插件可以很方便的快速写代码 输入rcc回车 1.组件嵌套 import React, { Component } from reactclass Navbar extends Component{render(){return <div>Navbar</div>} }const Swiper()>{return <div>Swiper</div> }cons…

学习babylon.js --- [2] 项目工程搭建

本文讲述如何搭建babylonjs的项目工程。 一 准备 首先创建一个目录叫MyProject&#xff0c;然后在这个目录里再创建三个目录&#xff1a;dist&#xff0c;public和src&#xff0c;如下&#xff0c; 接着在src目录里添加一个文件叫app.ts&#xff0c;本文使用typescript&#…

论文笔记--PTR: Prompt Tuning with Rules for Text Classification

论文笔记--PTR: Prompt Tuning with Rules for Text Classification 1. 文章简介2. 文章概括3 文章重点技术3.1 Pre-training & Fine-tuning & Prompt-based Fine Tuning3.2 PTR(Prompt Tuning with Rules)3.3 task decomposition3.4 Sub-prompts composition3.5 多个l…

平衡二叉搜索树--AVL详解剖析

目录 一、什么是AVL树 二、AVL树的作用 三、树节点的定义 四、节点的插入 五、旋转 1.左单旋 2.右单旋 左右双旋代码 &#xff1a; 4.右左双旋 一、什么是AVL树 AVL树就是二叉搜索树的进一步的优化&#xff0c;二叉搜索树虽可以缩短查找的效率&#xff0c;但是当数据有…

SDN系统方法 | 1. 概述

随着互联网和数据中心流量的爆炸式增长&#xff0c;SDN已经逐步取代静态路由交换设备成为构建网络的主流方式&#xff0c;本系列是免费电子书《Software-Defined Networks: A Systems Approach》的中文版&#xff0c;完整介绍了SDN的概念、原理、架构和实现方式。原文: Softwar…

SpringFactoriesLoader解析

一、SpringFactoriesLoader 介绍 1.1 SpringFactoriesLoader 简介 SpringFactoriesLoader 工厂加载机制是 Spring 内部提供的一个约定俗成的加载方式&#xff0c;与 java spi 类似&#xff0c;只需要在模块的 META-INF/spring.factories 文件中&#xff0c;以 Properties 类型…

DOT slam论文翻译

DOT:视觉SLAM的动态目标跟踪 摘要 - 在本文中&#xff0c;我们提出了DOT(动态目标跟踪)&#xff0c;这是一个添加到现有SLAM系统中的前端&#xff0c;可以显着提高其在高动态环境中的鲁棒性和准确性。DOT结合实例分割和多视图几何来生成动态对象的掩模&#xff0c;以允许基于刚…

实现 Rollup 插件alias 并使用单元测试提高开发效率

本篇文章是对 实现 Rollup 插件 alias | 使用 TypeScript 实现库的基本流程 | 使用单元测试提高开发效率 的总结。其中涉及到开发一个组件库的诸多知识点。 实现一个经常用的 rollup 插件 alias 首先执行npm init命令初始化一个package.json文件&#xff0c;因为插件使用了ty…

DevOps系列文章之Argo CD 使用

一、什么是 argo cd Argo CD 是用于 Kubernetes 的声明性 GitOps 连续交付工具。 二、为什么使用 argo cd Argo CD 可在指定的目标环境中自动部署所需的应用程序状态&#xff0c;应用程序部署可以在 Git 提交时跟踪对分支&#xff0c;标签的更新&#xff0c;或固定到清单的特…

测试开发之路 ---- 可读性,可维护性,可扩展性

目录 前言 测试框架与测试脚本的目标&#xff08;部分&#xff09; 分层 使用类似 xml 这种可扩展性强的语义存储数据 代码复用&#xff1a;抽象一切可抽象的&#xff0c;减少一切可能的代码相似与重复 活用 java 注解和反射&#xff08;python 中应该也有相关的机制&…

如何从视频中提取音频?分享三个免费的方法给大家!

在数字时代&#xff0c;视频和音频的使用越来越广泛。有时&#xff0c;您可能希望从视频中提取音频&#xff0c;以便单独使用或与他人分享。无需购买昂贵的软件或具备专业技能&#xff0c;下面将介绍三种免费的方法&#xff0c;帮助您从视频中提取音频。这些方法简单易行&#…

Unity学习笔记--siki学院保卫萝卜

生命周期&#xff1a; 在同一个脚本中的执行先后顺序&#xff1a;先左后右 Inspector 赋值 > 外部调用 > Awake > OnEnable > Start 脚本对象的失活与激活不作用于Awake方法&#xff0c;当方法中只有Awake方法时&#xff0c;控制脚本激活失活的对勾会消失掉 当…

vue3 中ref的函数用法

简介 这里说的ref不是响应式ref,是用在组件身上的ref标识&#xff0c;一般都是ref“某一个字符串”&#xff0c;本文介绍第二种用法&#xff0c;ref“()>{}”,对没错&#xff0c;ref可以等于一个回调函数 ref可以是一个回调 <el-input:ref"(vc: any) > (inputAr…

lwip-2.1.3自带的httpd网页服务器使用教程(三)使用CGI获取URL参数(GET类型表单)

上一篇&#xff1a;lwip-2.1.3自带的httpd网页服务器使用教程&#xff08;二&#xff09;使用SSI动态生成网页部分内容 认识URL参数 在上网的时候&#xff0c;我们经常会见到在网址后面带有?AB&CD这样的语法格式。例如&#xff1a;https://blog.csdn.net/ZLK1214/articl…