乐趣区

关于前端:搭建一个可视化用户行为轨迹打点体系SDK

一、基本原理

● 1、利用 xpath 的唯一性,绑定打点元素增加事件进行发送数据打点
● 2、后盾管理系统搭建一个可视化选取打点元素的性能并保留配置
● 3、前端依据页面 URL 获取到打点配置进行初始化(通过 xpath 绑定事件)
根本流程如图所示:

二、前端发送打点数据形式

前端有几种计划进行发送打点数据

1、传统 ajax 申请

利用传统的 ajax 申请进行发送数据,毛病是容易阻塞申请,对用户不敌对
而且弊病很大,用户敞开页面时会截断申请,也就是发送会终止掉,用于记录浏览时长不实用
axios.post(url, data); // 以 axios 为例

2、动静图片

咱们能够通过在 beforeunload 事件处理器中创立一个图片元素并设置它的 src 属性的办法来提早卸载以保证数据的发送,因为绝大多数浏览器会提早卸载以保障图片的载入,所以数据能够在卸载事件中发送。

const sendLog = (url, data) => {let img = document.createElement('img');
  const params = [];
  Object.keys(data).forEach((key) => {params.push(`${key}=${encodeURIComponent(data[key])}`);
  });
  img.onload = () => img = null;
  img.src = `${url}?${params.join('&')}`;
};

3. sendBeacon

为了解决上述问题,便有了 navigator.sendBeacon 办法,应用该办法发送申请,能够保证数据无效送达,且不会阻塞页面的卸载或加载,并且编码比起上述办法更加简略。

export const sendBeacon = (url, analyticsData) => {
    const apiUrl = config.apiRoot + url
    let data = getParams(analyticsData)
    // 兼容不反对 sendBeacon 的浏览器
    if (!navigator.sendBeacon) {const client = new XMLHttpRequest()
        // 第三个参数示意同步发送
        client.open('POST', apiUrl, false)
        client.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
        client.send(data)
        return
    }
    const formData = new FormData()
    Object.keys(analyticsData).forEach((key) => {let value = analyticsData[key]
        if (typeof value !== 'string') {
            // formData 只能 append string 或 Blob
            value = JSON.stringify(value)
        }
        formData.append(key, value)
    })
    navigator.sendBeacon(apiUrl, formData)
}

最初咱们应用了动静图片的形式,因为阿里云提供的阿里云 - 采集 - 通过 WebTracking 采集日志应答大量数据采集不造成网站自身服务器压力

三、搭建 SDK

利用 webpack 搭建我的项目,打包出单个 sdk 的 js 文件包,前端引入 sdk 即可(此局部不做赘述了,感兴趣能够搜寻 webpack 相干材料)用 webpack 写一个简略的 JS SDK
● 可视化选取 xpath- 参考插件
SDK 的次要性能:

  1. 暴露出初始化办法,以及打点的办法(为了反对手动打点)
  2. 增加选取 xpath 性能,并裸露给后盾管理系统应用
  3. 依据链接 URL 读取到打点配置列表
  4. 初始化绑定打点事件性能
  5. 进入页面记一次打点
  6. 记录浏览时长
  7. SDK 与父级 iframe 通信性能(为了传递数据给后盾管理系统)
    记录旅行时长示例:
// 统计时长
const viewTime = (data) => {let startTime = new Date().getTime() // 浏览开始工夫
  let endTime = null // 浏览完结工夫
  // 页面卸载触发
  window.addEventListener('unload', () => {endTime = new Date().getTime()
    let params = {viewTime: (endTime - startTime) / 1000,
      eventType: 'view',
      accessId: ACCESS_ID
    }
    params = Object.assign(params, data)
    sendLog(params)
  }, false)
}
// 选取 xpath 跨域跨页面通信
import Postmate from 'postmate'
import Inspector from '../plugins/inspect' // 选取 xpath 节点插件

let childIframe = null
const myInspect = new Inspector()
const getXpathForm = function (options) {myInspect.setOptions(options, (data) => {
        let params = {
            xpath: data,
            route: window.QWK_ANALYSIS_SDK_OPTIONS?.route || ''
        }
        childIframe.emit('send-data-event', params)
    })
}
export default {
    // 和父级 iframe 通信
    initMessage () {
        // 开发模式下启用选节点调试
        if (process.env.BUILD_ENV === 'dev') {document.querySelector('#selected').onclick = () => {
                myInspect.setOptions({deactivate: true}, (data) => {console.log(data)
                })
            }
        }
        const handshake = new Postmate.Model({
            // iframe 父级获取 xpath
            getXpath: (options) => {getXpathForm(options)
            },
            // 移除选取
            deactivate: () => {myInspect.deactivate()
            }
        })
        // When parent <-> child handshake is complete, events may be emitted to the parent
        handshake.then((child) => {childIframe = child})
    }
}
// 导出 SDK
// main.js 入口文件
import {init} from './lib/init'
import action from './lib/action'
import selectXpath from './lib/select-xpath'
// import {documentReady} from './plugins/common'

// 初始化选取 xpath 的跨域通信
selectXpath.initMessage()

// documentReady(() => {
//     // 初始化
//     // init().then(res => res)
// })

// 导出 SDK 模块
export {
    init,
    action
}

四、后盾管理系统搭建可视化选取 xpath

第一步:第三方网站引入 SDK

在 sdk 中写一个选取 xpath 性能并裸露进去给后盾管理系统调用
● 可视化选取 xpath- 参考插件

第二步:搭建管理系统

搭建一个加载网站的 iframe,如图所示:

