埋点

"埋点" 是一种在应用程序或网站中插入代码的技术,用于收集用户行为数据或特定事件的信息。它是用于分析和监控用户行为、应用性能和其他关键指标的一种常用方法。通过在特定位置插入代码或调用特定的 API,开发人员可以捕获有关用户如何与应用程序或网站交互的数据。

埋点的目的是为了收集关键的指标和数据,以便帮助了解用户行为、改进用户体验、优化应用性能、进行 A/B 测试和支持业务决策。通过埋点,可以收集以下类型的数据:

  1. 用户行为数据:例如页面浏览量、点击事件、表单提交、购买行为等。

  2. 应用性能数据:例如页面加载时间、API 调用延迟、错误日志等。

  3. 设备和环境数据:例如用户设备类型、操作系统、浏览器版本等。

  4. 用户属性数据:例如用户ID、地理位置、用户角色等。

常见的埋点方式包括:

  1. 手动埋点:开发人员在代码中显式地插入埋点代码,通常使用 JavaScript 或其他编程语言实现。

  2. 自动埋点:使用自动化工具或框架,自动收集某些标准事件或用户行为数据。

  3. 可视化埋点:使用可视化工具,在页面上直接选择元素或交互,并配置要捕获的事件。

埋点数据通常会被发送到数据分析平台或服务,如Google Analytics、Mixpanel、Amplitude、Heap等,用于处理和分析数据。分析人员和业务决策者可以使用这些数据来获得深入了解用户行为和应用性能的见解,以便优化产品和业务策略。

方式

埋点数据的上报方式可以有多种形式,具体选择哪种方式取决于我们的应用程序和需求。以下是一些常见的埋点数据上报方式:

  1. HTTP请求: 埋点数据可以通过发送HTTP请求将收集到的数据上报到后端服务器或第三方统计平台。通常使用POST请求将数据作为参数发送到指定的接口。后端服务器或第三方平台接收数据后进行处理和存储。但是一般而言,埋点域名并不是当前域名,因此请求会存在跨域风险,且如果 ajax 配置不正确可能会浏览器拦截,有些埋点还需要频繁上报,容易浪费请求资源。

  2. WebSocket: 如果需要实时上报数据或持续与后端保持连接,可以使用WebSocket协议。WebSocket允许客户端与服务器进行双向通信,可以实时发送埋点数据到后端。

  3. 日志文件: 埋点数据可以以日志文件的形式记录在客户端或服务器上,然后通过定时任务或其他手段将日志文件上传到后端进行处理。这种方式适用于离线处理和批量上报数据。

  4. 消息队列: 它是一种在计算机系统中用于异步通信的机制,用于在不同组件、模块或进程之间传递消息,是一种常见的通信方式,用于解耦不同部分的系统,实现并发处理,以及确保异步任务的顺序性。使用消息队列可以实现异步上报数据,将埋点数据放入消息队列,然后由后台服务从消息队列中取出数据进行处理和上报。

消息队列的基本原理如下,由三个部分组成:

  • 生产者(Producer) :生产者负责将消息放入消息队列。这些消息可以是任务、数据、事件等。

  • 消息队列:消息队列类比是一个中介,负责存储生产者放入的消息。消息队列通常有不同的模式,如先进先出(FIFO)或发布-订阅(Pub/Sub)等。

  • 消费者(Consumer) :消费者从消息队列中获取消息,并进行相应的处理。消费者可能是单个进程或多个并发进程。

消息队列的优势包括:

  • 解耦:生产者和消费者之间相互独立,不需要知道彼此的存在。

  • 异步处理:生产者不需要等待消息被消费,可以继续执行其他任务。

  • 并发处理:多个消费者可以并行处理消息,提高系统性能。

  • 顺序性:消息队列通常能够保证消息按照放入的顺序被消费。

常见的消息队列系统包括:

  • RabbitMQ:基于AMQP(高级消息队列协议)的消息队列系统,支持多种消息模式。

  • Apache Kafka:高吞吐量、分布式的消息系统,适用于大规模数据流处理。

  • Redis:除了缓存外,Redis也可以作为消息队列使用,支持发布-订阅模式。

  • Amazon SQS:亚马逊提供的托管式消息队列服务。

看来看去,还好认识Redis。。。。。。

  1. 可视化埋点工具: 一些可视化埋点工具提供了自动上报功能,它们会在前端代码中自动插入埋点代码,并将收集到的数据上报到它们的服务器。我们只需要在工具的配置界面定义需要追踪的事件,工具会自动生成并上报相应的数据。

  2. 第三方SDK: 有些第三方埋点服务提供SDK,只需要集成他们的SDK到我们的应用程序中,SDK会负责收集和上报数据。

  3. script、link标签:当我们使用 script 和 link 进行埋点上报时,需要挂载到页面上,而反复操作 dom 会造成页面性能受影响,而且载入 js/css 资源还会阻塞页面渲染,影响用户体验,因此对于需要频繁上报的埋点而言,script 和 link 并不合适。

  4. img标签:通常使用 img 标签去做埋点上报,img 标签加载并不需要挂载到页面上,基于 js 去 new image(),设置其 src 之后就可以直接请求图片。相比较于其他方式,该方式拥有以下特点:

  • img 兼容性好

  • 无需挂载到页面上且不会反复操作 dom结构

  • img 的加载不会阻塞 html 的解析,但 img 加载后并不渲染,它需要等待 Render Tree 生成完后才和 Render Tree 一起渲染出来。

通常埋点上报会使用 gif 图,相比较于jpg,gif图更小,合法的 GIF 只需要 43 个字节

9、 Navigator.sendBeaconNavigator.sendBeacon 是一个 Web API 方法,用于在浏览器后台异步发送数据,通常用于在页面卸载或关闭时发送数据,以确保数据能够被成功传递给服务器,即使页面正在关闭。这对于需要在用户离开页面时进行一些数据收集、日志记录等操作非常有用。

