基于 IntersectionObserver 实现一个组件的曝光监控
我们在产品推广过程中,经常需要判断用户是否对某个模块感兴趣。那么就需要获取该模块的曝光量和用户对该模块的点击量,若点击量/曝光量越高,说明该模块越有吸引力。
那么如何知道模块对用户是否曝光了呢?之前我们是监听页面的滚动事件,然后通过getBoundingClientRect()
现在我们直接使用IntersectionObserver
就行了,使用起来简单方便,而且性能上也比监听滚动事件要好很多。
1. IntersectionObserver
我们先来简单了解下这个 api 的使用方法。
IntersectionObserver 有两个参数,new IntersectionObserver(callback, options)
,callback 是当触发可见性时执行的回调,options 是相关的配置。
// 初始化一个对象
const io = new IntersectionObserver(
(entries) => {
// entries是一个数组
console.log(entries);
},
{
threshold: [0, 0.5, 1], // 触发回调的节点,0表示元素刚完全不可见,1表示元素刚完全可见,0.5表示元素可见了一半等
},
);
// 监听dom对象,可以同时监听多个dom元素
io.observe(document.querySelector('.dom1'));
io.observe(document.querySelector('.dom2'));
// 取消监听dom元素
io.unobserve(document.querySelector('.dom2'));
// 关闭观察器
io.disconnect();
在 callback 中的 entries 参数是一个IntersectionObserverEntry类型的数组。
主要有 6 个元素:
{
time: 3893.92,
rootBounds: ClientRect {
bottom: 920,
height: 1024,
left: 0,
right: 1024,
top: 0,
width: 920
},
boundingClientRect: ClientRect {
// ...
},
intersectionRect: ClientRect {
// ...
},
intersectionRatio: 0.54,
target: element
}
各个属性的含义:
{
time: 触发该行为的时间戳(从打开该页面开始计时的时间戳),单位毫秒
rootBounds: 视窗的尺寸,
boundingClientRect: 被监听元素的尺寸,
intersectionRect: 被监听元素与视窗交叉区域的尺寸,
intersectionRatio: 触发该行为的比例,
target: 被监听的dom元素
}
我们利用页面可见性的特点,可以做很多事情,比如组件懒加载、无限滚动、监控组件曝光等。
2. 监控组件的曝光
我们利用IntersectionObserver
这个 api,可以很好地实现组件曝光量的统计。
实现的方式主要有两种:
- 函数的方式;
- 高阶组件的方式;
传入的参数:
interface ComExposeProps {
readonly always?: boolean; // 是否一直有效
// 曝光时的回调,若不存在always,则只执行一次
onExpose?: (dom: HTMLElement) => void;
// 曝光后又隐藏的回调,若不存在always,则只执行一次
onHide?: (dom: HTMLElement) => void;
observerOptions?: IntersectionObserverInit; // IntersectionObserver相关的配置
}
我们约定整体的曝光量大于等于 0.5,即为有效曝光。同时,我们这里暂不考虑该 api 的兼容性,若需要兼容的话,可以安装对应的 polyfill 版。
2.1 函数的实现方式
用函数的方式来实现时,需要业务侧传入真实的 dom 元素,我们才能监听。
// 一个函数只监听一个dom元素
// 当需要监听多个元素,可以循环调用exposeListener
const exposeListener = (target: HTMLElement, options?: ComExposeProps) => {
// IntersectionObserver相关的配置
const observerOptions = options?.observerOptions || {
threshold: [0, 0.5, 1],
};
const intersectionCallback = (entries: IntersectionObserverEntry[]) => {
const [entry] = entries;
if (entry.isIntersecting) {
if (entry.intersectionRatio >= observerOptions.threshold[1]) {
if (target.expose !== 'expose') {
options?.onExpose?.(target);
}
target.expose = 'expose';
if (!options?.always && typeof options?.onHide !== 'function') {
// 当always属性为加,且没有onHide方式时
// 则在执行一次曝光后,移动监听
io.unobserve(target);
}
}
} else if (typeof options?.onHide === 'function' && target.expose === 'expose') {
options.onHide(target);
target.expose = undefined;
if (!options?.always) {
io.unobserve(target);
}
}
};
const io = new IntersectionObserver(intersectionCallback, observerOptions);
io.observe(target);
};
调用起来也非常方便:
exposeListener(document.querySelector('.dom1'), {
always: true, // 监听的回调永远有效
onExpose() {
console.log('dom1 expose', Date.now());
},
onHide() {
console.log('dom1 hide', Date.now());
},
});
// 没有always时,所有的回调都只执行一次
exposeListener(document.querySelector('.dom2'), {
// always: true,
onExpose() {
console.log('dom2 expose', Date.now());
},
onHide() {
console.log('dom2 hide', Date.now());
},
});
// 重新设置IntersectionObserver的配置
exposeListener(document.querySelector('.dom3'), {
observerOptions: {
threshold: [0, 0.2, 1],
},
onExpose() {
console.log('dom1 expose', Date.now());
},
});
那么组件的曝光数据,就可以在onExpose()
的回调方式里进行上报。
不过我们可以看到,这里面有很多标记,需要我们处理,单纯的一个函数不太方便处理;而且也没对外暴露出取消监听的 api,导致我们想在卸载组件前也不方便取消监听。
因此我们可以用一个 class 类来实现。
2.2 类的实现方式
类的实现方式,我们可以把很多标记放在属性里。核心部分跟上面的差不多。
class ComExpose {
target = null;
options = null;
io = null;
exposed = false;
constructor(dom, options) {
this.target = dom;
this.options = options;
this.observe();
}
observe(options) {
this.unobserve();
const config = { ...this.options, ...options };
// IntersectionObserver相关的配置
const observerOptions = config?.observerOptions || {
threshold: [0, 0.5, 1],
};
const intersectionCallback = (entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
if (entry.intersectionRatio >= observerOptions.threshold[1]) {
if (!config?.always && typeof config?.onHide !== 'function') {
io.unobserve(this.target);
}
if (!this.exposed) {
config?.onExpose?.(this.target);
}
this.exposed = true;
}
} else if (typeof config?.onHide === 'function' && this.exposed) {
config.onHide(this.target);
this.exposed = false;
if (!config?.always) {
io.unobserve(this.target);
}
}
};
const io = new IntersectionObserver(intersectionCallback, observerOptions);
io.observe(this.target);
this.io = io;
}
unobserve() {
this.io?.unobserve(this.target);
}
}
调用的方式:
// 初始化时自动添加监听
const instance = new ComExpose(document.querySelector('.dom1'), {
always: true,
onExpose() {
console.log('dom1 expose');
},
onHide() {
console.log('dom1 hide');
},
});
// 取消监听
instance.unobserve();
不过这种类的实现方式,在 react 中使用起来也不太方便:
- 首先要通过
useRef()
获取到 dom 元素; - 组件卸载时,要主动取消对 dom 元素的监听;
2.3 react 中的组件嵌套的实现方式
我们可以利用 react 中的useEffect()
hook,能很方便地在卸载组件前,取消对 dom 元素的监听。
import React, { useEffect, useRef, useState } from 'react';
interface ComExposeProps {
children: any;
readonly always?: boolean; // 是否一直有效
// 曝光时的回调,若不存在always,则只执行一次
onExpose?: (dom: HTMLElement) => void;
// 曝光后又隐藏的回调,若不存在always,则只执行一次
onHide?: (dom: HTMLElement) => void;
observerOptions?: IntersectionObserverInit; // IntersectionObserver相关的配置
}
/**
* 监听元素的曝光
* @param {ComExposeProps} props 要监听的元素和回调
* @returns {JSX.Element}
*/
const ComExpose = (props: ComExposeProps): JSX.Element => {
const ref = useRef<any>(null);
const curExpose = useRef(false);
useEffect(() => {
if (ref.current) {
const target = ref.current;
const observerOptions = props?.observerOptions || {
threshold: [0, 0.5, 1],
};
const intersectionCallback = (entries: IntersectionObserverEntry[]) => {
const [entry] = entries;
if (entry.isIntersecting) {
if (entry.intersectionRatio >= observerOptions.threshold[1]) {
if (!curExpose.current) {
props?.onExpose?.(target);
}
curExpose.current = true;
if (!props?.always && typeof props?.onHide !== 'function') {
// 当always属性为加,且没有onHide方式时
// 则在执行一次曝光后,移动监听
io.unobserve(target);
}
}
} else if (typeof props?.onHide === 'function' && curExpose.current) {
props.onHide(target);
curExpose.current = false;
if (!props?.always) {
io.unobserve(target);
}
}
};
const io = new IntersectionObserver(intersectionCallback, observerOptions);
io.observe(target);
return () => io.unobserve(target); // 组件被卸载时,先取消监听
}
}, [ref]);
// 当组件的个数大于等于2,或组件使用fragment标签包裹时
// 则创建一个新的div用来挂在ref属性
if (React.Children.count(props.children) >= 2 || props.children.type.toString() === 'Symbol(react.fragment)') {
return <div ref="{ref}">{props.children}</div>;
}
// 为该组件挂在ref属性
return React.cloneElement(props.children, { ref });
};
export default ComExpose;
调用起来更加方便了,而且还不用手动获取 dom 元素和卸载监听:
<comexpose always="" onexpose="{()" ==""> console.log('expose')} onHide={() => console.log('hide')}>
<div classname="dom dom1">dom1 always</div>
</comexpose>
Vue 组件实现起来的方式也差不多,不过我 Vue 用的确实比较少,这里就不放 Vue 的实现方式了。
3. 总结
现在我们已经基本实现了关于组件的曝光的监听方式,整篇文章的核心全部都在IntersectionObserver
上。基于上面的实现方式,我们其实还可以继续扩展,比如在组件即将曝光时踩初始化组件;页面中的倒计时只有在可见时才执行,不可见时则直接停掉等等。
IntersectionObserver 还等着我们探索出更多的用法!
也欢迎您关注我的公众号:“前端小茶馆”。
基于 IntersectionObserver 实现一个组件的曝光监控的更多相关文章
- 如何基于 React 封装一个组件
如何基于 React 封装一个组件 前言 很多小伙伴在第一次尝试封装组件时会和我一样碰到许多问题,比如人家的组件会有 color 属性,我们在使用组件时传入组件文档中说明的属性值如 primary , ...
- 基于log4net的日志组件扩展封装,实现自动记录交互日志 XYH.Log4Net.Extend(微服务监控)
背景: 随着公司的项目不断的完善,功能越来越复杂,服务也越来越多(微服务),公司迫切需要对整个系统的每一个程序的运行情况进行监控,并且能够实现对自动记录不同服务间的程序调用的交互日志,以及通一个服务或 ...
- 基于iview 封装一个vue 表格分页组件
iview 是一个支持中大型项目的后台管理系统ui组件库,相对于一个后台管理系统的表格来说分页十分常见的 iview是一个基于vue的ui组件库,其中的iview-admin是一个已经为我们搭好的后天 ...
- 一个基于swoole的作业调度组件,已经实现了redis和rabitmq队列消息存储。
https://github.com/kcloze/swoole-jobs 一个基于swoole的作业调度组件,已经实现了redis和rabitmq队列消息存储.参考资料:swoole https:/ ...
- 基于 React 实现一个 Transition 过渡动画组件
过渡动画使 UI 更富有表现力并且易于使用.如何使用 React 快速的实现一个 Transition 过渡动画组件? 基本实现 实现一个基础的 CSS 过渡动画组件,通过切换 CSS 样式实现简单的 ...
- 基于TypeScript的FineUIMvc组件式开发(概述)
WebForm与Mvc 我简单说一下WebForm与Mvc,WebForm是微软很早就推出的一种WEB开发架构,微软对其进行了大量的封装,使开发人员可以像开发桌面程序一样去开发WEB程序,虽然开发效率 ...
- 基于Ardalis.GuardClauses守卫组件的拓展
在我们写程序的时候,经常会需要判断数据的是空值还是null值,基本上十个方法函数,八个要做这样的判断,因此我们很有必要拓展出来一个类来做监控,在这里我们使用一个简单地,可拓展的第三方组件:Ardali ...
- 基于Centos7.4搭建prometheus+grafana+altertManger监控Spring Boot微服务(docker版)
目的:给我们项目的微服务应用都加上监控告警.在这之前你需要将 Spring Boot Actuator引入 本章主要介绍 如何集成监控告警系统Prometheus 和图形化界面Grafana 如何自定 ...
- Android消息传递之基于RxJava实现一个EventBus - RxBus
前言: 上篇文章学习了Android事件总线管理开源框架EventBus,EventBus的出现大大降低了开发成本以及开发难度,今天我们就利用目前大红大紫的RxJava来实现一下类似EventBus事 ...
随机推荐
- Workerman:PHP的socket框架
hi,我们今天来讲讲Workerman,什么是Workerman呢? 看看官网上的介绍 Workerman是一款开源高性能异步PHP socket框架.支持高并发,超高稳定性,被广泛的用于手机app. ...
- 一致性哈希做负载均衡,基于dubbo的简化版本,超级简单容易理解!!!
一致性哈希算法原理以及做分布式存储.一定先看:一致性哈希算法 dubbo提供了四种负载均衡实现:权重随机算法,最少活跃调用数算法,一致性哈希算法,加权轮询算法. 本文基于开源项目:guide-rpc- ...
- 老Python总结的字典相关知识
字典 Python中的字典(dict)也被称为映射(mapping)或者散列(hash),是支持Python底层实现的重要数据结构. 同时,也是应用最为广泛的数据结构,内部采用hash存储,存储方式为 ...
- Python Basics with numpy (optional)
Python Basics with Numpy (optional assignment) Welcome to your first assignment. This exercise gives ...
- 技术分享|SQL和 NoSQL数据库之间的差异:MySQL(VS)MongoDB
在当今市场上,存在各种类型的数据库,选择适合你业务类型的数据库对应用的开发和维护有着重要意义.本篇文章,将为大家分享SQL和NoSQL语言之间的区别,同时还将比较这两种类型的数据库,以帮助小伙伴们选择 ...
- 基于Docker配置本地Gitlab
技术背景 Github和Gitee(码云)是最常见的基于git的代码托管平台,现在基于svn的代码管理仓库已经相对比较少见了,大部分还都是企业内部的代码仓.但是基于开源的Gitlab,我们在企业内网也 ...
- 如何使用Vue中的slot
之前看官方文档,由于自己理解的偏差,不知道slot是干嘛的,看到小标题,使用Slot分发内容,就以为 是要往下派发内容.然后就没有理解插槽的概念.其实说白了,使用slot就是先圈一块地,将来可能种花种 ...
- 子域名查询、DNS记录查询
目录 子域名信息查询 Layer子域名爆破机 subDomainBrute 利用google查询 HTTP证书查询 DNS记录查询脚本 IP转换为经纬度 利用网页获取对方经纬度信息 首先关于DNS域名 ...
- C#/VB.NET 自定义动画路径
PPT中的动画效果可分为已有内置动画以及自定义动画.设置内置动画,只需直接指定动画效果类型即可.本文主要介绍如何实现自定义动画,即自定义形状动作线性路径.附C#及VB.NET代码供参考. 程序运行环境 ...
- 【JavaScript】JS从入门到深入(复习查漏向
[JavaScript]JS从入门到深入(复习查漏向 pre 精细得学过一遍JS后才发现,原来之前CTF中有些nodejs的题目以及一些游戏题的payload就变得很好理解了. 基础知识 ECMASc ...