咱们须要在这里调用 SDK 中的选取网站 xpath 性能的办法,这就必须和加载的 iframe 中的网站通信
因为是 iframe 加载的第三方网站,会有跨域问题,所以咱们须要一个插件来实现这一性能 postmate
GitHub 链接

<template>
    <div class="iframe-box" ref="content">
    </div>
</template>

<script>
    import Postmate from 'postmate'

    export default {
        name: 'WebIframe',
        props: {
            src: {
                type: String,
                default: ''
            },
            options: {
                type: Object,
                default: () => ({})
            }
        },
        data () {
            return {$child: null}
        },
        mounted () {this.$nextTick(() => this.initMessage())
        },
        methods: {initMessage () {
                let handshake = new Postmate({
                    container: this.$refs.content,
                    url: this.src,
                    name: 'web-iframe-name',
                    classListArray: ['iframe-class-content'],
                })
                handshake.then((child) => {
                    this.$child = child
                    child.on('send-data-event', (data) => {this.$emit('selected', data)
                    })
                })
            },
            // 获取选取 xpath
            getXpath () {
                let options = {
                    clipboard: false, // 是否主动复制
                    deactivate: true, // 抉择之后销毁
                    ...this.options
                }
                try {this.$child.call('getXpath', options)
                } catch (e) {this.$errMsg('加载 SDK 失败,请从新加载网站试试~ 也可能以后网站未引入用户行为轨迹跟踪 SDK,请分割相干人员增加')
                }
            },
            // 移除选取弹层
            remove () {this.$child && this.$child.call('deactivate')
            }
        }
    }
</script

选取节点后果,成果如图:

五、遇到的问题以及解决方案

1、后盾管理系统 iframe 加载第三方网站通信问题

这里因为是通过 iframe 来加载第三方网站进行可视化打点的,所以须要父级 iframe 和第三方网站进行通信,然而会有跨域问题,跨域问题解决方案有很多种,这里应用基于 postmessage 的第三方插件 postmate 来解决

2、动静路由问题

遇到比方文章详情的页面,因为文章详情会有很多的链接 https://www.baidu.com/article… 像这样的,前面的 detail/id 跟随着很多 id,这样的页面不可能每篇文章都去配置一下的,这样就须要做动静路由配置对立的动静参数用其余字符标识去加载配置。
计划:
①、在配置中增加动静路由标识,通过后端去读取数据库进行匹配动静路由(须要后端去做大量匹配)
②、纯前端操作,前端 sdk 和后盾管理系统相互传递动静路由
通过探讨,咱们选用了第二种,动静路由通过前端初始化 sdk 的时候去传入,这时候 sdk 中接管到传进来的动静路由,就依据这个路由去加载配置,SDK 传递给后盾可视化选取 xpath 配置,也要通过这个路由去保留配置
这时候两边就能够一一对应上了。咱们定义了这样的路由 https://www.baidu.com/article…{id},其中 {id} 为动静参数

3、动静节点绑定不到事件问题

因为动静节点是在加载页面时 dom 没有生成,这时候就初始化去绑定事件是查找不到 dom 节点的,因而该节点的打点就是生效的。为了解决这个问题,咱们能够通过全局点击事件去查找这个节点,利用 document 的点击去查找这个动静节点,而后失去以后点击的 target 比照 xpath 查找到的节点相等,阐明以后点击的节点就是 xpath 须要绑定事件的节点,此时发送对应的数据即可

let {data} = await getConfig(route)
let eventList = data.filter((m) => m.eventType !== 'visible')
let viewList = data.filter((m) => m.eventType === 'visible')
let dynamicList = [] // 动静生成的节点
// 点击事件或者其余事件
eventList.forEach((item) => {const node = item.xpath && document.evaluate(item.xpath, document).iterateNext()
  if (!node) {
    // 找不到节点,阐明有可能是动静生成的节点
    dynamicList.push(item)
    return
  }
  node.addEventListener(item.eventType || 'click', () => {action.track(item)
  })
})
// 通过 document 的点击开查找动静生成的节点
let dynamicClickList = dynamicList.filter((m) => m.eventType === 'click')
if (dynamicClickList && dynamicClickList.length) {document.onclick = (event) => {
    const target = event.target || window.event.target
    const parentNode = target.parentNode
    for (let item of dynamicClickList) {
      // 先把查找到的节点给存下来
      item.node = document.evaluate(item.xpath, document).iterateNext() || item.node}
    let xpathItem = dynamicClickList.find((m) => {return m.xpath && (target === m.node || parentNode === m.node)
    })
    // 查找到节点,发送打点
    xpathItem && delete xpathItem.node && action.track(xpathItem)
  }
}

4、指标进入可视区域

应用场景:有些横向滚动切换的元素,须要指标进入到用户可见的区域时进行打点,于是就有这样的需要
IntersectionObserver 参考文档链接

let observer = null // 可视区域
let isTrackList = [] // 曾经打点过的
if ('IntersectionObserver' in window) {
  // 创立一个监听节点可视区域
  observer = new IntersectionObserver(entries => {const image = entries[0]
    // 进入可视区域
    if (image.isIntersecting) {
      // 以后可视区域的打点配置
      let current = viewList.find((m) => m.xpath && image.target === document.evaluate(m.xpath, document).iterateNext())
      // 曾经打点过的
      let trackEd = isTrackList.find((m) => m.id === current.id)
      // 曾经打过的点不再打
      if (current && !trackEd) {isTrackList.push(current)
        action.track(current)
      }
    }
  })
}
viewList.forEach((item) => {const node = item.xpath && document.evaluate(item.xpath, document).iterateNext()
  if (!node) {return}
  // 监听节点
  observer.observe(node)
})
退出移动版