# 基于vue directive实现声明式埋点方案

注:本方案依赖vue (opens new window)lazysizes (opens new window)(曝光事件:lazybeforeunveil)

# 传统埋点 vs 声明式埋点

正文开始前,对比展示一下效果,方便读者判断是否有兴趣 :)

传统埋点

<template>
  <div @click="handleMoreClick(0)">More</div>
</template>

<script>
export default {
  methods: {
    handleMoreClick(type) {
      this.onAnalyticsEvent('查看更多', '签到活动card【查看更多】_click', 'b_4ezhmbjt', { type })
    },
    onAnalyticsEvent(element_id, title, bid, lab) {
      AnalyticsUtil.sendAnalyticsEvent({
        val: {
          element_id,
          title,
          bid,
          lab,
        }
      })
    },
  }
}
</script>

声明式埋点

<template>
  <div v-mge:b_4ezhmbjt="0">More</div>
</template>
<script>
/* 无需写其他代码 */
</script>

神奇的v-mge如何实现,埋点信息如何获取? 请看下文分解~

# 背景

  1. 前端页面埋点需求很多,基本采用传统的命令式埋点,随着项目业务需求开发持续进行,出现越来越多的冗余代码,虽做了部分公共封装,但与业务逻辑无关的埋点代码还是不可逆地累积,且分散在源码各处。
  2. 埋点过程有较多重复性劳动,低效率且容易出错。
    团队埋点流程:产品配置埋点->开发从平台逐个copy埋点信息->粘贴到源码->运行时发送埋点。

# 调研

  1. 接收到优化埋点的任务时开始调研,准备了两套方案:a: 可视化无痕埋点;b: 声明式埋点。

  2. 方案一最终流产。不过多介绍,原因总结如下:

    • 重新制定了埋点流程,需多方配合
    • 开发工作量大且复杂
      虽可提供动态埋点功能,但产品对现有埋点流程无痛感,无动力使用新方案。
      所以,为解决自己的问题,期望别人做太大改动是不切实际的。
  3. 声明式埋点依赖vue框架,可以提升效率,仍需要维护埋点信息(按页面统一维护),但实现起来足够简单,且对流程无影响。

# 实现

资源依赖如图:

  1. 通过Chrome插件(推荐chrome插件,可快速写写浏览器tampermonkey (opens new window))生成mge data(这部分是团队定制的,不作介绍)。
// 生成的数据样例
export default {
  b_4ezhmbjt: type => ({
    elementName: '查看更多',
    eventName: '签到活动card【查看更多】_click',
    eventType: 'click',
    custom: { type },
  }),
  // ...
}
  1. 在页面(page.vue)导入埋点数据、注册指令。
  2. 在根节点订阅(mgeSubscriber)指令发布的消息,所有埋点事件触发后都将埋点数据传递给该订阅者。

数据流向:

  1. 指令(v-mge)初始化时给dom节点绑定事件。
  2. 事件触发时根据埋点id获取对应的埋点信息,加上业务参数合成完整的埋点数据。
  3. 完整的埋点数据传递给Vue实例根节点提供(Provide)的mgeSubscriber
  4. mgeSubscriber接收到数据后上报到埋点服务器。

# 收益

  • 一个埋点大约能节约1~2分钟
  • 一个埋点大约5行代码,精简到可以忽略
  • 埋点数据从业务代码中剥离出来(单独文件管理,import到页面)
  • 覆盖90%以上的场景(UI组件内事件需要在handler中上报,不能通过指令埋点)

# 详细API

<template lang="html">
  <!-- 携带一个业务字段事件 -->
  <div v-dr-mge:b_5ix7ve3c="title">
    {{ a }}
    <!-- 携带多个业务字段 -->
    <div v-dr-mge:b_7sslet2v="[title, shopId]">
      {{ b }}
    </div>
    <!-- 不携带业务参数事件 -->
    <div v-dr-mge:b_vyc33sw0></div>
    <div v-for="(v, index) in arr">
      <!-- 曝光+click事件 -->
      <p v-dr-mge:b_1rlrj8dr|b_6zn0e86f="[v, index]">{{v}}</p>
    </div>
  </div>
</template>

# 可参考的指令源码

import 'lazysizes';

// 每个埋点传递进来的值,经过MGE_DATA转换后的结果,当事件触发时将结果发送给provide
const CACHE_DATA = {};

function handleMge(el, bidKey, e) {
  // click使用el,曝光使用e.target
  const uniqBid = (el || e.target).dataset[bidKey];
  const mgeInfo = CACHE_DATA[uniqBid];
  if (!mgeInfo) return;

  const { data, subscriber } = mgeInfo;
  data.bid = uniqBid.replace(/-\d+$/, '');
  subscriber(data);
}

// 绑定lazysizes提供的lazybeforeunveil事件
document.addEventListener('lazybeforeunveil', handleMge.bind(null, null, 'viewMgeBid'));

// 判断传递给指令的新值、旧值是否相等
function directiveValueEquals(v1, v2) {
  if (v1 === v2) return true;
  if (typeof v1 !== typeof v2) return false;
  return v1.toString() === v2.toString();
}

// 生成唯一值,解决一个bid被注册多次的场景。如在v-for生成的元素上使用dr-mge
const uniq = (() => {
  let id = 0;
  return bid => {
    id += 1;
    return `${bid}-${id}`;
  };
})();

/**
 * 创建mge指令
 * @param  {object} mgeData 从ocean获取的埋点数据
 * @return {object}         vue 指令
 */
