通过注释来埋点

目录

开始

插件编写

功能一

功能二

功能三

合并功能

运行代码

总结


这篇文章主要讲如何根据注释,通过babel插件自动地,给相应函数插入埋点代码,在实现埋点逻辑和业务逻辑分离的基础上,配置更加灵活

这篇文章想要达到的效果:
源代码:

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


const test1_2 = () => {};

转译之后:

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

const test1_2 = () => {};

代码中有两个函数,其中一个//_tracker的注释,另一个没有。转译之后只给有注释的函数添加埋点函数。
要达到这个效果就需要读取函数上面的注释,如果注释中有//_tracker,我们就给函数添加埋点。这样做避免了僵硬的给每个函数都添加埋点的情况,让埋点更加灵活。

下面让我们来看看怎么做

开始

准备babel入口文件index.js

const { transformFileSync } = require("@babel/core");
const path = require("path");
const tracker = require("./babel-plugin-tracker-comment.js");

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

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

console.log(code);

这里我们使用transformFileSyncAPI转译源代码,并将转译之后的代码打印出来。过程中,将手写的插件作为参数传入plugins: [[tracker, { trackerPath: "tracker", commentsTrack: "_tracker"}]]。除此之外,还有插件的参数

  • trackerPath表示埋点函数的路径,插件在插入埋点函数之前会检查是否已经引入了该函数,如果没有引入就需要额外引入。

  • commentsTrack标识埋点,如果函数前的注释有这个,就说明函数需要埋点。判断的标识是动态传入的,这样比较灵活

入口文件准备好了,接下来准备下源代码文件sourceCode.js

import "./index.css";

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

const test1_2 = () => {};

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

const test2_1 = function () {};

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

function test3_1() {}

这里准备了三种不同的函数类型,并且各个函数类型都有一个加了注释,另一个没加作为参照对象
就差插件了,下面写插件代码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[2]就知道了

a函数,前面有4个注释,三个行注释,一个块注释。
其对应的AST解析是这样的:


AST对象中,用leadingComments表示前面的注释,用trailingComments表示后面的注释。leadingComments中确实有4个注释,并且三个行注释,一个块注释,和代码对应上了。
函数要做的就是将其中含有//_tracker的comment path对象找出来

功能二

判断函数确实需要埋点之后,就要开始插入埋点函数了。但在这之前,还需要做一件事,就是检查埋点函数是否引入,如果没有引入就需要额外引入了

const { addDefault } = require("@babel/helper-module-imports");


if (paramCommentPath) {
  //add Import
  const programPath = path.hub.file.path;
  const importId = checkImport(programPath, options.trackerPath);
  state.importTackerId = importId;
}

//函数实现
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的函数,并且会返回插入引用的变量。

功能三

确定好了函数需要埋点,并且确定了埋点函数引入的变量,接下来就插入函数了。

if (paramCommentPath) {
  //add Import
  const programPath = path.hub.file.path;
  const importId = checkImport(programPath, options.trackerPath);
  state.importTackerId = importId;
  
  insertTracker(path, state);
}
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,还是直接返回的值--()=>''

合并功能

三个功能都写好了,接下来将三个功能合起来,就是我们的插件代码了

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, param, state) => {
  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);
          }
        },
      },
    },
  };
});

在获取注释的时候,代码中并不是直接获取到pathleadingComments,这是为什么?
比如这串代码:

//_tracker
const test1 = () => {};

我们在函数中遍历得到的path是()=>{}ast的path,这个path的leadingComments其实是null,而想要获取//_tracker,我们真正需要拿到的path,是注释下面的变量声明语句。所以在代码中有判断是否为表达式,如果是,那就需要先parentPath,得到赋值表达式path,然后在parentPath,才能拿到变量声明语句

运行代码

node index.js

得到输出:

总结

这篇文章写了如何根据函数上方的注释,选择性的给函数埋点。过程详尽,例子通俗易懂,真是不可多得的一篇好文章啊
有任何问题,欢迎金友们留言评论哦~

相关文章:

  1. 通过工具babel,给函数都添加埋点[4]

  2. 通过工具babel,给埋点函数传递参数[5]

