代码地址: https://github.com/geminate/vue-visual-track
一. 简介
之前公司经常有一些埋点需求,比如监控某个按钮的点击时间与次数、输入框的聚焦次数等,当时采用的方案都是修改原项目代码添加对应的事件。再通过请求发到统计端。
但是这样处理存在的问题就是如果需要对埋点进行调整,必须要修改项目内部代码并发版,而且埋点技术性太强,只能由开发处理。
为了解决这个问题,参照网上已有的相关方案,我在 可视化无痕埋点的方案上做了一下探索。
最终形成的是一个 基于 Electron 的埋点程序,由于埋点需求的灵活性很强,因此只实现了最基本的功能。
二. 可视化埋点操作流程
1. 在可视化界面上打开项目地址,并圈选需要埋点的 DOM 元素
2. 对选中的 DOM 元素添加自定义埋点事件
3. 在完成了多个 DOM 元素的事件添加后,根据保存的事件列表生成一个 JS 文件
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
}
在页面的每次跳转中 监控页面的相关事件,并与 之前创建的 事件列表进行比对即可