美文网首页
Vue 项目声明式主动埋点

Vue 项目声明式主动埋点

作者: VioletJack | 来源:发表于2023-03-01 10:13 被阅读0次

    公司系统需求加上埋点功能,用来统计各页面功能的使用情况。于是,结合网上资料以及之前使用埋点系统的经历,仔细研究研究。

    调研

    埋点分类

    常见的埋点类型有三种

    • 代码埋点
      • 通过 JavaScript 代码主动将所需要的信息上报给服务器。
      • 优点:可以精确的上报所需的数据,对于少量埋点需求较为合适。
      • 缺点:代码遍布项目各处,不好维护管理。且埋点只能通过开发人员手动完成。
    • 可视化埋点
      • 需要另外一个可视化埋点圈选系统来圈选需要埋点的 DOM 元素。然后通过在系统中集成 SDK 来主动上报这些区域的埋点信息。其实算是另一种意义上的代码埋点。
      • 优点:有圈选系统可以让产品、运维同学自行决定埋点区域。
      • 缺点:适用范围有限,如内网系统、移动端 Hybrid 页面这些就很难用外部的可视化埋点来做。
    • 无埋点
      • 其实也叫全量埋点,即全局监听系统事件,把用户所有行为都进行上报。
      • 优点:行为数据记录全面,无需增加或维护埋点代码。
      • 缺点:上报数据量大,对服务器有一定压力。且无法精确上报某一功能的特停数据。

    埋点目标

    • 数据监控:通过埋点让产品运维同学知道项目当前的具体情况,从而有针对性的去优化项目。
    • 异常监控:从开发角度去收集项目中发生的 JS 报错、接口报错等异常情况。发现问题、解决问题、优化项目。
    • 性能监控:收集项目运行中的各种性能指标,如白屏时间、首屏加载时间、接口请求时间等等。

    埋点 SDK 实现猜想

    以我之前工作中用到过的埋点系统 GrowingIO 为例。我们可以通过它的 SDK 文档 来验证上面的理论。

    • 它通过全局引入 JS 代码的方式来进行集成,它会在 window 全局对象下加上一个 gio 函数处理各种埋点行为。
    • 由于埋点系统会为很多项目服务,所以需要初始化的时候加上 gio('init', 'your projectId', {})
    • 它要求在需要圈选的 DOM 元素上 data-growing-container 属性,这其实是 HTML 元素的 dataset 属性,可以用来对元素进行自定义数据属性的读写操作。有了圈选标记,埋点事件拦截的时候就可以指哪打哪了。
    • 它通过 gio('track', eventId, eventLevelVariables); 函数实现了主动埋点行为,这个自然是必不可少的。总有埋点需求是自动埋点做不了的。
    • 它的无埋点记录的是所有元素的点击量和浏览量,应该是全局监听了元素的点击和可视事件。
    • 它的可视化圈选是通过 XPath 来唯一定位一个元素的,那么可视化圈选其实就是将目标 DOM 的 xPath 保存起来,在埋点的时候去获取指定 DOM 元素的点击量和访问量。(关于 xpath 的使用可以看 Introduction to using XPath in JavaScript - XPath | MDN

    我的埋点

    方案选择

    由于项目的埋点只需要记录一些指定的行为,所以全埋点方案被我 PASS 了。同时也没有必要另外写一个页面去做埋点的圈选,最终,选择了最简单粗暴地主动埋点。

    主动埋点 1.0

    一开始埋点其实很简单,通过在 JavaScript 代码中写埋点代码来进行实现。

    定义一个埋点工具对象。

    // logger.js
    export default {
      ...,
      track(data) {
        const configInfo = this.getConfigInfo() // 一些公共配置信息,如用户名、token、时间、url 等
        return fetch.post('/api/v1/web/log', {
          ...data,
          ...configInfo,
        })
      },
    }
    

    将 logger 对象绑到 Vue 的原型中。

    // main.js
    Vue.prototype.$logger = logger
    

    在需要的地方主动埋点。

    <template>
      <div>
        <el-button @click="download">download</el-button>
      </div>
    </template>
    
    <script>
      export default {
        name: 'demo',
        methods: {
          download() {
            window.open('file url')
            this.trackLogger()
          },
          trackLogger() {
            this.$logger.track({
              component_id: '2',
              component_name: '下载按钮',
            })
          },
        },
      }
    </script>
    
    <style lang="scss" scoped></style>
    

    遇到的问题

    其实主动埋点应该就是如此,但随着埋点代码的逐渐增多(已经从起初的 20 条增加到 203 条了……)。看代码的时候就非常难受了。描述一个场景:

    • 需要检查同事代码中的埋点情况,由于不清楚他的代码,就需要一点点找了。
    • 全局搜索埋点代码 $logger.track(),得到 n 个包含有埋点代码的函数。
    • 再逐个跟踪这些包含埋点代码的函数的触发位置(有时候还会是函数嵌函数),最终找到绑定函数的 DOM 元素。
    • 如此才算是确定了一个元素拥有埋点行为。

    声明式 vs 命令式

    面对上面的场景,我在想有没有办法能够省去逐个查函数的步骤,让主动埋点代码更加直观呢。这里就得提到另外一个点了:声明式代码与命令式代码的区别了。

    • 声明式代码:如 HTML、XML、CSS,它的特点是可读性更强,描述的时候更符合直觉、更形象。
    • 命令式代码:如 JavaScript,它的特点是更符合行为步骤的思考模式,适合处理一些逻辑性强的功能。

    举几个栗子

    比如画一幅画,用声明式的方式来描述是“我要画一幅画,它有青草、大树和天空”;而用命令式的方式描述是"我要画一幅画,首先需要画青草,然后再画大树,最后加上蓝色的天空。"

    还有一个例子,在 vue 中有一个 createElement 函数,它可以在 vue 的 render 函数中命令式的创建 DOM 元素。

    createElement(
      'anchored-heading',
      {
        props: {
          level: 1,
        },
      },
      [createElement('span', 'Hello'), ' world!'],
    )
    

    但这种命令式的写法可读性很差。vue 官方也发现了这个问题,于是引进了 JSX 来弥补这个缺陷。

    import AnchoredHeading from './AnchoredHeading.vue'
    
    new Vue({
      el: '#demo',
      render: function (h) {
        return (
          <AnchoredHeading level={1}>
            <span>Hello</span> world!
          </AnchoredHeading>
        )
      },
    })
    

    JSX 的写法明显就更偏向于声明式。

    那么回过来复习下主动埋点的目的:通过代码主动上报指定 DOM 元素的行为事件。所以个人感觉用声明式写法会更好一些。

    主动埋点 2.0

    说干就干,我试着将命令式埋点改为声明式埋点。

    首先在入口文件 main.js 中引入全局注册逻辑。

    // 事件名称
    const COMPONENT_MAP = {
      1: '图表切换',
      2: '下载按钮',
    }
    
    // 修复点击子元素不上报埋点信息的问题
    function bindDataset(el, value) {
      el.dataset.loggerId = value
      // 递归绑定 dataset 到所有子集上
      el.children.forEach((child) => {
        bindDataset(child, value)
      })
    }
    
    // 全局注册指令,在需要埋点的 DOM 上加上 dataset
    Vue.directive('logger', {
      bind: function (el, binding) {
        const { value } = binding
        bindDataset(el, value)
      },
    })
    
    // 全局监听组件点击事件,加入防抖是为了避免短时间内快速重复点击
    document.addEventListener(
      'click',
      throttle((e) => {
        if (e.target.dataset.loggerId) {
          this.$logger.track({
            component_id: e.target.dataset.loggerId,
            component_name: COMPONENT_MAP[e.target.dataset.loggerId],
          })
        }
      }, 2000),
    )
    

    在上面代码中,我将埋点通过vue 指令的方式将埋点信息绑定到目标 DOM 的 dateset 上面。然后通过全局 click 事件拦截来获得目标元素的点击行为,并上报埋点信息。

    • 由于没有找到如何直接在 Vue 组件上直接操作 DOM 的方式(ref 不算,那个需要写很多的 ref='xxx' 很不划算),所以想到了 Vue 指令。
    • 在点击 DOM 元素的时候,如果元素中有子节点那么全局 click 事件只能捕获到子节点的事件,于是我偷懒将子节点都加上了 dataset。(组件的子元素不会太多,偷个懒了)

    以上遇到的两个问题个人感觉不是最佳方案,如果有好的解决方案欢迎讨论呀!

    使用方式如下,可读性上强了不少。

    <div class="filter-wrap" @click="setFilterPopupVisible(true)" v-logger="1">
      <img class="filter-icon" :src="filterIconUrl" />
      <img
        class="filter-icon-checked"
        :src="filterSelectedIconUrl"
        v-show="isFilterActive"
      />
    </div>
    

    如此,以后在看埋点代码的时候只要全局搜索 v-logger 就可以很方便的看到有哪些 DOM 元素或者 vue 组件是进行了埋点的了。不需要反复去查各种事件了。

    最后

    折腾了一圈,主要就是想解决看主动埋点代码太恶心的问题。然后顺便复习一些知识点。

    • 声明式编程和命令式编程
    • 埋点相关知识
    • dataset
    • xpath

    参考资料

    相关文章

      网友评论

          本文标题:Vue 项目声明式主动埋点

          本文链接:https://www.haomeiwen.com/subject/hozhldtx.html