h5 埋点指北

3,697 阅读3分钟

背景

在日常的开发过程中,作为前端的 RD 同学,会经常的去配合 PM 去做一些业务埋点,而且有些埋点还贼复杂,埋点完成还要验收,对于 QA 跟 RD 来说都是一件很麻烦的事。

分析

痛点

  1. 埋点入侵了业务代码
// 这里有两个问题:1. 入侵了业务 2. 如果 clickReport 报错了,会影响正常的业务代码
const handleClick = () => {
    clickReport();
    doSomething();
}

  1. 携带业务参数过多,在组件间需要重复传递
// 层层传递无形之中增加了组件的复杂度
const Main = () => {
    const baseAnalytics = { city: 1, user: 2 };
    return (
        <div>
            <Child1 baseAnalytics={baseAnalytics} />
            <Child2 baseAnalytics={baseAnalytics} />
        </div>
}
  1. 部分复杂的埋点,需要在业务层面做很多额外的操作

目标

  1. 减少对业务的入侵
  2. 简化埋点的过程

方案

  1. 将埋点上报抽象出来 埋点的上报必然跟某个dom节点有关联,比如这个节点曝光了,或者这个节点被点击了。我们如果将我们的信息有规律的放在dom节点里,比如约公共的参数在上层,这样我们只要得到需要上报的节点,在一层一层的向上层查找,就可以将我们的数据聚合,并上报了

代码实现

const mergeLab = (parentLab: any, childLab: any) => {
  if (!parentLab) {
    return childLab;
  }

  if (!childLab) {
    return parentLab;
  }

  return { ...parentLab, ...childLab };
};

// 只支持 曝光 跟 点击 an 为 analytics 简称
export const findAnLabAndReport = (
  ele: HTMLElement | null,
  anData: any,
  eventType = "click",
  anReport
) => {
  const eventTypeId = eventType === "click" ? "anClickId" : "anViewId";

  // 查找到 body 算结束
  if (!ele || ele === document.body) {
    if (!anData.eventId || anData.eventId === "null") {
      return;
    }
    anReport(anData);
    return;
  }

  const data: any = ele.dataset || {};
  let newLab: any;
  let newEventId: string | undefined = anData.eventId;
  let newPageId: string | undefined = anData.pageId;

  if (data.anLab) {
    try {
      newLab = JSON.parse(data.anLab);
    } catch (e) {
      console.error(e);
    }
  }
  if (!anData.eventId && data[eventTypeId]) {
    newEventId = data[eventTypeId];
  }

  if (!anData.pageId && data.pageId) {
    newPageId = data.pageId;
  }

  findAnLabAndReport(
    ele.parentElement,
    {
      eventId: newEventId,
      pageId: newPageId,
      anLab: mergeLab(newLab, anData.anLab)
    },
    eventType,
    anReport
  );
};

  1. 处理点击事件上报
    利用事件冒泡机制,在body上面绑定点击事件,再从event对象中拿到触发点击的节点,将其传递给 findAnLabAndReport即可,这样我们就不在需要再业务代码中增加 clickReport 这样的方法了。
    注意:如果click事件被阻止冒泡了,这里就需要手动上传一下

实现效果

document.body.addEventListener(
    'click', 
    (e: any) => findAnLabAndReport(e.target, {}, 'click', clickReport),
);

const APP = () => {
  return (
    <div 
      data-page-id="PageId" 
      data-an-lab={JSON.stringify({ userid: -1, cityid: -1 })}
     >
      <ChildA />
    </div>
  );
};

const ChildA = () => {
  return (
    <div 
      data-an-click-id="ClickId" 
      data-an-lab={JSON.stringify({ id: 1, cityid: 2 })}
    >
        ChildA Click
    </div>
  );
};
  1. 处理曝光事件
    利用 intersection-observer 去做埋点曝光,核心还是要将处理的节点传递给我们的 findAnLabAndReport,拿到节点信息后,再将数据一层层聚拢,上报
const viewReportInit = (
  domOrDomList: HTMLElement | Array<HTMLElement>,
  viewReport,
  startObserver = true,
): Boolean => {
  if (!domOrDomList || (domOrDomList instanceof Array && !domOrDomList.length) || !startObserver) {
    return false;
  }

  let listerCount = domOrDomList instanceof Array ? domOrDomList.length : 1;

  const io = new IntersectionObserver(
    (IntersectionObserverEntryList: Array<IntersectionObserverEntry>) => {
      IntersectionObserverEntryList.forEach(
        (IntersectionObserverEntry: IntersectionObserverEntry) => {
          if (IntersectionObserverEntry.isIntersecting) {
            const targetEle = IntersectionObserverEntry.target;
            io.unobserve(targetEle);
            listerCount -= 1;
            findAnLabAndReport(targetEle as HTMLElement, {}, 'view', viewReport);
            if (listerCount <= 0) {
              io.disconnect();
            }
          }
        },
      );
    },
  );

  if (domOrDomList instanceof Array) {
    domOrDomList.forEach((ele: HTMLElement) => {
      io.observe(ele);
    });
  } else {
    io.observe(domOrDomList);
  }
  return true;
};

实现效果

const APP = () => {
  // 这里只执行一次,如果有依赖的需要执行多次的话,`会出现多次曝光`
  useEffect(() => {
    viewReportInit(
      Array.prototype.slice.call(
        document.querySelectorAll(".view-observer") || []
      ),
      data => console.log(data)
    );
  }, []);

  return (
    <div
      data-page-id="PageId"
      data-an-lab={JSON.stringify({ userid: -1, cityid: -1 })}
    >
      <ChildA />
      <ChildB />
    </div>
  );
};

const ChildA = () => {
  return (
    <div
      className="view-observer"
      style={{ height: "200px", backgroundColor: "#f0f0f0" }}
      data-an-view-id="viewIdA"
      data-an-lab={JSON.stringify({ id: 1, cityid: 2 })}
    >
      ChildA
    </div>
  );
};

const ChildB = () => {
  return (
    <div
      className="view-observer"
      style={{ height: "200px", backgroundColor: "#red" }}
      data-an-view-id="viewIdB"
      data-an-lab={JSON.stringify({ id: 2, cityid: 3 })}
    >
      ChildB
    </div>
  );
};

总结

笔者这里只是抛转引玉,具体的业务埋点可以根据自己的业务需求去定制,这里主要是想将埋点与业务代码去解耦,不再对业务代码造成严重的入侵,并且简化埋点的方式。 可以根据上面提供的核心方法 findAnLabAndReport 去做更多的业务定制,比如笔者在业务开发中,定制了 useObserver 这样一个 hook 去做曝光节点。

点击查看代码地址