可视化埋点sdk如何做呢

其实整体思路 首先划分两种选择,圈选模式,普通模式

当用户触发圈选选择

  1. 首先会禁止掉原生事件,比如click,onchange, 冒泡等事件
  2. 其次当用户hover ,或者click 待圈选元素,首先会获取到对应唯一标识符,其次将当前组件各种信息,属性,全部进行聚合收集,并通过添加样式表的方式,给目标元素从而进行高亮区分.
  3. 最终通过posemessage的方式,给圈选平台侧进行通信上报

整体思路我通过策略模式进行处理

js 复制代码
import resultArray from "@wind/wind-monitor-userConsider-markEngine/config/config";
import {
    TOPIC, TOPIC_STATUS, wlsCircleSelectText, WLS_CIRCLE_SELECT, WLS_CIRCLE_SELECT_ID,
    wlsCircleHighLightText, WLS_CIRCLE_HIGHLIGHT, WLS_CIRCLE_HIGHLIGHT_ID,
    wlsCircleMarkLightText, WLS_CIRCLE_MARKLIGHT, WLS_CIRCLE_MARKLIGHT_ID
} from "./types/constant";
import { Options } from "./types/options";
import { appendWLSStyle, removeWLSStyle } from "./utils/appendStyle";
import { win, doc } from "./utils/bom";
import eventUtil from "./utils/eventUtil";
import getEvent from "./utils/getEvent";
import getLogData from "./utils/getLogData";
import { DATA_CONSIDER_ID, DATA_CONSIDER_CONTROL_TYPE } from "./utils/userConsiderDic";

class CircleMark {
    options: Options;
    isItemSelected: boolean = false;
    topic!: TOPIC | null;
    actions!: { topic: TOPIC, status: string, function: Function }[];

    constructor (options: Options) {
        this.options = options;
        this.initActions();
        this.postMessageToMainWindow({ topic: 'circleMarkApp', data: { appName: this.options.appName } });
        this.addMessageListener();
    }

    initActions() {
        this.actions = [
            {
                topic: TOPIC.CIRCLE_SELECT, status: TOPIC_STATUS.ON, function: (topic: any, controlData: any) => {
                    // @ts-ignore
                    window.Is_Circle_Mark = true;
                    this.topic = topic;
                    this.bindCircleCollection(true);
                    appendWLSStyle(WLS_CIRCLE_SELECT_ID, wlsCircleSelectText);
                }
            },
            {
                topic: TOPIC.CIRCLE_SELECT, status: TOPIC_STATUS.OFF, function: (topic: any, controlData: any) => {
                    // @ts-ignore
                    window.Is_Circle_Mark = false;
                    this.topic = null;
                    this.bindCircleCollection(false);
                    removeWLSStyle(WLS_CIRCLE_SELECT_ID);
                }
            },
            {
                topic: TOPIC.CIRCLE_HIGHLIGHT, status: TOPIC_STATUS.ON, function: (topic: any, controlData: any) => {
                    this.topic = topic;
                    appendWLSStyle(WLS_CIRCLE_HIGHLIGHT_ID, wlsCircleHighLightText);
                    this.highLightElements(true, controlData);
                }
            },
            {
                topic: TOPIC.CIRCLE_HIGHLIGHT, status: TOPIC_STATUS.OFF, function: (topic: any, controlData: any) => {
                    this.topic = null;
                    removeWLSStyle(WLS_CIRCLE_HIGHLIGHT_ID);
                    this.highLightElements(false, controlData);
                }
            },
            {
                topic: TOPIC.CIRCLE_NAVIGATION_CONTROLS, status: TOPIC_STATUS.ON, function: (topic: any, controlData: any) => {
                    this.getNavigationControls();
                }
            },
            {
                topic: TOPIC.CIRCLE_MARKLIGHT, status: TOPIC_STATUS.ON, function: (topic: any, controlData: any) => {
                    this.topic = topic;
                    appendWLSStyle(WLS_CIRCLE_MARKLIGHT_ID, wlsCircleMarkLightText);
                    this.highLightMarkElements(true, controlData);
                }
            },
            {
                topic: TOPIC.CIRCLE_MARKLIGHT, status: TOPIC_STATUS.OFF, function: (topic: any, controlData: any) => {
                    this.topic = null;
                    removeWLSStyle(WLS_CIRCLE_MARKLIGHT_ID);
                    this.highLightMarkElements(false, controlData);
                }
            }
        ];
    }