sendBeacon 方法可以接受两个参数:

  • url:一个字符串,表示要发送数据的目标 URL。

  • data:一个 ArrayBufferView、Blob、DOMString 或 FormData 对象,包含要发送的数据。

sendBeacon 方法主要用于发送少量的数据,通常用于发送一些统计信息、日志记录等,而不适用于大量数据的传输。此外,sendBeacon 方法是异步的,它会在后台发送数据,不会阻塞页面的关闭或卸载。而使用 sendBeacon 可以确保即使用户关闭了页面,数据也能够成功发送给服务器,从而帮助开发人员更好地进行数据采集和记录。

不论选择哪种方式,都需要考虑数据的安全性、实时性和准确性。同时,还需注意数据量的大小和频率,避免因过多的上报数据而影响应用程序性能或网络负载。定期监控和分析上报的数据,确保收集到有用的信息,为产品优化和决策提供支持。

而我们开发人员需要考虑的就是,如何触发埋点上报事件,是曝光、点击、滚动还是什么事件,之后需要获取对应的dom页面信息。为了避免重复上报,还需要进行数据去重,并选择合适的上报方法。

IntersectionObserver

在 JavaScript 中,IntersectionObserver 是一个构造函数,可以创建一个新的观察者对象,用于观察目标元素与其包含父元素或视口之间的交叉区域。

14fa6a3453a1f80f3b32c1222f17d7fd.jpeg
image.png

IntersectionObserver 对象具有以下属性:

  • root: 表示目标元素所在的根元素。它是一个 DOM 节点,默认为 null,表示使用视口作为根元素。可以设置这个属性来观察目标元素与其包含父元素之间的交叉区域。

  • rootMargin: 一个用于扩展或缩小 root 元素边界的 margin 值,用于扩大或缩小交叉区域的边界。如果在初始化的时候未被指定,它将被设置成默认值 "0px 0px 0px 0px"

  • thresholds: 一个数组,表示当目标元素与根元素的交叉区域达到指定的阈值时,触发回调函数。阈值是一个0.0到1.0的数字数组,默认为 [0],表示目标元素进入或离开根元素时立即触发回调。

  • activeObservations: 一个 IntersectionObserverEntry 对象数组,表示当前正在被观察的元素的交叉信息。

  • constructor: 一个表示 IntersectionObserver 构造函数的引用。

IntersectionObserver 对象通常在实例化时使用这些属性进行配置,然后通过调用 observe() 方法观察指定的目标元素。

const options = {
  root: document.querySelector('#container'),
  rootMargin: '50px',
  threshold: [0, 0.5, 1]
};

const observer = new IntersectionObserver(callback, options);

function callback(entries, observer) {
  entries.forEach(entry => {
    console.log(entry.intersectionRatio); // 交叉区域的比例
    console.log(entry.isIntersecting); // 是否与根元素交叉
    console.log(entry.target); // 目标元素本身
  });
}

const targetElement = document.querySelector('#target');
observer.observe(targetElement);

上述代码创建了一个 IntersectionObserver 实例,并将其配置为观察一个名为 targetElement 的元素。当该元素进入或离开其父元素,即root设置的对象: #container 的交叉区域时,触发回调函数 callback。回调函数中的 entries 参数是一个包含 IntersectionObserverEntry 对象的数组,提供了关于交叉区域的信息,如交叉比例、是否交叉等。

IntersectionObserverEntry

IntersectionObserverEntry 对象是由 IntersectionObserver 回调函数传递的观察者的条目信息。它提供了有关目标元素与其根元素或视口之间交叉区域的详细信息。当目标元素进入或离开根元素或视口的交叉区域时,会创建一个新的 IntersectionObserverEntry 对象,并传递给观察者的回调函数。

IntersectionObserverEntry 对象具有以下属性:

  • time: 表示交叉区域变化发生的时间戳,以毫秒为单位。

  • rootBounds: 一个 DOMRectReadOnly 对象,表示根元素的边界框信息。

  • boundingClientRect: 一个 DOMRectReadOnly 对象,表示目标元素的边界框信息(相对于视口)。

  • intersectionRect: 一个 DOMRectReadOnly 对象,表示目标元素与根元素的交叉区域的边界框信息。

  • intersectionRatio: 一个表示交叉区域的比例的值,范围从 0.0(目标元素完全在根元素之外)到 1.0(目标元素完全在根元素之内)。

  • isIntersecting: 一个布尔值,表示目标元素当前是否与根元素交叉。如果目标元素至少有一个像素与根元素交叉,则为 true,否则为 false

  • target: 表示被观察的目标元素本身。

在观察者的回调函数中,可以通过 IntersectionObserverEntry 对象的这些属性来了解目标元素与根元素的交叉情况,并根据需要执行相应的操作。

注意在Intersection Observer API 的V2版本当中新增了一个 isVisible 属性(新版Chrome浏览器已经支持,Safari等其他浏览器内不支持),用来标识元素是否“可见”(因为即便元素在可视区域内,也有可能因为被其他元素遮挡、样式属性hiden等影响导致元素不能被看到),官方说明:为了保证性能,这个字段的值不一定是准确的,除非特殊场景,否则不建议使用这个字段。而大部分场景isIntersecting足矣;

const options = {
  threshold: [0, 0.5, 1]
};

const observer = new IntersectionObserver(callback, options);
//entries即为IntersectionObserverEntry对象
function callback(entries, observer) {
  entries.forEach(entry => {
    console.log('IntersectionRatio:', entry.intersectionRatio);
    console.log('Is Intersecting:', entry.isIntersecting);
    console.log('Target element:', entry.target);
    console.log('Root bounds:', entry.rootBounds);
    console.log('Bounding client rect:', entry.boundingClientRect);
    console.log('Intersection rect:', entry.intersectionRect);
    console.log('----------------------');
  });
}

const targetElement = document.querySelector('#target');
observer.observe(targetElement);

每当 targetElement 进入或离开视口的交叉区域时,回调函数会被触发,并打印 IntersectionObserverEntry 对象的相关属性信息。

getBoundingClientRect

