目录
前言:
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.根据接口下发分页数据估算可见元素;2.监听滚动视图的滚动事件,实时计算元素相对位置;3. 利用浏览器(或其他平台如小程序、Taro)标准 API 监听元素与可见区域的相交变化。下面分别介绍一下这三种方法的具体原理、适用范围及优缺点。
实现思路:长列表的数据往往通过分页接口进行加载,可以利用这一特性,以单页数据返回的维度粗略估算元素的可见性,具体说就是以每一次的接口返回的数据当做当前可见的元素的列表;
优点:
缺点:
由于缺点很明显,误差太大,现在很少有人这么来实现曝光埋点,但是在很多精度要求不高的场景或者年代很久的代码中还能看到这种实现方式
实现思路:监听长列表(或滚动视图容器)的滚动事件,通过平台 UI 基础接口(如浏览器 DOM 接口 getBoundingClientRect)实时获取元素坐标(包括位置和大小信息等),并计算同可视区域的相对状态(是否有重叠)来判定元素是否 “可见”;
优点:
缺点:
这种方式虽然计算量大、逻辑复杂、性能较差(当然也可以进行一些性能上的优化,代价是代码复杂度变的更高,不利于后续更新维护),但是计算结果是准确的,在没有出现方法三中的 Web 端标准接口(2016)之前,在计算精度要求严格的场景下,这视乎是唯一的选择;
优点:
缺点:
基于以上,这种方式是目前最推荐的一种实现元素曝光监听的方式,具体怎么用呢,下面对 H5、小程序、Taro 的使用场景分别来介绍一下。
简单来说,利用 Intersection Observer API 来进行视图元素的可见性观察主要分成这么几个步骤:创建观察者、对目标元素添加观察、处理观察结果(回调)、停止观察;
首先我们需要创建一个观察者IntersectionObserver
,用于监听目标元素相对于根视图(可以是父视图或当前窗口)的相交状态变化情况(即元素的 “可见” 状态)。具体创建方式是利用 Web 标准 API:IntersectionObserver
构造方法,具体代码如下:
let observer = new IntersectionObserver(callback, options);
其中callback
就是当监听到元素位置变化时触发的回调方法,具体定义及用法会在第三步处理观察结果中具体介绍;
options
是定制观察模式的参数,参数定义如下
interface IntersectionObserverInit {
root?: Element | Document | null;
rootMargin?: string;
threshold?: number | number[];
}
callback
);可以是单一数值(number)也可以是一组数值;例如当设置为 0.25 时,只有当相交达到 0.25 时(增大到 0.25 或减小到 0.25 都会触发)才会触发回调;如果是一组数值的话,相交比例达到其中任意值时也都会触发回调;(备注:除此外,元素首次添加观察时也会触发一次回调,不论是否达到阈值)例如上图中的 threshold 设置状态,每当元素滑动到虚线位置与父视图边界相交时就会触发回调
有了观察者后,就可以对目标元素进行观察了,具体代码如下:
let target = document.querySelector('#listItem');
observer.observe(target);
需要注意添加观察的时机,要保证在目标元素创建好以后再添加观察;如果是动态创建的元素(例如分页加载数据),需要在每次创建完元素后再次对新增的元素添加观察;
当被观察的目标元素与参照视图(root)相交的比例达到设置的阈值时,就会触发我们注册的回调方法(callback
),回调方法的定义如下:
interface IntersectionObserverCallback {
(entries: IntersectionObserverEntry[], observer: IntersectionObserver): void;
}
可以看到该回调方法内可以接收到两个参数:
entries
:IntersectionObserverEntry 的数组,是相交状态发生变化的元素的集合,每个 IntersectionObserverEntry 对象内有 6 个属性:
observer:当前观察者;
有了这些信息,就可以轻松监测目标元素的可见状态变化,方面进行后续的埋点上报、数据记录、延迟加载等各种处理;
注册的回调函数将会在主线程中被执行,所以该函数执行速度要尽可能的快。如果有一些耗时的操作需要执行,建议使用 Window.requestIdleCallback() 方法。
如果需要停止观察,可以在合适的时间解除对某个元素的观察或终止对所有目标元素的观察;
// 停止观察某个目标元素
observer.unobserve(target)
// 终止对所有目标元素可见性变化的观察,关闭监视器
observer.disconnect()
需要注意的是在 Intersection Observer API 的 V2 版本新增了一个 isVisible
属性(新版 Chrome 浏览器已经支持,Safari 等其他浏览器内不支持),用来标识元素是否 “可见”(因为即使元素在可视区域内,也有肯能因为被其他元素遮挡、样式属性 hiden 等影响导致元素不能被看到);官方说明中,为了保证性能,这个字段的值不一定是准确的,除非特殊场景,不建议使用这个字段,大部分场景isIntersecting
就够了;
浏览器 | 平台 | 支持的版本 | 发布时间 |
---|---|---|---|
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 或其他方式来兼容低版本浏览器;
同 Web 端接口类似,微信小程序提供了对应的小程序版本 API 接口,功能同 web 端的 Intersection Observer API 类似,使用方式也基本相同,只是部分细节存在差异;具体步骤:
通过微信小程序框架提供的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 端基本一致,但也存在差异:
IntersectionObserver.disconnect()
停止监听。回调函数将不再触发
if (this._observer) this._observer.disconnect()
注意:在组件内,如果在 attached 组件生命周期函数内添加内部子元素的相交变化观察可能无法监听成功,原因是此时组件布局还未完成,组件内节点未完成创建;
Taro 内也提供了对应的 IntersectionObserver 的 API,其 API 的定义及使用方式基本是同微信小程序端对齐的;Taro 本身是支持 H5、小程序等多端的,其 IntersectionObserver 接口内部对 H5、微信小程序、京东小程序等各平台进行了对齐抹平,具体来说在 H5 端是按照微信小程序端的格式进行的封装,其内部实现是调用的 Web 端的 Intersection Observer API,在小程序端由于标准对齐,基本上就是桥接对应平台小程序原生的接口;
由于接口定义及使用方式同微信小程序对齐,这里就不再赘述 Taro 端的具体使用方式,需要说明的是由于 Taro 框架的特殊性(相比小程序原生方式多了一层),在用 Taro 进行小程序端滑动曝光监听开发时,有几个容易出错或需要特殊处理的点:
由于 Taro 运行时机制,在 Taro 组件的数据更新方法(例如 setState)执行后立刻添加监听可能会不生效,原因是对应的由数据驱动的小程序元素实例此时还未完成创建或挂载,需要添加延迟或在 Taro.nextTick 回调内执行(Taro 最新版本已经默认将 observe 方法添加到 Taro.nextTick 内执行);如果遇到添加监听不生效的情况,可以尝试这个方法;
Taro.nextTick(() => {
//将监听添加时机(延迟作到下一个时间片再执行),解决监听添加时元素尚未创建导致的监听无效问题
expoObjserver.relativeTo('.clpScrollView').observe('.coupon-item', result => {
console.log(result.intersectionRatio);
//...
});
});
在创建 observer 时需要传入小程序的页面或者组件实例,而在 Taro 组件或页面内直接使用 this 获取的是 Taro 层的页面或组件的实例,两者是不同的;
那么如何获取小程序层的组件实例呢:
Taro.getCurrentInstance().page
获取小程序页面的实例;props.$scope
获取到小程序的自定义组件对象实例如果创建及设置正确,随着列表的滑动或其他元素的位置变化,对应的回调方法应该会被触发,在回调方法内我们需要接收回调的入参数并进行处理(例如上报相关业务信息)。根据 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
插件,向子元素内注入一些通用属性;实际验证发现,利用插件插入后回调的 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 官方文档关于 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 内列表滑动元素曝光埋点搞定~
作为一位过来人也是希望大家少走一些弯路
在这里我给大家分享一些自动化测试前进之路的必须品,希望能对你带来帮助。
(软件测试相关资料,自动化测试相关资料,技术问题答疑等等)
相信能使你更好的进步!
点击下方小卡片