vue埋点指令实现

759 阅读5分钟

前言

    有需求的地方就有统计,有统计的地方就需要埋点,跟有人的地方就有江湖一样......

        产品的埋点文档,发现点击埋点的场景比较多。因为使用的是阿里云sls日志服务去埋点,所以通过使用手动侵入代码式的埋点。定好埋点的形式后,技术实现方法也有很多,哪种比较好呢?

稍加思考...

      决定封装个埋点指令,这样使用起来会比较方便,因为指令的颗粒度比较细能够直击要害,挺适合上面所说的业务场景。

基础知识

钩子函数

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。

  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

  • unbind:只调用一次,指令与元素解绑时调用。

接下来我们来看一下钩子函数的参数 (即 elbindingvnodeoldVnode)。

钩子函数参数

指令钩子函数会被传入以下参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM。
  • binding:一个对象,包含以下 property:
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
  • vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。
  • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用。

除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。

这是一个使用了这些 property 的自定义钩子样例:

<div id="hook-arguments-example" v-demo:foo.a.b="message"></div>

Vue.directive('demo', {
    bind: function (el, binding, vnode) {
        var s = JSON.stringify
        el.innerHTML =
            'name: ' + s(binding.name) + '<br>' +
            'value: ' + s(binding.value) + '<br>' +
            'expression: ' + s(binding.expression) + '<br>' +
            'argument: ' + s(binding.arg) + '<br>' +
            'modifiers: ' + s(binding.modifiers) + '<br>' +
            'vnode keys: ' + Object.keys(vnode).join(', ')
    }
})

new Vue({
    el: '#hook-arguments-example',
    data: {
        message: 'hello!'
    }
})

埋点指令

v-click点击埋点

用户点击事件时上报埋点,并且支持自定义属性等信息

import Vue from 'vue'
// 统计实例
import census from "./census"

/**
 * @description 处理json字符转换为上报属性
 * @param {string} ele自定义埋点属性
 * @return {Object} 转换处理后的属性对象
 */
function jsonAttrsToObj(jsonAttrs: string) {
    let params = {}
    try {
        if (params) {
            params = JSON.parse(jsonAttrs)
        }
    } catch (error) {
        params = {}
    }
    return params
}

// 自定义点击埋点指令
export default (Vue: any) => {
    Vue.directive('track', {
        inserted: (el: Element, binding: any) => {
            const {
                arg,
                value
            } = binding
            // 点击埋点
            if (arg === 'click') {
                el.addEventListener('click', () => {
                    let params = jsonAttrsToObj(el.getAttribute('data-track'))
                    census.trackEvent(value, params || {})
                })
            }
        }
    })
}

v-exposure曝光埋点

展现埋点不区分是否在可视区只要组件容器渲染就上报的埋点,支持附近属性上报

import Vue from 'vue'
// 统计实例
import census from "./census"

/**
 * @description 处理json字符转换为上报属性
 * @param {string} ele自定义埋点属性
 * @return {Object} 转换处理后的属性对象
 */
function jsonAttrsToObj(jsonAttrs: string) {
    let params = {}
    try {
        if (params) {
            params = JSON.parse(jsonAttrs)
        }
    } catch (error) {
        params = {}
    }
    return params
}

// 自定义点击埋点指令
export default (Vue: any) => {
    Vue.directive('track', {
        inserted: (el: Element, binding: any) => {
            const {
                arg,
                value
            } = binding
            // 点击埋点
            if (arg === 'click') {
                el.addEventListener('click', () => {
                    let params = jsonAttrsToObj(el.getAttribute('data-track'))
                    census.trackEvent(value, params || {})
                })
                // 展示埋点
                //展现埋点
            } else if (arg === 'exposure') {
                let params = jsonAttrsToObj(el.getAttribute('data-track'))
                census.trackEvent(value, params || {})
            }
        }
    })

}

v-appear滚动到可视区展现埋点

v-appear是元素只有在可视区内才会上报的埋点,主要为基于 IntersectionObserver实现,为了兼容性,需要引入 intersection-observer

intersection-observer 安装

yarn add intersection-observer

代码实现

import Vue from 'vue'
require('intersection-observer')

// 统计实例
import census from "./census"

/**
 * @description 处理json字符转换为上报属性
 * @param {string} ele自定义埋点属性
 * @return {Object} 转换处理后的属性对象
 */
function jsonAttrsToObj(jsonAttrs: string) {
    let params = {}
    try {
        if (params) {
            params = JSON.parse(jsonAttrs)
        }
    } catch (error) {
        params = {}
    }
    return params
}

// 自定义点击埋点指令
export default (Vue: any) => {
    Vue.directive('track', {
        inserted: (el: Element, binding: any) => {
            const {
                arg,
                value
            } = binding
            // 点击埋点
            if (arg === 'click') {
                el.addEventListener('click', () => {
                    let params = jsonAttrsToObj(el.getAttribute('data-track'))
                    census.trackEvent(value, params || {})
                })
                // 展示埋点
            } else if (arg === 'exposure') {
                let params = jsonAttrsToObj(el.getAttribute('data-track'))
                census.trackEvent(value, params || {})
            }
        }
    })

    // 自定义曝光指令: https://www.baidu.com/link?url=PRt_n4u-5bMJKml_BNsClKASYmnZmnmNlbvGjcDhsBvAoWqZZQIWrx_GJ0CGXEcjw3v6nhoZPLgR2PluQJotyTrCmJH_eqNkvLpLDX6z7RW&wd=&eqid=a0bc371d00060d1000000002619ca56f
    const options: any = {
        root: null, //默认浏览器视窗
        threshold: 0.3 //元素完全出现在浏览器视窗内才执行callback函数。
    }
    // let appearList = [] //记录已经上报过的埋点信息
    const callback = (entries: any, observer: any) => {
        entries.forEach((entry: any) => {
            let eventAttrs = jsonAttrsToObj(entry.target.getAttribute('data-appear'))
            // 返回一个布尔值, 如果根与目标元素相交(即从不可视状态变为可视状态),则返回 true。如果返回 false,变换是从可视状态到不可视状态。
            if (entry.isIntersecting) {
                const {
                    __eventName = 'fin_default_appear'
                } = entry.target
                census.trackEvent(__eventName, eventAttrs)
                // 上报后清除监听
                // appearList.push(__eventName)
                observer.unobserve(entry.target)
            }
        })
    }
    const observer = new IntersectionObserver(callback, options)
    const addListenner = (ele: Element, binding: {
        value: any
    }) => {
        // if (appearList.indexOf(binding.value) !== -1) return
        // 缓存曝光事件的名称
        (ele as any).__eventName = binding.value
        observer.observe(ele)
    }
    const removeListener = (ele: any) => {
        observer.unobserve(ele)
    }
    //自定义曝光指令
    Vue.directive('appear', {
        bind: addListenner,
        unbind: removeListener
    })

}

使用

v-click使用

//引入
import {
    vtrack
} from 'track.ts'

// 增加通用指令
Vue.use(vtrack)

模板中使用

<div
   :src ="url"
   v-click="`xxx_click_event`"
   :data-track="`${JSON.stringify({
    ...trackAttr})}`">
</div>

v-exposure使用

模板中使用

<div
   :src ="url"
   v-exposure="`xxx_click_event`"
   :data-track= "`${JSON.stringify({    ...trackAttr})}`">
</div>

v-appear使用

模板中使用

<div
   :src ="url"
   v-appear="`xxx_click_event`"
   :data-exposure = "`${JSON.stringify({    ...trackAttr})}`">
</div>