getBoundingClientRect 是一个 DOM 元素的方法,用于获取该元素相对于视口(viewport)的位置信息。它返回一个 DOMRect 对象,其中包含了元素的位置、大小等属性。

DOMRect 对象包含以下属性:

  • top: 元素上边界相对于视口顶部的距离。

  • right: 元素右边界相对于视口左边的距离。

  • bottom: 元素下边界相对于视口顶部的距离。

  • left: 元素左边界相对于视口左边的距离。

  • width: 元素的宽度。

  • height: 元素的高度。

使用 getBoundingClientRect 可以用来计算元素在视口中的位置,判断元素是否可见,实现动态布局等等。

IntersectionObserver源码

下面的代码是一个简化版本的示例,并非完整的 IntersectionObserver 源代码。但是也说明 了IntersectionObserver 的基本工作原理。

// IntersectionObserver 构造函数
function IntersectionObserver(callback, options) {
  this.callback = callback;
  this.options = options;
  this.targets = []; //观察的目标数组
  // 监听视口的滚动事件
  window.addEventListener('scroll', this.handleScroll.bind(this));
}

// 监听目标元素是否进入或离开视口的方法 observe方法
IntersectionObserver.prototype.observe = function (target) {
  if (this.targets.indexOf(target) === -1) {//不存在该观察的元素,push进target数组
    this.targets.push(target);
  }
  this.handleScroll(); // 立即检查目标元素的状态
};

// 处理视口滚动事件
IntersectionObserver.prototype.handleScroll = function () {
  const viewportHeight = window.innerHeight; //当前仅视为相对于window,root为null
  
  // 遍历所有观察目标元素
  this.targets.forEach(target => {
    //利用该getBoundingClientRect的api
    const targetRect = target.getBoundingClientRect();
    //options设置的margin参数
    const rootMargin = this.options.rootMargin || '0px';

    // 计算目标元素的可见区域
    const isVisible = targetRect.top - parseInt(rootMargin) < viewportHeight && targetRect.bottom + parseInt(rootMargin) > 0;

    // 创建 IntersectionObserverEntry 对象
    const entry = {
      time: Date.now(),
      target: target,
      rootBounds: null,
      boundingClientRect: targetRect,
      intersectionRect: isVisible
        ? {
            top: Math.max(targetRect.top, 0),
            bottom: Math.min(targetRect.bottom, viewportHeight),
            left: 0,
            right: window.innerWidth,
            width: window.innerWidth,
            height: Math.min(targetRect.height, viewportHeight)
          }
        : { top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0 },
      intersectionRatio: isVisible
        ? (Math.min(targetRect.bottom, viewportHeight) -
           Math.max(targetRect.top, 0)) /
          targetRect.height
        : 0,
      isIntersecting: isVisible
    };

    // 调用回调函数,并传递 IntersectionObserverEntry 对象
    this.callback([entry], this);
  });
};

// 创建一个 IntersectionObserver 实例
const observer = new IntersectionObserver(function (entries, observer) {
  // 在此处执行交叉区域变化的逻辑
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log('Element is visible');
    } else {
      console.log('Element is not visible');
    }
  });
}, {
  root: null, // 使用视口作为根元素
  threshold: 0.5 // 50% 的交叉区域时触发回调
});

// 观察目标元素
const targetElement = document.querySelector('#target');
observer.observe(targetElement);

以上只是 IntersectionObserver 的简化版本,实际的 IntersectionObserver API 在浏览器中进行了更复杂和高效的实现,IntersectionObserver 内部还需要获取options当中的参数root元素的getBoundingClientRect的属性,与观察对象元素的各个属性进行计算,并返回最终结果。

为什么要介绍上述几个API呢?IntersectionObserver是一个用于监测元素在视口(viewport)内可见性的API,虽然它的主要用途不是为了埋点,但在某些情况下,IntersectionObserver可以为埋点上报提供一些有用的功能和优势。

比如区域曝光埋点,什么时候触发埋点数据上报以及该元素是否存在其他的情况(如该元素内是否包含了其他的元素、是否存在展现和隐藏元素的情况等),这些都无法让我们精确的知道埋点上报的时机, 而IntersectionObserver可以仅在特定元素进入视口时触发埋点,节省性能和资源;实现延迟加载和懒加载,在元素变为可见时触发埋点,即使用户在页面上进行快速滚动或交互,也可以准确地检测元素的可见性,同时自定义触发条件,如元素进入视口的百分比、持续时间等;若有动态加载、展示和隐藏元素的情况,也可以捕捉到这些动态变化,从而更全面地收集用户交互数据。

接下来,我们看看Golong是个啥

GolongInit 初始化

在init函数当中,主要做的是一些参数和函数的初始化,以及一些准备工作。

初始化GolongInitLite对象

  • 处理调试工具,初始化全局参数

  • 设置t值

含义:cookie中的t值,表示运行当前M页的容器(例如转转iOS、转转安卓、58APP、微信等),其中不同小程序拥有不同t值,兼容处理要从userAgent进行判断.而小程序则必须为每一个在 webview 中使用的页面链接的 url 上自动封装 __t 参数,保证小程序中的 M 页也可以正确的埋入 t 值;

  • t值取值逻辑

    • 首先根据调用方法传入的t来设置,先取参数的值

    • 再从顶层窗口 url 上去 t (iframe)

    • 再从当前 url 参数中取

    • 最后从ua中取

const t = __t || getQueryFromTop('__t') || getQuery('__t') || getTFromUA()
  • 设置tk值,一般是客户端注入的,如果没有就是nginx层设置的

    tk即为token,取值方法

    • 首先cookies里面取tk, cookies里面的tk值是客户端埋入的

    • 如果cookies里面没有从url上取tk, 小程序环境中,则只有在url上有tk

    • 如果还没有,使用idzz或者id58

  • 设置 referer 的值,即设定 pageUrl 就是 referer,将其设置为zzreferer存在cookie当中。initReferer()函数当中进行设置,setCookie({ zzreferer: pageUrl })

  • 过滤 pageuqery 中的无效值, 防止 url 过长

  • 把指定的参数统一放到window['zz_Golong_backup']中

  • 初始化使用插件,挂载use函数