    addMessageListener() {
        //监听外部控件控制开关功能
        eventUtil.on(win, 'message', (event: any) => {
            if (!event.data?.type?.includes('webpack')) {
                try {
                    const data = event.data?.topic ? event.data : JSON.parse(event.data);

                    const { topic, status, controlData } = data;
                    const action = this.actions.find((item) => (item.topic === topic && item.status === status));
                    action?.function(topic, controlData);
                } catch (e) {
                    console.log(e);
                }
            }
        })
    }

    isIframeElementVisible(iframe, element) {
        var rect = element.getBoundingClientRect();
        var viewHeight = iframe.documentElement.clientHeight;
        return !(rect.bottom < 0 || rect.top - viewHeight >= 0);
    }

 highLightElements(highLight: any, controlData: any) {
        if (highLight) {
            controlData.forEach((item: any) => {
                const items = document.querySelectorAll(`[${DATA_CONSIDER_ID}="${item.dataConsiderId}"]`);
                items.forEach((item: any) => {
                    item && item.classList.add(WLS_CIRCLE_HIGHLIGHT);
                });
                if (items.length >= 1) {
                    const element = items[0];
                    if (!this.isIframeElementVisible(document, element)) {
                        // @ts-ignore
                        element.scrollIntoViewIfNeeded ? element.scrollIntoViewIfNeeded() : element.scrollIntoView();
                    }
                }
            });
        } else {
            controlData.forEach((item: any) => {
                const items = document.querySelectorAll(`[${DATA_CONSIDER_ID}="${item.dataConsiderId}"]`);
                items.forEach((item: any) => {
                    item && item.classList.remove(WLS_CIRCLE_HIGHLIGHT);
                });
            });
        }
    }

    getNavigationControls() {
        const items = [...document.querySelectorAll(`[${DATA_CONSIDER_ID}]`)];
        let controls = items.filter(item => {
            const type = item.attributes[DATA_CONSIDER_CONTROL_TYPE]?.value;
            const controlConfig = resultArray.find(x => {
                return x.controlType === type &&
                    (!x.eventName.includes("click")
                        && !x.eventName.includes("scroll") ||
                        ["dropdown", "checkbox", "checkboxgroup", "radio", "radiogroup", "rate", "switch"].includes(x.controlType))
            });
            return controlConfig !== undefined;
        }).reduce((pre, cur) => {
            const id = cur.attributes[DATA_CONSIDER_ID]?.value;
            let control = {
                controlId: id.split("_").reverse()[0],
                controlType: cur.attributes[DATA_CONSIDER_CONTROL_TYPE]?.value
            }
            let existControl = pre.find(x => { return x.controlId === control.controlId });
            if (control.controlId?.length > 0 && !existControl) {
                return pre.concat(control);
            }
            else {
                return pre;
            }
        }, []);
        this.postMessageToMainWindow({ topic: 'navigationControls', data: controls });
    }

    highLightMarkElements(highLight: any, controlData: any) {
        if (highLight) {
            let Items = document.querySelectorAll(`[${DATA_CONSIDER_ID}]`);
            // @ts-ignore
            Items.forEach((item) => {
                const controlId = item.getAttribute(DATA_CONSIDER_ID);
                if (!controlData.find((x: { dataConsiderId: any; }) => { return x.dataConsiderId === controlId })) {
                    item && item.classList.add(WLS_CIRCLE_MARKLIGHT);
                }
            });
        } else {
            let Items = document.querySelectorAll(`[${DATA_CONSIDER_ID}]`);
            // @ts-ignore
            Items.forEach((item) => {
                const controlId = item.getAttribute(DATA_CONSIDER_ID);
                if (!controlData.find((x: any) => { return x.dataConsiderId === controlId })) {
                    item && item.classList.remove(WLS_CIRCLE_MARKLIGHT);
                }
            });
        }
    }



selectElement(event: any, targetElement: any) {
        this.isItemSelected = true;
        const elements = doc.getElementsByClassName(WLS_CIRCLE_SELECT);
        for (let i = 0, len = elements.length; i < len; i++) {
            elements[i].classList.remove(WLS_CIRCLE_SELECT);
        }
        eventUtil.stopDefault(event);
        eventUtil.stopBubble(event);
        targetElement.classList.add(WLS_CIRCLE_SELECT);
    }

    postMessageToMainWindow(message: any) {
        const { postMsgOpts } = this.options;
        postMsgOpts.forEach((opt) => {
            const { targetWindow, targetOrigin } = opt;
            targetWindow.postMessage(message, targetOrigin);
        });
    }

