v-track
Version:
一个基于Vue指令的埋点插件
218 lines (202 loc) • 7.07 kB
JavaScript
/*
* @Author: 宋慧武
* @Date: 2019-03-06 17:49:29
* @Last Modified by: 宋慧武
* @Last Modified time: 2020-06-05 20:23:06
*/
import {
zipArray,
exactlySameVnode,
_exactMatch,
_partialMatch
} from "../utils/helper";
import * as debug from "../utils/debug";
import { isVisible } from "../utils/dom";
import VisMonitor from "../utils/vis-monitor";
const MODIFIERS = ["async", "delay", "watch", "show", "once", "custom"]; // 修饰符
/*******************************************************************************
* @desc 监听数据发生改变时触发埋点,需处理两种情况:
* ① 初始化时开始监听 v-track:xxxxx.watch="{ common_exp }"
* ops.immediate 表示初始化时立即开始监听
*
* ② 点击事件之后开始监听 v-track:18016.click.async="{ refreshHotSpot, exposureId }"
* el.contains(this.target) 避免多个“地方”同时监听同一个值出现多次上报的问题
*******************************************************************************/
function _watcher(el, exp, cbk, ctt, ops = {}) {
el.$unwatch = ctt.$watch(
() => ctt[exp],
(nv, ov) => {
nv !== ov && (ops.immediate || el.contains(this.target)) && cbk();
this.target = null; // 释放当前操作的watcher
}
);
}
/*************************************************************************
* @desc 自定义指令 v-track
*
* @param {*} el 指令所绑定的元素
* @param {String} arg 埋点对应event ID
* @param {Boolean} modifiers.click true: 事件行为埋点; false: 页面级埋点
* @param {Boolean} modifiers.watch 异步埋点
* @param {Boolean} modifiers.async 点击事件异步埋点
* @param {Boolean} modifiers.delay 埋点是否延迟执行,默认先执行埋点再执行cbk
*
* @property {Function} tck 对应埋点方法
*
* @example v-track:18015
* @example v-track:18015.watch
* @example v-track:18015.watch.delay
* @example v-track:18015.click
* @example v-track:18015.click.async
* @example v-track:18015.click.delay
* @example v-track:18015.[自定义事件名].delay
* @example v-track:18015.[自定义事件名].async
*************************************************************************/
export function bind(
el,
{ value, arg: id, modifiers, rawName },
{ context, componentInstance },
_,
__,
events
) {
if (!events[id]) throw new Error("tracking event does not exist");
let queue = [];
let tck = events[id].bind(null, context);
const watcher = (exp, cbk, ops) =>
_watcher.call(this, el, exp, cbk, context, ops);
const exactMatch = (...args) => _exactMatch.call(null, modifiers, args);
const partialMatch = (...args) => _partialMatch.call(null, modifiers, args);
if (!Object.keys(modifiers).length) {
events[id](context, value);
}
// 异步埋点
else if (exactMatch("watch")) {
const exp = Object.keys(value).shift();
watcher(exp, tck, {
immediate: true
});
}
// 指定延长时间埋点
else if (exactMatch("delay")) {
el.$timer && clearTimeout(el.$timer);
el.$timer = setTimeout(() => {
events[id](context);
}, value);
} else if (exactMatch("watch", "delay")) {
const { delay, ...args } = value;
const exp = [...Object.keys(args)].pop();
tck = () => {
el.$timer && clearTimeout(el.$timer);
el.$timer = setTimeout(() => {
const visible = isVisible(context.$el);
visible && events[id](context);
}, delay);
};
watcher(exp, tck, {
immediate: true
});
}
// 区域曝光埋点
else if (partialMatch("show")) {
const [args] = zipArray(value);
const tck = events[id].bind(null, context, ...args);
const once = partialMatch("once");
const custom = partialMatch("custom");
if (!el.$visMonitor) {
setTimeout(() => {
const vm = new VisMonitor(
el,
custom && context.$refs[value.ref],
value && context.$refs[value.viewport],
value && value.percent
);
(once ? vm.$once : vm.$on).call(vm, "fullyvisible", tck);
el.$visMonitor = vm;
}, 0);
}
} else if (
(!componentInstance && modifiers.click) ||
(componentInstance && partialMatch("native"))
) {
/**
* @desc DOM元素事件行为埋点(需区分是否带参数)
* @var {Function} fn 获取第一个参数作为回调函数
* @var {String} exp 获取最后一个参数并作为监听对象
*/
switch (typeof value) {
case "object": {
const [args, keys] = zipArray(value);
const fn = args.shift();
const exp = [...keys].pop();
debug.checkFun(fn);
tck = events[id].bind(null, context, ...args);
queue = [tck, fn.bind(null, ...args)];
modifiers.delay && queue.reverse();
modifiers.async && watcher(exp, queue.shift());
break;
}
case "function":
queue = [tck, value];
modifiers.delay && queue.reverse();
break;
}
el.$listener = e => {
this.target = e.target;
queue.forEach(sub => sub(e));
};
el.addEventListener("click", el.$listener);
} else if (
/**
* @desc 组件自定义事件行为埋点(需区分是否带参数)
* @var {Function} fn 获取第一个参数作为回调函数
* @var {String} exp 获取最后一个参数并作为监听对象
*/
componentInstance &&
componentInstance.$el === el
) {
let args, keys, fn, exp;
const eventName = Object.keys(modifiers)
.filter(key => !MODIFIERS.includes(key))
.pop();
if (typeof value === "object") {
[args, keys] = zipArray(value);
fn = args.shift();
exp = [...keys].pop();
debug.checkFun(fn);
}
if (el[`$on_${eventName}`]) return;
componentInstance.$on(eventName, (...data) => {
this.target = el;
tck = events[id].bind(null, context, ...data);
queue = [tck, (fn || value).bind(null, ...data)];
modifiers.delay && queue.reverse();
modifiers.async && watcher(exp, queue.shift());
queue.forEach(sub => sub());
el[`$on_${eventName}`] = true; // 避免重复监听
});
} else {
throw new Error(`${rawName} directive is not supported`);
}
}
/**
* @desc 由于 DOM 更新采用 diff 算法更新,如果新旧节点相同,则 el 会全等,导致 bind 绑定无法更
* 新,出现事件绑定诡异的问题,但由于 DOM update 执行频率很高,会导致性能问题,所以这里加
* 了一层exactlySameVnode过滤,即只有在新旧节点发生变化时才会重新绑定,否则相反
*
* @param {*} el 同bind
* @param {...any} args 同bind
*/
export function updated(el, ...args) {
if (!el.$listener) return;
if (!exactlySameVnode(args[1], args[2])) {
unbind.call(this, el);
bind.call(this, el, ...args);
}
}
export function unbind(el) {
el.$listener && el.removeEventListener("click", el.$listener);
el.$timer && clearTimeout(el.$timer);
el.$unwatch && el.$unwatch();
el.$visMonitor && el.$visMonitor.destroy();
}