startMutationObserver 开启MutationObserver

该函数拥有两个参数,autoGoodsExpose和autoAreaExpose,分别开启对应的商品曝光和区域曝光埋点上报。

该函数主要功能就是观察根节点的子孙节点的新增和删除,主要利用了MutationObserver API

MutationObserver

MutationObserver 是 JavaScript 中的一个 Web API,它允许开发者监视 DOM 树的变化。通过 MutationObserver,可以监听指定 DOM 元素及其子元素的增加、删除、属性变化等事件,从而在 DOM 发生变化时执行特定的操作或逻辑。

使用 MutationObserver 的基本步骤如下:

  1. 创建一个 MutationObserver 实例,并传入一个回调函数作为参数。该回调函数会在 DOM 发生变化时被调用。

  2. 使用 observe() 方法指定要监听的 DOM 元素及其配置选项。配置选项包括是否监听子元素的变化、是否监听属性变化等。

  3. 在回调函数中处理 DOM 变化时的逻辑。

<!DOCTYPE html>
<html>
<head>
  <title>Example</title>
</head>
<body>
  <div id="container">
    <p>Example</p>
  </div>

  <script>
    // 创建 MutationObserver 实例并定义回调函数
    const observer = new MutationObserver((mutationsList, observer) => {
      for (const mutation of mutationsList) {
        if (mutation.type === 'childList') {
          console.log('子元素发生变化!');
        } else if (mutation.type === 'attributes') {
          console.log('元素属性发生变化!');
        }
      }
    });

    // 监听指定 DOM 元素及其子元素的增加、删除、属性变化
    observer.observe(document.getElementById('container'), {
      childList: true,
      attributes: true,
      subtree: true
    });

    // 模拟 DOM 变化
    setTimeout(() => {
      document.getElementById('container').appendChild(document.createElement('p'));
    }, 2000);
  </script>
</body>
</html>

在上述示例中,创建了一个 MutationObserver 实例,并定义了回调函数。通过 observer.observe() 方法,监听 id"container"<div> 元素及其子元素的增加、删除和属性变化。

然后,通过 setTimeout 模拟了一个 DOM 变化,向 id"container"<div> 元素中添加了一个新的 <p> 元素。,MutationObserver 监测目标节点及其子孙节点的添加、删除、属性变化等操作。当 DOM 发生变化时,回调函数会输出相应的提示信息。

Mutation Observer 接口提供了监视对DOM树所做更改的能力,通过new实例化后,调用observe()开始观察,disconnect()可停止观察

visitOrderTracking 自动埋点逻辑初始化

  • zpm逻辑初始化,依赖注入参数 routeList,即项目里的pageIdConfig.js文件,存储pageId信息,bm_consign将此文件放置到了router文件夹里面,类似下面的代码形式:

// pageIdInfoArr
   [{     name: 'order-detail',     route: '/order/detail',     pageId: 'K1083',     level: 6   },    {      name: 'external',      route: '/helpsale/external',      pageId: 'M6670',      level: 2    }]
  • 初始化点击事件监听 initEventListener,页面点击监听,收集参数,sortId,sortName等

    document.addEventListener('click', clickCallback, true)

clickCallback

  • sectionId 是当前页面区域ID对页面中区域划分唯一标识,所以该节点需要存在ZZ_SECTION_ID属性,否则不能进行下一步查找。

if (document.querySelector) {
    const ele = document.querySelector(`[${ZZ_SECTION_ID}]`)
    if (!ele) return
  }

clickCallback核心:

递归算法,

  • 获取 sectionId,先获取父节点的,没有则获取自身的,二者如若存其一,则结束

  • 赋值父节点为父父级节点, parent = parent.parentElement,继续获取

  • 一直到没有父节点或者整个 dom 树没有 sectionId 则退出

查找顺序 父级 --> 自身 --> 父父级

最后开启页面zpmshow事件

visitStackTracking 初始化页面展现钩子逻辑

初始化页面展现钩子,记录页面的访问路径根据

  • 查找cookie当中是否有zzVisitStack,有就赋值给visitStack,且设置访问栈的最大长度stackMaxLen = 2

  • 判断visitStack当前长度

    • 不同,push操作

    • 相同,更新并替换访问事件

    • 为0,push当前路由

    • 不为0,比对当前路由与上一次记录的路由是否相同,进行更新访问栈的操作

  • 判断visitStack长度是否超出最大长度,超出了,则留下最新访问的两个记录,即最后两条

  • 设置cookie为 zzVisitStack

其余操作

1、开启统计页面停留时长功能,

2、将两个函数分别添加到生命周期onShow、onHide的执行函数队列当中,利用performance对象属性,保存startTime = performance.now()

  • onShow当中的函数设置setInterval,定时计算停留经过时间:elapsedTime = ((performance.now() - startTime) / 1000).toFixed(2),并与其他数据进行整合,存储为本地localStorage.setItem('Golong-stay-time-info')

  • onHide当中的函数,清除定时,计算一次 ((performance.now() - startTime) / 1000).toFixed(2),与其他数据进行整合,存储本地localStorage.setItem('Golong-stay-time-info'),获取该值之后进行Golong.send()发送,清除本地缓存。

3、init中开启setCloseMark,会默认使用pageId为每个页面设置 setCloseMark,setClostMarkByPageId函数当中获取t值,为空则设置为25(表示zhuanzhuan m的域名),如果存在着[15, 16, 78, 79]的特定值(15、16代表为转转安卓端和ios端,78、79为找靓机安卓端和ios端)设置window.ZZAPP['setCloseMark']({ mark: pageId })

4、GolongInit初始化挂载各个函数,如setUserBackup,统一参数放置到window[zz_Golong_backup]当中.

GolongPagelife 页面生命周期

拥有四个周期函数onShow 、onHide、onLoad、onUnload

