一文帮你搞定 H5、小程序、Taro 长列表曝光埋点

目录

前言:

1. 监听列表内元素曝光的常见方法

1. 1 方式一:根据接口下发分页数据估算可见元素

1. 2 方式二:监听滚动事件,实时计算元素相对位置

1. 3 方式三:利用浏览器标准 API 监听元素可视区变化

2. 列表内元素曝光事件监听的具体实现

2. 1 Web(H5)端

2. 1. 1 具体使用方法:

第一步:创建一个观察者(IntersectionObserver)

第二步:对目标元素添加观察

第三步:处理观察结果

第四部:停止观察

2. 1. 2 Tips

2. 1. 3 Intersection Observer API 的浏览器支持情况

2. 2 小程序端(微信小程序)

第一步:创建一个观察者(IntersectionObserver)

第二步:指定参照节点(参照区域)

第三步:开启观察

第四步:处理回调

第五步:停止监听

Tips

2. 3 Taro 框架内(Taro3+React)

1. 监听不生效的问题

2. 创建 Observer 需传入原生组件实例

3. 回调方法内如何获取目标元素的其他信息?

方案一:taro-plugin-inject 方案

方案二:访问 Taro 虚拟 DOM


前言:

 H5、小程序和Taro是当前流行的前端开发框架,它们都支持在移动端构建应用程序。在这些应用程序中,长列表曝光埋点是一个常见的需求,用于统计用户在列表中浏览的数据。

1. 监听列表内元素曝光的常见方法

长列表(或滚动视图)中元素的曝光埋点,关键是如何监听子元素的 “曝光” 事件。“曝光” 即元素进入到了屏幕的可见区域,也就是能被用户看到了,这是人类的直观视觉感受,那么如何用代码的方式来判定呢?目前大概有这么三种方法:1.根据接口下发分页数据估算可见元素;2.监听滚动视图的滚动事件,实时计算元素相对位置;3. 利用浏览器(或其他平台如小程序、Taro)标准 API 监听元素与可见区域的相交变化。下面分别介绍一下这三种方法的具体原理、适用范围及优缺点。

1. 1 方式一:根据接口下发分页数据估算可见元素

实现思路:长列表的数据往往通过分页接口进行加载,可以利用这一特性,以单页数据返回的维度粗略估算元素的可见性,具体说就是以每一次的接口返回的数据当做当前可见的元素的列表;

优点:

  • 这种方式的好处是简单:仅仅根据分页接口每次请求的数据进行元素曝光的判断,计算很简单;

缺点:

  • 缺点就是误差太大:一方面分页接口单次请求的数据也往往会超出一屏,另一方面列表内元素的高度可能也是不同的、分页返回的数据条数也可能存在差异,这种方式来计算元素的曝光误差太大;

由于缺点很明显,误差太大,现在很少有人这么来实现曝光埋点,但是在很多精度要求不高的场景或者年代很久的代码中还能看到这种实现方式

1. 2 方式二:监听滚动事件,实时计算元素相对位置

实现思路:监听长列表(或滚动视图容器)的滚动事件,通过平台 UI 基础接口(如浏览器 DOM 接口 getBoundingClientRect)实时获取元素坐标(包括位置和大小信息等),并计算同可视区域的相对状态(是否有重叠)来判定元素是否 “可见”;

优点:

  • 相比方式一,精度有了很大的改进,如果计算的方式正确,计算结果可以说是准确的;
  • 另外由于使用的是平台内的通用基础能力接口,兼容性较好;

缺点:

  • 计算量大,性能损耗严重:这种计算方式需要监听滚动视图的滚动事件,在滚动回调事件内实时进行列表内所有元素的位置坐标计算(获取所有元素的位置并同当前可见区域进行对比),这样带来的计算量是相当大的,往往会造成页面的性能问题(如滑动卡顿);
  • 代码分散、逻辑复杂:除了需要监听滚动视图的滚动事件,还要在首屏数据加载或者数据刷新时,额外进行一次计算,整体复杂度及对页面的性能影响都比较大;
  • 其他问题:可能引发其他额外操作,如在 H5 中 getBoundingClientRect() 的频繁调用也可能引发浏览器的样式重计算和布局; iframe 里,无法直接访问内部元素等等;

