一、基本原理

● 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 = nullconst 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)})