周期函数执行函数队列onShowFns、onHideFns、onLoadFns、onUnloadFns

三个参数,type, href, time,当触发相应的type之后,会依次执行对应的Fns队列当中的函数。

利用EVENTTYPE定义自定义事件,监听页面的history,主要为监听popstate pushState replaceState事件,监听document.hidden触发自定义事件,pageshow、pagehide事件,和页面现有的load,unload,hashchange等事件,就会对应触发生命周期hooks函数,执行相应的队列。

type EVENTTYPE =
  | 'visibilitychange'
  | 'pageshow'
  | 'replaceState'
  | 'pushState'
  | 'popstate'
  | 'pagehide'
  | 'beforeunload'
  
  //事件类型,相应的值发生变化,触发对应的周期函数
      switch (evt.type as EVENTTYPE) {
      case 'pageshow':
        if (!this.supportNative || !this.zzJsSdk) {
          Logger.info('Golong生命周期执行onLoad')
          PageLife.onLoad()
          Logger.info('Golong生命周期执行onShow')
          PageLife.onShow()
        }
        break
      case 'visibilitychange':
        if (!this.supportNative || !this.zzJsSdk) {
          if (document.visibilityState === 'visible') {
            Logger.info('Golong生命周期执行onShow')
            PageLife.onShow()
          } else {
            Logger.info('Golong生命周期执行onHide')
            PageLife.onHide()
          }
        }

        break
      case 'pagehide':
        if (!this.supportNative || !this.zzJsSdk) {
          if (!isiOS()) {
            Logger.info('Golong生命周期执行onHide')
            PageLife.onHide()
            Logger.info('Golong生命周期执行onUnload')
            PageLife.onUnload()
          }
        }
        break
      case 'beforeunload':
        if (!this.supportNative || !this.zzJsSdk) {
          if (isiOS()) {
            Logger.info('Golong生命周期执行onHide')
            PageLife.onHide()
            Logger.info('Golong生命周期执行onUnload')
            PageLife.onUnload()
          }
        }
      case 'popstate':
      case 'pushState':
      case 'replaceState':
        this.wrapHandle((from, to) => {
          if (from != to) {
            Logger.info('Golong生命周期执行onHide')
            PageLife.onHide()
            Logger.info('Golong生命周期执行onLoad')
            PageLife.onLoad()
            Logger.info('Golong生命周期执行onShow')
            PageLife.onShow()
          }
        })
        break
    }
    
   addEventListener(eventType, this.handleEvents, true))

当路由变更,hash变更,或者页面的hidden由false变为true,都会触发页面展现的钩子函数,执行相应队列中的方法

Page Visibility API

Page Visibility API,监听⻚面的展示/隐藏,⻚面展示的时候则向cookie中追加一个记录。它拥有以下属性:

  • document.visibilityState: 只读属性,表示当前页面的可见状态。它具有以下几个值:

    • "hidden": 页面不可见,即页面在后台或最小化状态。

    • "visible": 页面完全可见。

    • "prerender": 页面正在渲染,但不可见,例如在预渲染阶段。

  • document.hidden: 这是一个只读属性,用于指示当前页面是否可见,表示⻚面是否隐藏。这可能意味着⻚面在后台标签⻚或浏览器中被最小化了。这个值是为了向后兼容才继续被浏览器支持的,应该优先使用 document.visibilityState 检测⻚面可⻅性。如果页面不在用户的视野内,它的值为 true,否则为 false

  • visibilitychange 事件:当页面的可见状态发生变化时,就会触发这个事件。可以通过监听这个事件来执行相应的操作。

GolongPerf 性能

Golong-perf里边主要用到以下Web API

Performance

Performance 是一个与性能相关的API,可以获取到当前页面中与性能相关的信息,它提供了许多用于测量和分析网页性能的关键 API。以下是一些重要的 Performance 接口的 API:

  • performance.timing: 这个属性提供了一个包含了各种关键时间点的 PerformanceTiming 对象,可以用来测量网页加载的不同阶段,如导航开始、资源加载、DOM 解析等。

  • performance.now() : 这个方法返回一个高精度的时间戳,用于测量性能。它适用于执行时间间隔的测量,比如代码执行时间。

  • performance.mark()performance.measure() : 这些方法用于在代码中插入标记点,以便测量不同代码块之间的性能差异。可以使用 performance.mark() 创建标记,然后使用 performance.measure() 来测量两个标记之间的时间间隔。

  • performance.getEntries()performance.getEntriesByType() : 这些方法用于获取各种性能条目(如资源、导航等)的列表。您可以使用这些方法来分析特定类型的性能数据。

  • performance.navigation: 这个属性提供了导航类型的信息,帮助您确定是新导航还是页面刷新。

  • performance.memory: 这个属性提供了有关内存使用情况的信息。它返回一个 PerformanceMemory 对象,包含了内存使用的统计数据。

Performance.timing

Performance.timing对象包含了延迟相关的性能信息,window.performance.timing 对象提供了许多属性,用于访问有关网页加载和性能的时间信息。以下是这些属性的列表:

  1. navigationStart: 导航开始的时间戳,即浏览器开始获取当前文档的时间。

  2. unloadEventStart: 前一个文档的 unload 事件开始时间。

  3. unloadEventEnd: 前一个文档的 unload 事件结束时间。

  4. redirectStart: 重定向开始的时间戳,即从前一个文档到当前文档的重定向开始时间。

  5. redirectEnd: 重定向结束的时间戳,即从前一个文档到当前文档的重定向结束时间。

  6. fetchStart: 开始获取文档的时间戳,通常是发起请求的时间。

  7. domainLookupStart: 域名解析开始的时间戳,即浏览器开始解析域名的时间。

  8. domainLookupEnd: 域名解析结束的时间戳,即浏览器完成域名解析的时间。

  9. connectStart: 开始建立连接的时间戳,即浏览器开始建立网络连接的时间。

  10. connectEnd: 完成网络连接的时间戳,即浏览器完成网络连接的时间。

  11. secureConnectionStart: 安全连接开始的时间戳,用于HTTPS连接。

  12. requestStart: 开始发送请求的时间戳,即浏览器开始向服务器发送请求的时间。

  13. responseStart: 开始接收响应的时间戳,即浏览器开始接收服务器响应的时间。

  14. responseEnd: 响应结束的时间戳,即浏览器完成接收服务器响应的时间。

  15. domLoading: 开始解析文档对象模型(DOM)的时间戳。

  16. domInteractive: DOM 变为可交互的时间戳,即用户可以与页面进行交互的时间。

  17. domContentLoadedEventStart: DOMContentLoaded 事件开始的时间戳。

  18. domContentLoadedEventEnd: DOMContentLoaded 事件结束的时间戳。

  19. domComplete: DOM 解析完成的时间戳。

  20. loadEventStart: 加载事件开始的时间戳,即文档加载事件开始的时间。

  21. loadEventEnd: 加载事件结束的时间戳,即文档加载事件结束的时间。