这种方式虽然计算量大、逻辑复杂、性能较差(当然也可以进行一些性能上的优化,代价是代码复杂度变的更高,不利于后续更新维护),但是计算结果是准确的,在没有出现方法三中的 Web 端标准接口(2016)之前,在计算精度要求严格的场景下,这视乎是唯一的选择;

1. 3 方式三:利用浏览器标准 API 监听元素可视区变化

优点:

  • 性能更高: 浏览器底层实现,并进行了相应优化,性能没有问题:监听不会在主线程进行(只要回调方法会在主线程触发)
  • 计算量小:这里的计算量小是指的我们 web 开发者需要进行的计算操作,因为大部分的计算浏览器 API 内已经帮我们计算好了,我们只需要根据需求场景在此基础上进行简单的处理即可满足需求;
  • 计算更结果准确:浏览器 API 实现的计算结果是比较准确的,这块毋庸置疑;
  • 代码更优雅:大部分的监听、计算逻辑都在 API 内部实现了,开发者的代码量不会太多太复杂,代码更简洁从而更利用后续维护;

缺点:

  • 需要新浏览器支持(根据文档描述的浏览器兼容情况其实已经满足绝大多数的使用场景),太低版本的浏览器不支持,如果需要兼容,也有办法,通过官方提供的 polyfill 可以解决(引入 polyfill,当然不可避免的带来代码体积的增量,项目中实测打包后 js 文件体积增大了 8kb,这算是唯一的缺点吧);(w3c 官方提供了对应 polyfill)

基于以上,这种方式是目前最推荐的一种实现元素曝光监听的方式,具体怎么用呢,下面对 H5、小程序、Taro 的使用场景分别来介绍一下。

2. 列表内元素曝光事件监听的具体实现

2. 1 Web(H5)端

简单来说,利用 Intersection Observer API 来进行视图元素的可见性观察主要分成这么几个步骤:创建观察者、对目标元素添加观察、处理观察结果(回调)、停止观察;

2. 1. 1 具体使用方法:

第一步:创建一个观察者(IntersectionObserver)

首先我们需要创建一个观察者IntersectionObserver ,用于监听目标元素相对于根视图(可以是父视图或当前窗口)的相交状态变化情况(即元素的 “可见” 状态)。具体创建方式是利用 Web 标准 API:IntersectionObserver构造方法,具体代码如下:

let observer = new IntersectionObserver(callback, options);
  • 其中callback 就是当监听到元素位置变化时触发的回调方法,具体定义及用法会在第三步处理观察结果中具体介绍;

  • options是定制观察模式的参数,参数定义如下

    interface IntersectionObserverInit {
        root?: Element | Document | null;
        rootMargin?: string;
        threshold?: number | number[];
    }
    
    • root: 用于指定观察的参照区域,一般是目标元素的父视图容器或整个视图窗口(必须是目标元素的父级元素。如果未指定或者为 null,则默认为浏览器视窗)
    • rootMargin:参照区域(root)的外边距,类似于 CSS 中的 margin 属性,比如 "10px 20px 30px 40px" (top, right, bottom, left),用于对参照物的区域范围进行调整(收缩或扩张);
    • threshold:相交比例阈值,用于定制需要观察的相交比例的临界值;元素的交集(相交比例)发生变化时并不是每次变化都会执行回调方法,只有当相交比例达到设置的阈值时才会触发回调(callback);可以是单一数值(number)也可以是一组数值;例如当设置为 0.25 时,只有当相交达到 0.25 时(增大到 0.25 或减小到 0.25 都会触发)才会触发回调;如果是一组数值的话,相交比例达到其中任意值时也都会触发回调;(备注:除此外,元素首次添加观察时也会触发一次回调,不论是否达到阈值)

一文帮你搞定 H5、小程序、Taro 长列表曝光埋点_第1张图片

例如上图中的 threshold 设置状态,每当元素滑动到虚线位置与父视图边界相交时就会触发回调

第二步:对目标元素添加观察

有了观察者后,就可以对目标元素进行观察了,具体代码如下:

let target = document.querySelector('#listItem');
observer.observe(target);

需要注意添加观察的时机,要保证在目标元素创建好以后再添加观察;如果是动态创建的元素(例如分页加载数据),需要在每次创建完元素后再次对新增的元素添加观察;

第三步:处理观察结果

当被观察的目标元素与参照视图(root)相交的比例达到设置的阈值时,就会触发我们注册的回调方法(callback),回调方法的定义如下:

interface IntersectionObserverCallback {
    (entries: IntersectionObserverEntry[], observer: IntersectionObserver): void;
}

可以看到该回调方法内可以接收到两个参数:

  • entries :IntersectionObserverEntry 的数组,是相交状态发生变化的元素的集合,每个 IntersectionObserverEntry 对象内有 6 个属性:

    • time:发生相交的时间戳,单位毫秒。(发生交集变化的时间相对于文档创建的时间)
    • target:被观察的目标元素,是一个 DOM 节点对象;
    • rootBounds: root 元素(参照区域)的矩形边界
    • boundingClientRect:目标元素的边界信息,边界的计算方式与 Element.getBoundingClientRect() 相同。
    • intersectionRect:目标元素同 root 元素的相交区域
    • intersectionRatio:目标元素同根元素相交的部分尺寸与目标元素整体尺寸的比值,即 intersectionRect 与 boundingClientRect 的比值;
    • isIntersecting:目标元素同根元素是否相交(根据设定的阈值判定)
  • observer:当前观察者;

有了这些信息,就可以轻松监测目标元素的可见状态变化,方面进行后续的埋点上报、数据记录、延迟加载等各种处理;

注册的回调函数将会在主线程中被执行,所以该函数执行速度要尽可能的快。如果有一些耗时的操作需要执行,建议使用 Window.requestIdleCallback() 方法。

第四部:停止观察

如果需要停止观察,可以在合适的时间解除对某个元素的观察或终止对所有目标元素的观察;

// 停止观察某个目标元素
observer.unobserve(target)

// 终止对所有目标元素可见性变化的观察,关闭监视器
observer.disconnect()

2. 1. 2 Tips

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

2. 1. 3 Intersection Observer API 的浏览器支持情况

浏览器 平台 支持的版本 发布时间
Chrome PC 51 2016-05-25
Chrome Android Android 51 2016-06-08
WebView Android Android 51 2016-06-08
Safari macOS 12.1 2019-03-25
Safari on iOS iOS 12.2 2019-03-25
Edge PC 15 2017-04-05
Firefox PC 55 2017-08-08
Firefox for Android Android 55 2017-08-08
Opera PC 38 2016-06-08
Opera Android Android 41 2016-10-25

可以根据具体使用场景(支持的浏览器版本情况)来决定是否直接使用标准 API 还是需要添加 polyfill 或其他方式来兼容低版本浏览器;

2. 2 小程序端(微信小程序)

同 Web 端接口类似,微信小程序提供了对应的小程序版本 API 接口,功能同 web 端的 Intersection Observer API 类似,使用方式也基本相同,只是部分细节存在差异;具体步骤:

第一步:创建一个观察者(IntersectionObserver)

通过微信小程序框架提供的IntersectionObserver wx.createIntersectionObserver(Object component, Object options)方法,可以方便的创建观察者;

Page({
  data: {
    appear: false
  },
  onLoad() {
    this._observer = wx.createIntersectionObserver(this,{
      thresholds: [0.2, 0.5]
    })
    //其他处理...
  },
  onUnload() {
    if (this._observer) this._observer.disconnect()
  }
})

类比 web 端的 IntersectionObserver 构造方法,不同的是小程序里这一步不需要设置回调方法,回调方法放到后面添加观察的时候注册;

入参说明:component一般需要传当前页面或组件实例;options可定义触发阈值、是否同时观测多个目标节点等信息

第二步:指定参照节点(参照区域)

不同于 web 端的创建时指定,小程序端提供了两个单独接口用于指定参照节点(参照区域)

  • IntersectionObserver IntersectionObserver.relativeTo(string selector, Object margins) :使用选择器指定一个节点,作为参照区域之一,即以该节点的布局区域作为参照区域。
  • IntersectionObserver IntersectionObserver.relativeToViewport(Object margins): 指定页面显示区域作为参照区域之一

示例:

this._observer = this._observer.relativeTo('.scroll-view')

同样可以通过margins来对参照区域进行扩展(或收缩);如果有多个参照节点,则会取它们布局区域的 交集 作为参照区域。

第三步:开启观察

通过前两步创建好观察者,设置好相关参数(触发阈值、是否多目标等)并指定参照区域后,就可以对目标元素进行观察了。这里通过选择器的方式(web 端是元素实例)来指定目标元素,同时这里需要指定相交状态变化的回调方法:IntersectionObserver.observe(string targetSelector, function callback)

