Vue 可视化无痕埋点的探索

2019/08/01

代码地址: https://github.com/geminate/vue-visual-track

一. 简介

之前公司经常有一些埋点需求,比如监控某个按钮的点击时间与次数、输入框的聚焦次数等,当时采用的方案都是修改原项目代码添加对应的事件。再通过请求发到统计端。

但是这样处理存在的问题就是如果需要对埋点进行调整,必须要修改项目内部代码并发版,而且埋点技术性太强,只能由开发处理。

为了解决这个问题,参照网上已有的相关方案,我在 可视化无痕埋点的方案上做了一下探索。

最终形成的是一个 基于 Electron 的埋点程序,由于埋点需求的灵活性很强,因此只实现了最基本的功能。

二. 可视化埋点操作流程

1. 在可视化界面上打开项目地址,并圈选需要埋点的 DOM 元素

track-1.gif

2. 对选中的 DOM 元素添加自定义埋点事件

track-2.jpg

3. 在完成了多个 DOM 元素的事件添加后,根据保存的事件列表生成一个 JS 文件

track-3.jpg

4. 在原 VUE 项目中引入生成的 JS 文件,完成埋点

三. 项目编写过程中遇到的问题

1. 如何实现 可视化选择 DOM 元素

最初打算建立一个 web 项目,然后通过 webview 嵌入需要埋点的项目的地址,但由于 webview 权限较低,最终选择了桌面程序 Electron。

Electron 的 webview 组件有着比较高的权限,可以向 webview 内部打开的页面嵌入自己的JS代码,因此我们可以为打开的页面里的所有元素添加指向事件,在指向时改变其背景色,也就简单实现了 DOM 的可视化选择。这个与 chrome 控制台的 dom 元素框选十分类似。

利用 webview 的 executeJavaScript 方法可以向 webview 打开的页面中嵌入自己的 JS 字符串,这里使用了 raw-loader 将文件作为字符串引入

import insertJs from './insert.jsraw'

onDomReady () {
    this.$refs.webview.executeJavaScript(insertJs)
}
const {ipcRenderer} = require('electron')
let selectDom = null

// eslint-disable-next-line no-unused-vars
var selectPlugin = {

  startSelect: function () {
    document.body.style.cursor = 'crosshair'
    this.addEvent(document.body, 'mouseover', this.mouseOverHandler)
    this.addEvent(document.body, 'mouseout', this.mouseOutHandler)
  },

  stopSelect: function () {
    document.body.style.cursor = 'auto'
    this.removeEvent(document.body, 'mouseover', this.mouseOverHandler)
    this.removeEvent(document.body, 'mouseout', this.mouseOutHandler)
  },

  mouseOverHandler: function (e) {
    e.stopPropagation()
    e.target.style.backgroundColor = 'rgb(160,191,232)'
    selectDom = e.target
  },

  mouseOutHandler: function (e) {
    e.stopPropagation()
    e.target.style.backgroundColor = ''
  }
  
  ...
}

2. 如何获取选中元素的唯一选择器

在能够在目标页面上进行可视化选择之后,我们需要获取到 能够唯一标识这个元素的选择器,以便将来生成的 JS 文件中能够根据这个选择器找到我们圈选的元素。


  getUniqueSelect: function (node) {
    let path
    while (node) {
      let name = node.localName
      if (name) {
        name = name.toLowerCase()
        const parent = node.parentElement
        let reChildrenNode = []
        const childNodes = (parent && parent.childNodes) || []
        for (let i = 0; i < childNodes.length; i++) {
          if (childNodes[i].nodeName.toLowerCase() === name && !/\s/.test(childNodes.nodeValue)) {
            reChildrenNode.push(childNodes[i])
          }
        }
        if (reChildrenNode.length > 1) {
          const index = reChildrenNode.indexOf(node) + 1
          name += ':nth-of-type(' + index + ')'
        }
        path = name + (path ? '>' + path : '')
        node = parent
      }
    }
    return path
  }
  

上面的方法会返回类似 html>body>div>div>ul:nth-of-type(1)>li:nth-of-type(3)>a 这样的选择器字符串,选择器可以作为圈选元素的 唯一DOM标识。

3. 如何实现 VUE 的无痕埋点事件注入

想要实现 Vue 的无痕埋点,在对各个元素添加事件的时候 只能采取 事先注册好的命令式埋点。

在页面一打开的时候就将埋点事件注册到 document 元素上,之后用 e.target 和 事件的 选择器字符串进行匹配即可。

由于 选择器字符串的唯一性 只在同一个页面中有效,因此我们想要在 vue 项目中完全确定一个事件的所属元素的话,必须要有 页面地址和唯一选择器两样才可以。

新版本的 vue-router 使用 pushState 进行页面跳转,因此我们想要 无痕的监控 Vue 页面跳转需要对该方法进行修改,加入我们自己的回调函数。

const pushState = window.history.pushState
window.history.pushState = function (obj, name, hash) {
  if (typeof window.onPushstate === 'function') {
    window.onPushstate(hash)
  }
  return pushState.apply(window.history, arguments)
}

const popState = window.history.popState
window.history.popState = function (obj, name, hash) {
  if (typeof window.onpopstate === 'function') {
    window.onpopstate(hash)
  }
  return popState.apply(window.history, arguments)
}

const replaceState = window.history.replaceState
window.history.replaceState = function (obj, name, hash) {
  if (typeof window.onPushstate === 'function') {
    window.onPushstate(hash)
  }
  return replaceState.apply(window.history, arguments)
}

window.onPushstate = function () {
  // do something
}

window.onpopstate = function () {
   // do something
}

在页面的每次跳转中 监控页面的相关事件,并与 之前创建的 事件列表进行比对即可

文章导航