export default function createMgeDirective(mgeData) {
  if (mgeData == null || typeof mgeData !== 'object') return;
  // eslint-disable-next-line
  return {
    inserted(el, binding, vnode) {
      const { arg, value } = binding;
      const { $mgeSubscriber } = vnode.context.$root._provided;
      // 如果根节点未提供处理mge事件的handle,则忽略该指令
      if (!$mgeSubscriber) {
        console.error('root节点provide没有$mgeSubscriber');
        return;
      }

      arg.split('|').forEach(bid => {
        const mgeInfo = mgeData[bid];
        if (!mgeInfo) {
          console.error('未注册埋点信息的bid:', bid);
          return;
        }

        const uniqBid = uniq(bid);
        const eventData = typeof mgeInfo === 'function' ? mgeInfo(...[].concat(value)) : mgeInfo;

        el.setAttribute(`data-${eventData.eventType}-mge-bid`, uniqBid);
        CACHE_DATA[uniqBid] = {
          data: eventData,
          subscriber: $mgeSubscriber, // 当mge事件触发时将data传递给handle
        };

        if (eventData.eventType === 'click') {
          el.addEventListener('click', handleMge.bind(null, el, 'clickMgeBid'));
        } else if (
          eventData.eventType === 'view'
          && !el.classList.contains('lazyload')
        ) {
          el.classList.add('lazyload');
        }
      });
    },
    componentUpdated(el, binding) {
      const { arg, value, oldValue } = binding;

      arg.split('|').forEach(bid => {
        // 从dataset中查找 uniqBid
        const uniqBid = Object.keys(el.dataset)
          .map(key => el.dataset[key])
          .find(v => new RegExp(`${bid}-\\d+$`).test(v));
        if (!uniqBid) return;

        const mgeInfo = mgeData[bid];
        const cacheData = CACHE_DATA[uniqBid];

        if (mgeInfo && cacheData) {
          // 更新缓存值
          cacheData.data = typeof mgeInfo === 'function' ? mgeInfo(...[].concat(value)) : mgeInfo;

          // 曝光事件,当传递的参数改变时需要重新曝光,重置class
          if (cacheData.data.eventType === 'view' && !directiveValueEquals(value, oldValue)) {
            el.classList.remove('lazyloaded');
            el.classList.add('lazyload');
          }
        } else {
          console.error('指令更新异常,未找到该埋点信息或缓存数据', bid);
        }
      });
    },
    unbind(el, binding) {
      binding.arg.split('|').forEach(bid => {
        delete CACHE_DATA[bid];
      });
    },
  };
}
💗 博主正处于裸辞待业状态,欢迎 商务合作 💗

相关文章

从 React 看前端 UI 代码范式革命

alt text 前言 本来打算写的主题是“我为什么讨厌 React Hooks API”,展开聊聊“小甜甜”是如何变成“牛夫人”的,没想到越写越严肃:) React 是两次前端范式革命的引领者,至今仍有繁荣的社区和旺盛的创造力; React 多次天才又激进的创新,一些想法被借鉴改良、一些引发广泛质疑,大部分是被认同和接受的; ...

WebCodecs 性能表现及优化思路

笔者开源 WebAV 已经一年半,还写了系列文章帮助初学者入门 Web 音视频。 之前一直隐隐担心在 Web 平台处理音视频与 Native APP 会有明显性能差距,因为 WebCodecs API 毕竟被浏览器代理了一层,且一些数据处理需要 js 配合,不确定有多大的性能损耗。 相信刚接触 WebCodecs 的读者也非常关心它的性能表现如何。 ...

Web 网页自集成 HTTP 代理方案

前言 大部分程序员,想必都有会一个常用的抓包代理工具; 但在座的各位,可曾见过这样一款集成在 Web 应用中的代理工具? 它是明显区别于传统代理工具的,有以下特性: 零安装,零配置,Web 点击即用、APP 扫码即用;_(不 ...

Web 文件系统(OPFS 及工具)介绍

文件系统是往往是构建大型软件的基石之一,很长一段时间 Web 平台因缺失成熟的文件系统成为构建大型软件的阻碍,如今 OPFS 可弥补这一缺憾。 本文介绍 OPFS 背景和基本使用方法、使用过程中的注意事项,及如何配合笔者开源的 opfs-tools、opfs-tools-explorer 两个项目,充分发挥 OPFS 的性能与开发效率。 Web 存储 A ...

opfs-tools (文件系统 API) 项目介绍

文件系统是许多领域程序的基石,所有通用编程语言都会内置完备的文件系统 API。 Web 很长一段时间没有提供完善的访问文件系统的规范,使得需要高频读写文件、大文件处理软件在 Web 端都会受到一些限制,比如音视频剪辑、游戏、数据库等等。 之前在浏览器中实现视频裁剪、截帧等相关功能时,发现缺少基本的操作文件的 API,比如读写、移动、复制文件。 而 [OPFS] ...

Web 终极拦截技巧(全是骚操作)

拦截的价值 > 计算机科学领域的任何问题都可以通过增加一个中间层来解决。 —— Butler Lampson 如果系统的控制权、代码完全被掌控,很容易添加中间层; 现实情况我们往往无法控制系统的所有环节,所以需要使用一些 “非常规”(拦截) 手段来增加中间层。 拦截的方法 拦截/覆写 浏览器 API 最常见的场景有通过拦截 console ...

Web 音视频(六)图像素材处理

Web 音视频目录 前序章节介绍了如何在浏览器中解析、创建视频,以及给视频添加一些自定义素材(图片、音频、文字...); 本章介绍如何给图像素材加特效、加动画,实现转场、移动水印、图像滤镜美化等功能。 你可以跳过原理介绍,直接查看 WebAV 示例 素材动画 在视频制作中实现动画跟其他场景略有不同,因为视频 ...