示例:

this._observer.observe('.ball', (res) => {
    console.log(res);
    this.setData({
        appear: res.intersectionRatio > 0
    })
})

第四步:处理回调

当元素相对于参照区域的相交情况发生变化(同 web 端一致,触发时机由第一步创建观察者时设置的 thresholds 阈值决定)就会触发相应的回调方法。回调方法内接受的参数同 web 端基本一致,但也存在差异:

  • 小程序端是单个触发,回调方法的入参是单个元素(对比 web 端是多个一起回调,入参是变化元素的数组);
  • 小程序端入参内同时包含目标节点的节点 ID 及自定义数据;

第五步:停止监听

IntersectionObserver.disconnect()

停止监听。回调函数将不再触发

if (this._observer) this._observer.disconnect()

Tips

注意:在组件内,如果在 attached 组件生命周期函数内添加内部子元素的相交变化观察可能无法监听成功,原因是此时组件布局还未完成,组件内节点未完成创建;

2. 3 Taro 框架内(Taro3+React)

Taro 内也提供了对应的 IntersectionObserver 的 API,其 API 的定义及使用方式基本是同微信小程序端对齐的;Taro 本身是支持 H5、小程序等多端的,其 IntersectionObserver 接口内部对 H5、微信小程序、京东小程序等各平台进行了对齐抹平,具体来说在 H5 端是按照微信小程序端的格式进行的封装,其内部实现是调用的 Web 端的 Intersection Observer API,在小程序端由于标准对齐,基本上就是桥接对应平台小程序原生的接口;

由于接口定义及使用方式同微信小程序对齐,这里就不再赘述 Taro 端的具体使用方式,需要说明的是由于 Taro 框架的特殊性(相比小程序原生方式多了一层),在用 Taro 进行小程序端滑动曝光监听开发时,有几个容易出错或需要特殊处理的点:

1. 监听不生效的问题

由于 Taro 运行时机制,在 Taro 组件的数据更新方法(例如 setState)执行后立刻添加监听可能会不生效,原因是对应的由数据驱动的小程序元素实例此时还未完成创建或挂载,需要添加延迟或在 Taro.nextTick 回调内执行(Taro 最新版本已经默认将 observe 方法添加到 Taro.nextTick 内执行);如果遇到添加监听不生效的情况,可以尝试这个方法;

Taro.nextTick(() => {
    //将监听添加时机(延迟作到下一个时间片再执行),解决监听添加时元素尚未创建导致的监听无效问题
    expoObjserver.relativeTo('.clpScrollView').observe('.coupon-item', result => {
        console.log(result.intersectionRatio);
        //...
    });
});

2. 创建 Observer 需传入原生组件实例

在创建 observer 时需要传入小程序的页面或者组件实例,而在 Taro 组件或页面内直接使用 this 获取的是 Taro 层的页面或组件的实例,两者是不同的;

那么如何获取小程序层的组件实例呢:

  • 在纯 Taro 项目中可以直接使用Taro.getCurrentInstance().page获取小程序页面的实例;
  • 如果是把 Taro 组件编译为原生自定义组件的混合模式,可以通过 props.$scope 获取到小程序的自定义组件对象实例

3. 回调方法内如何获取目标元素的其他信息?

如果创建及设置正确,随着列表的滑动或其他元素的位置变化,对应的回调方法应该会被触发,在回调方法内我们需要接收回调的入参数并进行处理(例如上报相关业务信息)。根据 Taro 文档定义,回调方法的入参是ObserveCallbackResult类型:

interface ObserveCallbackResult {
      /** 目标边界 */
      boundingClientRect: BoundingClientRectResult
      /** 相交比例 */
      intersectionRatio: number
      /** 相交区域的边界 */
      intersectionRect: IntersectionRectResult
      /** 参照区域的边界 */
      relativeRect: RelativeRectResult
      /** 相交检测时的时间戳 */
      time: number
    }

可以看到其中只有 intersectionRatio、time 等位置相交的相关信息,但是却没有自定义数据字段(作为对比微信小程序提供的回调方法内除了这些还包括节点 ID、节点自定义数据属性 dataset 等信息),那么在 Taro 内如何获取目标元素上的其他数据信息呢?