<think>首先,用户的问题是想在微信小程序中实现元素曝光的埋点功能。我需要根据提供的引用信息来回答。这些引用是关于微信小程序埋点的解决方案,包括神策数据SDK、Uniapp实现全局事件监听、uni-app微信小程序广点通事件埋点和小程序组件化埋点实践。关键点是"元素曝光",这意味着当用户滚动到小程序页面上的某个元素时,触发一个事件来进行数据埋点。类似IntersectionObserver,它用于检测元素是否进入视口。从引用中提取相关信息:-引用[1]:神策数据的小程序SDK支持全埋点,可能通过代理技术实现自动化数据采集。-引用[2]:提到在Uniapp-微信小程序中实现全局事件监听,进行非侵入式数据埋点。劫持事件来捕获特定事件。-引用[3]:涉及uni-app微信小程序的广点通事件埋点,包括如何在特定条件下调用埋点函数,如PAGE_VIEW事件。-引用[4]:提到了微信小程序组件化埋点实践中使用createIntersectionObserver,这是微信小程序提供的API,用于观察组件是否进入视口,非常适合元素曝光检测。用户的问题专门问"实现元素曝光的埋点方案",所以引用[4]直接相关,因为它提到了createIntersectionObserver。我还需要参考用户之前的上下文。但用户消息中说:"上述引用仅供参考,并不是用户真正的需求,请尽可能多的参考用户上一次的问题和你的回答。"回顾历史消息:用户上一个问题是相同的(见"UserQuery:我想在微信小程序中实现元素曝光的埋点功能"),而我的回答应该基于引用。在这个消息之前,用户说:"参考站内引用:"然后列出引用。而我的系统指令要求我使用中文回答,结构清晰,确保回答真实可靠,并生成相关问题。我的回答结构应该:1.**清晰结构**:逐步解释如何实现元素曝光埋点。2.**使用引用**:融入引用中的信息,自然地添加引用标识,如[^1],[^2]。3.**生成相关问题**:在末尾添加相关问题部分。4.**LaTeX格式**:检查行内数学表达式使用$...$,独立公式用$$...$$。但在这种上下文中,可能不需要数学内容,除非提及数学概念。问题主要是技术实现,不太涉及数学,所以可能不需要LaTeX。确认一下:如果没有数学内容,就省略。5.**系统级指令**:所有行内数学用$...$,但我这里可能没有数学。独立公式用$$...$$单独成段。使用中文。现在,构建回答:-**开头**:直接回应问题,解释什么是元素曝光埋点。-**核心方案**:基于引用,尤其是引用[4],说明使用createIntersectionObserverAPI。微信小程序原生支持这个API来观察元素是否曝光。-引用[4]提到:"constob=opt.context?opt.context.createIntersectionObserver(observerOptions):wx.createIntersectionObserver(null,observerOptions);"这表明在组件化环境中使用。-**实现步骤**:分步指导用户如何实现:1.**引入API**:在页面或组件中使用wx.createIntersectionObserver或小程序的API。2.**设置观察器**:创建IntersectionObserver实例,设置阈值、视口等选项。3.**观察目标元素**:在页面的onReady生命周期中,选择需要曝光的元素并观察它。4.**监听回调**:当元素进入视口时,触发回调函数,执行埋点上报。-**埋点SDK**:结合引用,提到可以使用SDK如神策数据SDK[^1]来进行数据上报。-**非侵入式方式**:如引用[2]提到的全局事件监听,可以考虑使用中间件或拦截事件。-**代码示例**:提供一个简单的代码示例,展示如何在微信小程序页面中使用createIntersectionObserver实现元素曝光埋点。代码示例应该使用正确的语法,由于是中文回答,代码块可能保持英文,但注释可以用中文。结构回答:1.**引言**:解释元素曝光埋点和常用方案。2.**详细步骤**:逐步描述实现方法。3.**代码片段**:提供示例代码。4.**总结和建议**:快速建议最佳实践。5.**相关问题**:添加相关问题部分。最后,生成相关问题:-如:1.如何在uni-app框架中使用元素曝光埋点?2.优化元素曝光性能的方法?3.处理小程序中多个元素曝光的策略?确保引用标识自然添加:[^1],[^2],[^3],[^4]。完整回答大纲:-步骤1:理解需求,元素曝光埋点当元素进入视口时触发事件。-步骤2:使用微信小程序原生API,createIntersectionObserver。参考[^4]。-步骤3:在页面生命周期中实现。-步骤4:回调中进行埋点上报,可选SDK如神策SDK[^1]。-代码示例:```javascriptPage({onReady(){constobserver=wx.createIntersectionObserver(this);observer.relativeToViewport();//相对视口observer.observe('.target-selector',(res)=>{if(res.intersectionRatio>0){//元素曝光,进行埋点wx.request({url:'埋点API',data:{event:'exposure',elementId:'someId'},});//或使用SDK,如sensors.track('element_exposure')}});},});```-相关问题。</think> ### 微信小程序实现元素曝光埋点方案 在微信小程序中,"元素曝光埋点"是指在用户滚动页面时,当某个UI组件(如按钮、图片或文本块)进入屏幕可视区域时自动触发数据上报,常用于追踪用户浏览行为(例如广告曝光统计或内容阅读量统计)。以下是基于微信小程序原生支持的实现方案,结合引用中提到的SDK和API进行设计。方案采用 **IntersectionObserver API**(元素交集观察器),这是微信小程序推荐的视口曝光检测机制,支持非侵入式集成[^4]。 --- ### 一、实现步骤详解(原生微信小程序方案) #### 1. **核心API介绍** - 使用 **`wx.createIntersectionObserver`** API创建观察器对象,可监听元素是否进入视口: - **`threshold` 参数**:设置触发曝光的阈值(例如`0.5`表示元素50%进入视口时上报)。 - **`observe()` 方法**:绑定目标元素的选择器。 - **回调函数**:当元素曝光时执行埋点上报逻辑[^4]。 #### 2. **完整实现流程** - **步骤1:在页面或组件中声明观察器** - 在页面`onReady`生命周期中初始化观察器,避免元素未渲染问题。 - **步骤2:配置观察器选项** - 设置阈值和参考视口(`relativeToViewport()`确保相对于屏幕窗口)。 - **步骤3:绑定目标元素** - 使用CSS选择器(如`.class`或`#id`)指定需曝光的组件。 - **步骤4:曝光回调上报** - 在回调中调用SDK埋点方法或自定义请求。引用[1]和[3]的SDK可用于上报逻辑。 #### 3. **代码示例** ```javascript // 页面JS文件(例如index.js) Page({ onReady() { // 创建观察器,设置阈值0.3(元素30%进入视口即触发)[^4] const observer = wx.createIntersectionObserver(this, { thresholds: [0.3] }).relativeToViewport(); // 观察目标元素(例如选择器为#ad-banner的广告组件) observer.observe('#ad-banner', (res) => { if (res.intersectionRatio > 0) { console.log('元素曝光', res); // 执行埋点上报(示例使用神策SDK[^1]或自定义请求) // 方式1: 调用神策SDK(需先引入sa-sdk-miniprogram) sensors.track('ad_exposure', { ad_id: '12345' }); // 方式2: 自定义HTTP请求(如广点通事件上报[^3]) wx.request({ url: 'https://tracking-api.com/event', data: { event: 'exposure', element: 'ad-banner' } }); } }); } }); ``` - **关键参数解释**: - **`res.intersectionRatio`**:当前元素与视口的交叉比例(>0表示曝光中)。 - **`sensors.track()`**:引用[1]的神策SDK上报方法,支持全埋点代理技术。 - **相对定位**:`relativeToViewport()`确保观察基准为屏幕窗口,而非父容器。 --- ### 二、进阶优化与注意事项 #### 1. **性能优化** - **销毁观察器**:在页面`onUnload`中调用`observer.disconnect()`释放资源,避免内存泄漏。 - **动态组件处理**:对循环渲染的列表元素,使用`wx.createSelectorQuery()`获取节点后批量观察。 - **节流上报**:在回调内加延迟逻辑(如`setTimeout`),避免滚动时频繁上报影响性能[^4]。 #### 2. **非侵入式集成** - 引用[2]的全局事件监听方案可通过劫持`Page`生命周期自动注入观察器,无需修改业务代码: ```javascript // 在app.js中重写Page构造函数 const originalPage = Page; Page = function(config) { const onReady = config.onReady || function() {}; config.onReady = function() { // 自动为每个页面添加曝光观察器 setupExposureObserver.call(this); onReady.call(this); }; originalPage(config); }; ``` #### 3. **兼容性处理** - **平台判断**:使用条件编译区分小程序环境(如引用[3]的`#ifdef MP-WEIXIN`)。 - **降级方案**:低版本小程序若不支持`IntersectionObserver`,可改用`wx.createSelectorQuery()`计算元素位置。 --- ### 三、推荐工具与最佳实践 - **使用SDK简化**:集成引用[1]的**神策数据SDK**(`sa-sdk-miniprogram`),支持自动化代理埋点,减少手动编码[^1]。 - **组件化封装**:将观察器逻辑封装为自定义组件,如`<exposure-tracker selector="#target">`,提升复用性[^4]。 - **埋点验证**:开启小程序调试模式(`wx.setEnableDebug()`)或使用神策分析平台验证数据准确性。 > **最佳实践总结**:优先使用`wx.createIntersectionObserver`实现轻量级曝光检测,结合SDK上报(如神策/广点通[^3])确保数据可靠。对于复杂项目,采用引用[4]的组件化设计可维护性更高。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

chengbo_eva

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

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

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

打赏作者

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

抵扣说明:

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

余额充值