通过这些属性,我们了解页面加载和渲染的各个阶段,从而优化页面性能和用户体验。而这些属性值都是时间戳,以毫秒为单位。

performance.getEntries()

Performance.getEntries(FilterOptions),对于给定的filter,此方法返回 PerformanceEntry对象数组,PerformanceEntry对象代表了 performance 时间列表中的单个 metric 数据,metric(MaiTreeKe)可以理解为基本单元,一条记录。手动构建 mark或者measure生成Performance entries, 在资源加载的时候,也会被动生成(例如图片、script、css等资源加载)

通过 performance.getEntries() 获取各个资源请求的 PerformanceEntry 对象,统计耗时 performanceEntries 是指浏览器性能API中的 PerformanceEntry 对象数组。这个数组包含了在页面生命周期中收集的不同类型的性能数据,比如资源加载时间、导航信息、用户交互延迟等等。

这些数据是通过 Performance API 提供的接口,如 performance.getEntries()performance.getEntriesByType(type) 来获取的。在这个返回的数组中,每个 PerformanceEntry 对象都代表了特定的性能指标或事件。不同类型的性能指标(如资源加载、用户交互等)会有不同的属性。

常见的 PerformanceEntry 对象属性包括:

  • name: 标识资源或事件的名称。

  • entryType: 表示性能条目的类型,如 "resource"、"navigation" 等。

  • startTime: 开始时间,表示性能事件的开始时间。

  • duration: 持续时间,表示性能事件的时长。

  • initiatorType: 发起者类型,用于资源加载事件,如 "script"、"img" 等。

  • 其他类型特定的属性,如 decodedBodySizetransferSize 等,用于描述资源加载事件。

通过遍历 performanceEntries 数组,可以获取页面中不同类型的性能数据,进而进行分析、优化和监控。这对于了解页面性能并针对性地进行改进非常有帮助。

PerformanceObserver

PerformanceObserver 是 Web Performance API 中的一个接口,用于监听和收集与页面性能相关的信息,如资源加载、绘制事件等。它提供了一种异步的方式来观察性能数据的变化,从而帮助开发人员分析和优化网页性能。

通过使用 PerformanceObserver,开发人员可以监听不同类型的性能事件,以及收集有关页面加载、渲染、资源使用等方面的数据。PerformanceObserver 提供了灵活的机制,使开发人员能够根据需求选择要观察的事件类型,并在事件发生时执行相应的回调函数。

// 创建一个 PerformanceObserver 实例
const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  entries.forEach(entry => {
    if (entry.entryType === 'resource') {
      console.log('Resource:', entry.name);
      console.log('Start Time:', entry.startTime);
      console.log('Duration:', entry.duration);
    }
  });
});

// 启动监听 resource 类型的事件
observer.observe({ entryTypes: ['resource'] });

在这个示例中,PerformanceObserver 被用于监听资源加载事件。当资源加载时,回调函数会被触发,然后可以从事件中获取有关资源的信息,如资源名称、开始时间和持续时间等。

以下是一些可以被 PerformanceObserver 监听的事件类型:

  • paint: 用于监听绘制事件,如 first-paint(第一个像素绘制事件)和 first-contentful-paint(第一个有意义内容绘制事件)。

  • resource: 用于监听资源加载事件,可以获得关于资源加载时间、资源类型等的信息。

  • navigation: 用于监听页面导航事件,如导航开始、导航完成等。

  • element: 用于监听元素渲染事件,可以获得关于元素在页面中的渲染时间等信息。

  • longtask: 用于监听长任务事件,即在主线程上运行时间较长的任务,以帮助识别页面中的性能问题。

  • measure: 用于监听自定义测量事件,可以在代码中插入标记点,然后通过监听 measure 事件来测量两个标记之间的时间间隔。

以下术语都与前端性能衡量指标和性能度量有关,它们在网页加载和交互时用于评估用户体验。需要通过PerformanceObserver监听对应的事件来获取,以下是这些术语的解释:

  1. FMP (First Meaningful Paint) :首次有意义绘制,是指页面加载过程中,浏览器第一次渲染了用户实际可见的内容,使用户能够感知到页面的加载进度。FMP 反映了用户首次感受到页面正在加载的时间点。PerformanceObserver监听paint事件,在事件回调函数中找到 first-contentful-paint 条目,并输出其开始时间+持续时间,即 firstMeaningfulPaint 时间。

  2. LCP (Largest Contentful Paint) :最大内容绘制,是指页面加载过程中,最大且有意义的内容绘制完成的时间。通常是页面上最大的图片、视频、文本块等。LCP 反映了用户感知的加载速度,越快越好。Performance API 中 largestContentfulPaint 属性的值,代表页面上最大且有意义的内容绘制完成的时间点。

  3. First Paint Time:首次绘制时间,是指浏览器第一次在屏幕上绘制像素的时间点。它不一定是有意义的内容,只是开始渲染的时间。

  4. FP (First Paint) :首次绘制,指浏览器第一次在屏幕上绘制任何内容,不一定有意义。它通常与页面加载速度有关。

  5. FCP (First Contentful Paint) :首次有内容绘制,是指页面加载过程中,浏览器第一次在屏幕上绘制了任何有意义的内容,可以是文本、图片等。FCP 也反映了用户首次感受到页面正在加载的时间点。Performance API 中 firstContentfulPaint 属性的值,代表页面上首次出现有意义的内容绘制的时间点。

  6. FID (First Input Delay) :首次输入延迟,是指从用户首次与页面交互(如点击链接、按钮等)到浏览器实际响应该交互的时间间隔。FID 反映了页面交互的响应速度,越短越好。它通常通过 Performance API 中的 firstInputDelay 属性来获取。

  7. CLS (Cumulative Layout Shift) :累计布局位移,是指页面加载过程中,不断变化的内容导致元素位置发生变化的总和。CLS 反映了页面在加载过程中的稳定性,避免元素突然移动。

