import { uuidV4 } from '@tencent/merlin-core'; import { BehaviorCatcherConfig, ExposeHTMLElement, DomDatasetBoolean, ReporterContext } from '../types'; import { ExposeRoot } from './ExposeRoot'; import { BehaviorCatcher } from './common'; import { BehaviorCatcherMap } from '../BehaviorCatcherMap'; interface ExposeTargetConfig extends BehaviorCatcherConfig { /** 配置中指定的 exposeRootName */ rootName?: string; /** 只曝光一次 */ once: boolean; /** 是否上报结束曝光 */ conceal: boolean; context?: ReporterContext; } export class ExposeTarget extends BehaviorCatcher { /** 配置中指定的 exposeRootName */ rootName?: string; private exposeId: string; /** 当前绑定的 exposeRoot */ private exposeRoot?: ExposeRoot; private context?: ReporterContext; private isExposing = false; /** 上一次触发 expose 的时间戳 */ private lastExposeTime = 0; /** 当前元素首次曝光的 Id */ private firstExposeId = ''; /** 是否上报结束曝光 */ private conceal: boolean; /** 是否只曝光一次 */ private once: boolean; constructor(config: ExposeTargetConfig) { super(config); this.context = config.context; this.rootName = config.rootName; this.conceal = config.conceal; this.once = config.once; const newExposeId = uuidV4(); this.exposeId = newExposeId; this.firstExposeId = newExposeId; if (config.rootName && config.rootName.length > 0 && this.el.dataset.mlRootName !== config.rootName) { this.el.dataset.mlRootName = config.rootName; // 保存在 DOM 上 } } /** 是否已经绑定了 exposeRoot */ get hasExposeRoot() { return !!this.exposeRoot; } /** 曝光根节点元素 */ get exposeRootEl() { return this.exposeRoot?.el; } /** 根据配置找到对应 ExposeRoot,并启动 observe */ bindExposeRoot(exposeRootMap: BehaviorCatcherMap) { const exposeRoot = !this.rootName ? exposeRootMap.get(this.getCurrentExposeRootElement() ?? document.body) : exposeRootMap.find((root) => root.name === this.rootName); if (!exposeRoot) { // 指定了目前不存在的 exposeRoot,可能是后续再加入的 exposeRoot,待加入时由 exposeRoot 主动找到该 exposeTarget 并绑定 return; } if (this.exposeRoot && this.exposeRoot.el === exposeRoot.el) { // 重复绑定,不做处理 return; } this.exposeRoot = exposeRoot; exposeRoot.observe(this); } /** 双向解绑 */ unbindExposeRoot() { this.exposeRoot?.unObserver(this); this.exposeRoot = undefined; } updateConfig(config: ExposeTargetConfig) { this.rootName = config.rootName; if (config.rootName && config.rootName.length > 0 && this.el.dataset.mlRootName !== config.rootName) { this.el.dataset.mlRootName = config.rootName; // 保存在dom上 } } /** 发生曝光时更新内部状态并上报曝光 */ handleExpose() { if (this.once && this.exposeId !== this.firstExposeId) return; // 已经曝光过且只需要曝光一次 this.isExposing = true; this.lastExposeTime = Date.now(); this.context?.reporter?.reportElementExpose({ key: this.key, extInfo: this.extInfo, exposeId: this.exposeId, }); } /** 结束曝光时清算并上报曝光结束 */ settleExpose() { if (this.once && this.exposeId !== this.firstExposeId) return; // 已经曝光过且只需要曝光一次 if (this.lastExposeTime === 0 || !this.isExposing) { // 没有触发 expose 就发生了结束 // this.context?.reporter?.log('没有触发 expose 就发生了结束', this.el); return; } const exposeDuration = Date.now() - this.lastExposeTime; const settledExposeId = this.exposeId; this.exposeId = uuidV4(); this.lastExposeTime = 0; this.isExposing = false; if (this.conceal) { this.context?.reporter?.reportElementConceal({ key: this.key, extInfo: this.extInfo, exposeId: settledExposeId, allTime: exposeDuration, }); } } destroy() { if (this.lastExposeTime && this.isExposing) { // destroy 时若还在曝光中,则上报结束曝光 this.settleExpose(); } // 清空绑定关系 this.unbindExposeRoot(); } /** 沿 DOM 向上找到 ExposeRoot */ private getCurrentExposeRootElement(): ExposeHTMLElement | null { const helper = (el: ExposeHTMLElement) => { const parent = el.parentElement || (el.parentNode as ExposeHTMLElement); if (!parent) { return null; } if (parent === document.body) { return parent; } return parent?.dataset.mlRoot === DomDatasetBoolean.TRUE ? parent : helper(parent); }; return helper(this.el); } }