    isMarkedAsConsider(targetElement: any) {
        const { attributes } = targetElement;
        let flag = false;
        if (attributes[DATA_CONSIDER_ID]) {
            flag = true;
        }
        return flag;
    }

    findElementByAttr(element: HTMLElement) {
        let trueElement = element;
        let i = 0;
        while (!this.isMarkedAsConsider(trueElement) && i < 3) {
            if (trueElement.parentElement) {
                trueElement = trueElement.parentElement;
            }
            else {
                break;
            }
            i++;
        }
        return trueElement;
    }

    circleHoverHandle(e: Event) {
        let scope = this;
        if (scope.isItemSelected) {
            setTimeout(() => {
                scope.isItemSelected = false;
            }, 500);
            return;
        }
        try {
            let { event, targetElement } = getEvent(e);
            let element = targetElement;
            if (!this.isMarkedAsConsider(element)) {
                element = this.findElementByAttr(targetElement.parentElement);
            }

            if (this.isMarkedAsConsider(element)) {
                const assignData = {
                    et: 'mouseenter',
                    ed: 'auto_hover',
                }
                const logData = getLogData(event, element, assignData);
                //@ts-ignore
                if (window.Is_Circle_Mark) {
                    this.selectElement(event, element);
                }
            }
        } catch (err) {
            console.log(`circleHoverHandle`, err);
        }
    }

    circleClickHandle(e: Event) {
        try {
            const { event, targetElement } = getEvent(e);
            let element = targetElement;
            if (!this.isMarkedAsConsider(element)) {
                element = this.findElementByAttr(targetElement.parentElement);
            }

            if (this.isMarkedAsConsider(element)) {
                const { appName } = this.options;
                const logData = getLogData(event, element);
                //@ts-ignore
                if (window.Is_Circle_Mark) {
                    this.selectElement(event, element);
                    this.postMessageToMainWindow({ topic: 'circleMark', data: { ...logData, appName } });
                } else {
                    console.log(logData);
                }
            }
        } catch (err) {
            console.log(`circleClickHandle`, err);
        }
    }

    throttleLast(fn: Function, delay: number): Function {
        let timer: any;
        return function (...args: any) {
            clearTimeout(timer);
            timer = setTimeout(() => {
                // @ts-ignore
                fn.apply(this, args)
            }, delay);
        }
    }

    bindCircleCollection(switcher: boolean = false) {
        const circleHoverThrottle = this.throttleLast(this.circleHoverHandle.bind(this), 500);
        const circleClickHandleThrottle = this.circleClickHandle.bind(this);
        if (switcher) {
            eventUtil.on(doc.body, 'click', circleClickHandleThrottle);
            eventUtil.on(doc.body, 'mouseenter', circleHoverThrottle);
        }
        else {
            eventUtil.off(doc.body, 'click', circleClickHandleThrottle);
            eventUtil.off(doc.body, 'mouseenter', circleHoverThrottle);
        }
    }
}

export default CircleMark;




const event = {
    on: (target: any, type: any, handler: any) => {
        if (target.addEventListener) {
            target.addEventListener(type, handler, true);
        } else {
            target.attachEvent(
                'on' + type,
                (event: Event) => handler.call(target, event),
                false,
            );
        }
    },
    off: (target: any, type: any, handler: any) => {
        if (target.removeEventListener) {
            target.removeEventListener(type, handler, true);
        } else {
            target.detachEvent(
                'on' + type,
                (event: Event) => handler.call(target, event),
                false
            );
        }
    },
    stopDefault: (e: Event) => {
        if (typeof e.preventDefault === 'function') {
            e.preventDefault();
        }
        if (typeof e.returnValue === 'boolean') {
            e.returnValue = false;
        }
    },
    stopBubble: (e: Event) => {
        if (typeof e.stopPropagation === 'function') {
            e.stopPropagation();
        }
        if (typeof e.cancelBubble === 'boolean') {
            e.cancelBubble = true;
        }
    },
};

export default event;






mport { ref, title } from "./bom";
import { DATA_CONSIDER_ID, DATA_CONSIDER_CONTROL_TYPE, DATA_CONSIDER_INDEX } from "./userConsiderDic";