实际调试发现,虽然文档中没有 id、dataset,但是实际返回值内是有这俩字段的,哈哈,看到这里是不是觉得没啥问题了,以为只是 Taro 文档跟我们开了个小小的玩笑;先别高兴的太早,虽然实际返回值里有 dataset 字段,但是不幸的是 dataset 字段始终是空的,实际返回结果(result)示例如下:

{
    "id": "_n_82",
    "dataset": {},
    "time": 1687763057268,
    "boundingClientRect": {
        "x": 89.109375,
        "y": 19,
        ...
    },
    "intersectionRatio": 1,
    "intersectionRect": {
        "x": 89.109375,
        "y": 19,
        ...
    },
    "relativeRect": {
        "x": 82.109375,
        ...
    }
}

what?why?看到这里估计大家有想砸键盘的冲动,先别着急,我们先来分析一下为什么dataset是空呢?

这是由于dataset是小程序的特殊的模版属性,主要作用是可以在事件回调的 event 对象中获取到 dataset 相关数据,Taro 对于这些能力是部分支持的,Taro 通过在逻辑层的模拟已经支持在事件回调对象中通过 event.target.dataset 或 event.currentTarget.dataset 获取到。但是由于是在逻辑层模拟实现的,并没有真正在模板设置这个属性。所以在小程序中有一些 API(如:createIntersectionObserver)获取到页面的节点的时候,是获取不到dataset的。

上一点所说的,Taro 对于小程序 dataset 的模拟是在小程序的逻辑层实现的。并没有真正在模板设置这个属性。

但在小程序中有一些 API(如:createIntersectionObserver)获取到页面的节点的时候,由于节点上实际没有对应的属性而获取不到。

 

--来自 Taro 官方文档: Taro-React-dataset

既然在回调传参中直接取值是空,那我们该怎么获取元素上的自定义数据呢?

方案一:taro-plugin-inject 方案

官方给出的解决方案是使用taro-plugin-inject插件,向子元素内注入一些通用属性;实际验证发现,利用插件插入后回调的 dataset 中确实能看到有对应的属性,但是该方法插入的属性只能是统一的固定值,无法根据实际数据动态设置属性值,因此该方案不能满足诉求。

//项目 config/index.js 中的 plugins 配置:
  plugins: [
    [
      '@tarojs/plugin-inject',
      {
        components: {
          View: {
            'data-index': "'dataIndex'",
            'data-info': "'dateInfo'"
          },
        },
      },
    ]
  ],

//实际返回值
{
    "id": "_n_82",
    "dataset": {
        "index": "dataIndex",
        "info": "dateInfo"
    },
    "time": 1687766275879,
    ...
}
方案二:访问 Taro 虚拟 DOM

根据 Taro 官方文档关于 React 框架使用差异的描述 (Taro-React-生命周期触发机制),Taro3 在小程序逻辑层上实现了一份遵循 Web 标准 BOM 和 DOM API。通过这些 API 可以获取对应的虚拟 DOM 节点 (TaroElement 对象),既然是逻辑层实现的,那么节点上应该也能看到对应的dataset信息。

那么具体如何实现呢?回调参数中虽然没有我们想要的自定义数据字段,但是可以拿到节点 id 信息,可以通过 Taro 提供的document.getElementById();API 利用节点 id 获取对应的 Taro 虚拟 DOM 节点,从该节点上拿到我们需要的dataset信息,代码如下:

Taro.nextTick(() => {
    //将监听添加时机(延迟作到下一个时间片再执行),解决监听添加时元素尚未创建导致的监听无效问题
    expoObjserver.relativeTo('.clpScrollView').observe('.coupon-item', result => {
        if (!result?.id) return;
        // !!!获取虚拟DOM节点
        const tarTaroElement = document.getElementById(result?.id);
        const dataInfo = tarTaroElement?.dataset; //拿到dataset信息
        console.log(tarTaroElement);
        console.log(dataInfo);
       //...
    });
});

至此,我们就拿到了想要的自定义数据(业务数据),后续就是根据述求随意的使用这些数据了,Taro 内列表滑动元素曝光埋点搞定~

  作为一位过来人也是希望大家少走一些弯路

在这里我给大家分享一些自动化测试前进之路的必须品,希望能对你带来帮助。

(软件测试相关资料,自动化测试相关资料,技术问题答疑等等)

相信能使你更好的进步!

点击下方小卡片

你可能感兴趣的:(软件测试工具,软件测试,自动化测试,java,linux,服务器,android,appium,小程序,自动化)