以下是获取部分时间的示例:

if ('PerformanceObserver' in window) {
  const observer = new PerformanceObserver((list) => {
  //监听paint事件
    const paintEntries = list.getEntriesByType('paint');
    
    const fcpEntry = paintEntries.find(entry => entry.name === 'first-contentful-paint'); 
    const lcpEntry = paintEntries.find(entry => entry.name === 'largest-contentful-paint');

    if (fcpEntry) {
      const fcpTimestamp = fcpEntry.startTime;//fcp时间
    }

    if (lcpEntry) {
      const lcpTimestamp = lcpEntry.startTime;//lcp时间
    }
    
    if (fcpEntry) { 
    const fmpTimestamp = fcpEntry.startTime + fcpEntry.duration;//开始加上持续时间为fmp时间
    }
  });

  observer.observe({ entryTypes: ['paint'] });
}

在不同的浏览器当中,可能对性能事件和指标的支持有所不同,所以在使用这些API的时候需要考虑浏览器的兼容性。

回归代码

在对应的代码当中, 统计页面性能--> Golong-perf-perfomance perf.ts, 资源性能数据--> Golong-perf-perfomance resource.ts

预渲染页参数特殊处理: setPrerenderData(params, store)白屏参数设置默认值(domReady ssr 的首屏时间) 如果没有计算首屏时间,则给默认值 100ms(完全加载时间)。

以下是利用perfomance API计算的时间节点:

const ptiming = performance.timing
  // 白屏时间
  result.blankTime = fix(ptiming.responseStart - ptiming.navigationStart)
  // 浏览器发起请求的时间
  result.fStartTime = fix(ptiming.fetchStart - ptiming.navigationStart)
  // connect开始
  result.cStartTime = fix(ptiming.connectStart - ptiming.navigationStart)
  // connect结束
  result.cEndTime = fix(ptiming.connectEnd - ptiming.navigationStart)
  // dns开始时间
  result.dnsStartTime = fix(ptiming.domainLookupStart - ptiming.navigationStart)
  // dns结束时间
  result.dnsEndTime = fix(ptiming.domainLookupEnd - ptiming.navigationStart)
  ......
}

fmp.ts 计算fmp

FMP(first meaning paint)首次有效绘制时间,提供了在加载和使用当前页面期间发生的各种事件的性能计时信息,这也是页面性能一大依据。浏览器第一次渲染了用户实际可见的内容,使用户能够感知到页面的加载进度,FMP 反映了用户首次感受到页面正在加载的时间点。

获取取FMP,其实就是一个页面加载的内容所需的耗时,如何确定页面已经处于完成状态?用户可以看见的展示信息的元素都已稳定呈现,在这里通过 Mutation Observer 和 calculateScore 函数进行实现

初始化 Mutation Observer

initObserver,初始化 Mutation Observer,监听document文档下的所有dom结构变化,存在document.body,则触发calculateScore,并将该节点的Date.now() - performance.timing.fetchStart和score作为对象的属性,保存在SCORE_ITEMS数组中。

var observerOptions = {
  childList: true,  // 观察目标子节点的变化,是否有添加或者删除
  attributes: true, // 观察属性变动
  subtree: true     // 观察后代节点,默认为 false
}
calculateScore具体执行
// DOM结构计算score
const calculateScore = (el: any, tiers?: number, parentScore?: boolean) => {
  try {
    let score = 0
    const tagName = el.tagName
    //不是script style meta head标签
    if ('SCRIPT' !== tagName && 'STYLE' !== tagName && 'META' !== tagName && 'HEAD' !== tagName) {
      const childrenLen = el.children ? el.children.length : 0 
      //计算当前子节点数量
      if (childrenLen > 0) { //遍历子节点  有一个节点tiers就加一
        for (let child = el.children, len = childrenLen - 1; len >= 0; len--)        {
          score += calculateScore(child[len], tiers + 1, score > 0)
          //递归计算总分
        }
      }
      if (score <= 0 && !parentScore) {
        if (!(el.getBoundingClientRect && el.getBoundingClientRect().top < WH())) return 0
      }
      score += 1 + 0.5 * tiers //权重 父节点为1,子节点为0.5 当前节点的一个分数 
    }
    return score
  } catch (error) {}
}

先递归再进行是否在视口内的判断,这样每一个子元素都可以判断到,防止父元素不在视口内但子元素通过定位定位到了视口内的情况

calFinallyScore结算 dom结构变化就重新计算。而触发最终计算,需要当所有资源加载完毕,即 readyState === complete直接触发,或者监听window.load 事件、window.beforeunload事件来进行

  1. 判断DOM的变化次数是否大于4次,即SCORE_ITEMS 元素是否超过四个,如果是的话,就找出最后一次变化时间,判断当前的时间与最后变化的元素的时间节点时间差是否大于固定的2倍时间间隔,即当前时间 - 最后一个 SCORE_ITEMS 元素渲染时间差 > 2 * 500(即两次 calFinallyScore 执行时间)如果是的话,就代表,元素不再变化的时间点,Fmp 已经完成。

  2. 判断如果 DOM 变化次数超过10次,即SCORE_ITEMS 元素超过10个,并且当前页面window.performance.timing.loadEventEnd !== 0,loadEventEnd属性具体表示当前文档加载事件完成的时间戳。加载事件是指所有网页资源(如图像、脚本、样式表)都已加载完成,文档被视为完全加载的时间点),即表示load 结束。SCORE_ITEMS 最后一个元素的分数为倒数第9个元素分数,说明load 结束并且 dom 不再变化,而最后一次 和 第一次的分数相同,表示该DOM结构没有发生变化。