//目的是为了能够获取当前准确的位置
const getBoundingClientRect = (elm: any) => {
    const rect = elm?.getBoundingClientRect();
    const { left, top, right, bottom } = rect;
    const width = rect.width || right - left;
    const height = rect.height || bottom - top;

    return {
        width,
        height,
        left,
        top,
        bottom,
        right,
    };
}

const sliceText = (text = '') => {
    const limit = 15;
    const len = text.length;
    if (len > limit * 2) {
        return `${text.substring(0, limit)}...${text.substring(len - limit, len)}`;
    }
    return text;
}

const buildLogData = (eventData: { dataConsiderId: any; dataConsiderControlType: any; dataConsiderEvent: string; dataConsiderEvents: string[]; dataConsiderIndex: any; left: any; top: any; width: any; height: any; }) => {
    return {
        eventData: {
            ...eventData,
            rUrl: ref,
            docTitle: title,
            t: new Date().getTime(),
        },
    };
}

const getLogData = (event: { pageX: any; clientX: number; pageY: any; clientY: number; }, targetElement: { nodeName?: any; innerText?: any; value?: any; attributes?: any; getBoundingClientRect?: any }, assignData = {}) => {
    const { attributes } = targetElement;
    const dataConsiderId = attributes[DATA_CONSIDER_ID]?.value.toString();
    const dataConsiderControlType = attributes[DATA_CONSIDER_CONTROL_TYPE]?.value.toString();
    let dataConsiderIndex = [];
    if (attributes[DATA_CONSIDER_INDEX]) {
        dataConsiderIndex.push(attributes[DATA_CONSIDER_INDEX]?.value.toString());
    }
    if (attributes[`${DATA_CONSIDER_INDEX}-y`]) {
        dataConsiderIndex.push(attributes[`${DATA_CONSIDER_INDEX}-y`]?.value.toString());
    }
    const nodeName = targetElement.nodeName && targetElement.nodeName.toLocaleLowerCase() || '';
    const text = targetElement.innerText || targetElement.value;
    const rect = getBoundingClientRect(targetElement);
    const documentElement = document.documentElement || document.body.parentNode;
    // @ts-ignore
    const scrollX = (documentElement && typeof documentElement.scrollLeft == 'number' ? documentElement : document.body).scrollLeft;
    // @ts-ignore
    const scrollY = (documentElement && typeof documentElement.scrollTop == 'number' ? documentElement : document.body).scrollTop;
    const pageX = event.pageX || event.clientX + scrollX;
    const pageY = event.pageY || event.clientY + scrollY;

    // @ts-ignore
    let canSelectAttributes = Array.from(attributes).map((x: any) => { return x.name; });
    if (nodeName === 'button') {
        canSelectAttributes.push('button-name');
    }
    let multipleItemNoIndex = documentElement?.querySelectorAll ? documentElement.querySelectorAll(`[${DATA_CONSIDER_ID}="${dataConsiderId}"]`)?.length > 1 && dataConsiderIndex.length === 0 : false;

    const eventData = {
        dataConsiderId,
        dataConsiderControlType,
        dataConsiderEvent: '',
        dataConsiderIndex,
        nodeName,
        canSelectAttributes,
        multipleItemNoIndex,
        text: sliceText(text),
        offsetX: ((pageX - rect.left - scrollX) / rect.width).toFixed(6),
        offsetY: ((pageY - rect.top - scrollY) / rect.height).toFixed(6),
        pageX,
        pageY,
        scrollX,
        scrollY,
        left: rect.left,
        top: rect.top,
        width: rect.width,
        height: rect.height,
    };

    const logData = buildLogData({ ...eventData, ...assignData });

    return logData;
}

export default getLogData;
相关推荐
就是帅我不改43 分钟前
基于领域事件驱动的微服务架构设计与实践
后端·面试·架构
ruokkk1 小时前
当你配置了feign.sentinel.enable=true时发生什么
后端·架构
abigalexy4 小时前
百万并发QPS-分布式架构设计
分布式·架构
小傅哥5 小时前
Ai Agent 自研项目 VS 字节扣子,差点超越?
后端·架构
Gavin在路上5 小时前
企业架构之导论(1)
架构
SimonKing5 小时前
Web不用跳白页,直接在当前页面下载文件
后端·程序员·架构
DemonAvenger6 小时前
边缘计算场景下Go网络编程:优势、实践与踩坑经验
网络协议·架构·go
静谧之心6 小时前
分层架构下的跨层通信:接口抽象如何解决反向调用
java·开发语言·设计模式·架构·golang·k8s·解耦
高阳言编程15 小时前
1. 概论
架构