上述条件表示fmp已经结束,之后取消 MutationObserver,this.observer.disconnect()

根据SCORE_ITEMS数组当中的数据,过滤掉 SCORE_ITEMS[n] < SCORE_ITEMS[n-1] 的元素,因为这些数据与最大差值无关。

计算出分数变化最大时的时间点,差值最大即为fmp时间节点,大于10s会自动被过滤。

大致流程

通过监听dom节点变化,记录每一次变化的时间节点,触发计算机制,得出当前dom计算出来的分数,通过监听页面 load,beforeunload等事件,来触发最终计算fmp的时机,即calFinallScore函数

通过判断dom的变化次数,判断时间间隔,以及对比分数的变化情况来判断是否已经可以来计算FMP。

  • 如果不通过,就间隔500ms再来判断。

  • 如果通过,就开始最终计算。

  • 遍历所有的时间节点,寻找分数差值最大的时间节点,即为fmp的时间节点

  • 然后再获取页面中所有图片的加载时长,计算出最后一张图片的加载时间,即(performance.getEntriesByName(element)[0] as PerformanceResourceTiming) ?.responseEnd

  • 最后通过 setPerformance 方法设置到 perf 上面去。

GolongExposure GolongAdExposure

只讲核心部分,原理与IntersectionObserver这个api类似,在埋点上报之前都需要做一个去重的处理,上报过的不再进行上报。

// IntersectionObserver属性的处理
const ISObserver = () => {
  observer = new IntersectionObserver(
    (entries) => {
      let maxMetric = ''
      entries.forEach((entry) => {
        if (entry.intersectionRatio && entry.intersectionRatio > 0) {
          maxMetric = entry.target.getAttribute('data-ad-ticket')
          // send Golong
          if (maxMetric) sendGolongLog(maxMetric)
        }
      })
    },
    {
      threshold: 0.5
    }
  )
  targetArr.forEach(function(item) {
    observer.observe(item)
  })
}
// inviewport判断取可视区的最大值
const ExposureCollector = () => {
  let maxMetric = ''
  for (let i = 0; i < targetArr.length; i++) {
    if (inviewport(targetArr[i], height, width)) {
      maxMetric = targetArr[i].getAttribute('data-ad-ticket')
      // send Golong
      if (maxMetric) sendGolongLog(maxMetric)
    }
  }
}

inviewport即借助getBoundingClientRect对象原理实现的

通过监听用户的滚动事件,观察用户是否进行下滚动,下滚动才进行上报,该函数还使用了防抖操作

// 监听页面滚动上报  使用防抖
const exposureDefault = (() => {
  let initFlag = false
  return () => {
    // 判断上下滚动
    let startScrollTop = 0
    if (initFlag) return
    initFlag = true
    //监听滚动事件
    window.addEventListener(
      'scroll',
      (e) => {
        e.stopPropagation()
        const nextScrollTop =
          document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop
        // 下滚动才发埋点
        if (nextScrollTop - startScrollTop > 0) {
          try {
            act({})
          } catch (err) {}
        }
        startScrollTop = nextScrollTop
      },
      false
    )
  }
})()

GolongAreaExposure

面积曝光,观察对象接触即触发,原理与IntersectionObserverEntry和IntersectionObserver对象类似。

// 元素出现在可视区域时的操作逻辑
const observerLogic = (entries: IntersectionObserverEntry[]) => {
  // console.log('entries', entries)
  const ctx: {
    metric?: string
    isdot?: string
    backupParam?: string
    uid?: string
    backupParamObj?: Backup
  } = {}
  entries.forEach((entry) => {
    // 元素至少50%可见   解构entry,赋值给ctx对象   交叉比例过0
    if (entry.intersectionRatio && entry.intersectionRatio > 0) {
      ctx.metric =
        entry.target.getAttribute(AREA_ATTR_NAME) || entry.target.getAttribute(SECTION_ID_NAME) // 可见元素的指标
      ctx.isdot = entry.target.getAttribute(DATA_ISDOT) // 是否是dot
      ctx.backupParam =
        entry.target.getAttribute(BACK_UP_NAME) || entry.target.getAttribute(ZZ_BACKUP) || '{}' // 备份参数
      ctx.uid = ctx.metric + ctx.backupParam // 唯一标识
    }
  })
}

GolongGoodsExposure

需要维护所有 MutationObserver 实例,防止重复监听出现。

监听元素相交即push,上报

ioPromise.then(() => {
    const newISObserver = new IntersectionObserver(
      (entries) => {
        // 元素出现在可视区的操作逻辑
        let goodInfo = {}
        entries.forEach((entry) => {
          // 可见元素的可见比例大于0
          if (entry.intersectionRatio && entry.intersectionRatio > 0) {
            goodInfo = entry.target.getAttribute(GOODS_EXPOSURE_NAME)
            if (!goodInfo) return
            try {
              goodInfo = JSON.parse(goodInfo as string)
            } catch (e) {
              goodInfo = {}
            }
            const old = cachedGoodInfo.get(targetContainer)
            if (!old) {
              cachedGoodInfo.set(params.targetContainer, [goodInfo])
            } else {
              old.push(goodInfo)
            }

            sendGoodsExposure(params, callback)()
          }
        })
      },
      {
        threshold: 0.5 // 元素50%显示时,即认为出现在了可视区
      }
    )

    targetGoods.forEach((item) => newISObserver.observe(item))

    ISObserverMap.set(targetContainer, newISObserver)
  })

原文作者: 原野风殇 原文地址: https://juejin.cn/post/7265891728163667983

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