我们在产品推广过程中,经常需要判断用户是否对某个模块感兴趣。那么就需要获取该模块的曝光量和用户对该模块的点击量,若点击量/曝光量越高,说明该模块越有吸引力。

那么如何知道模块对用户是否曝光了呢?之前我们是监听页面的滚动事件,然后通过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,可以很好地实现组件曝光量的统计。

实现的方式主要有两种:

  1. 函数的方式;
  2. 高阶组件的方式;

传入的参数:

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 中使用起来也不太方便:

  1. 首先要通过useRef()获取到 dom 元素;
  2. 组件卸载时,要主动取消对 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 实现一个组件的曝光监控的更多相关文章

  1. 如何基于 React 封装一个组件

    如何基于 React 封装一个组件 前言 很多小伙伴在第一次尝试封装组件时会和我一样碰到许多问题,比如人家的组件会有 color 属性,我们在使用组件时传入组件文档中说明的属性值如 primary , ...

  2. 基于log4net的日志组件扩展封装,实现自动记录交互日志 XYH.Log4Net.Extend(微服务监控)

    背景: 随着公司的项目不断的完善,功能越来越复杂,服务也越来越多(微服务),公司迫切需要对整个系统的每一个程序的运行情况进行监控,并且能够实现对自动记录不同服务间的程序调用的交互日志,以及通一个服务或 ...

  3. 基于iview 封装一个vue 表格分页组件

    iview 是一个支持中大型项目的后台管理系统ui组件库,相对于一个后台管理系统的表格来说分页十分常见的 iview是一个基于vue的ui组件库,其中的iview-admin是一个已经为我们搭好的后天 ...

  4. 一个基于swoole的作业调度组件,已经实现了redis和rabitmq队列消息存储。

    https://github.com/kcloze/swoole-jobs 一个基于swoole的作业调度组件,已经实现了redis和rabitmq队列消息存储.参考资料:swoole https:/ ...

  5. 基于 React 实现一个 Transition 过渡动画组件

    过渡动画使 UI 更富有表现力并且易于使用.如何使用 React 快速的实现一个 Transition 过渡动画组件? 基本实现 实现一个基础的 CSS 过渡动画组件,通过切换 CSS 样式实现简单的 ...

  6. 基于TypeScript的FineUIMvc组件式开发(概述)

    WebForm与Mvc 我简单说一下WebForm与Mvc,WebForm是微软很早就推出的一种WEB开发架构,微软对其进行了大量的封装,使开发人员可以像开发桌面程序一样去开发WEB程序,虽然开发效率 ...

  7. 基于Ardalis.GuardClauses守卫组件的拓展

    在我们写程序的时候,经常会需要判断数据的是空值还是null值,基本上十个方法函数,八个要做这样的判断,因此我们很有必要拓展出来一个类来做监控,在这里我们使用一个简单地,可拓展的第三方组件:Ardali ...

  8. 基于Centos7.4搭建prometheus+grafana+altertManger监控Spring Boot微服务(docker版)

    目的:给我们项目的微服务应用都加上监控告警.在这之前你需要将 Spring Boot Actuator引入 本章主要介绍 如何集成监控告警系统Prometheus 和图形化界面Grafana 如何自定 ...

  9. Android消息传递之基于RxJava实现一个EventBus - RxBus

    前言: 上篇文章学习了Android事件总线管理开源框架EventBus,EventBus的出现大大降低了开发成本以及开发难度,今天我们就利用目前大红大紫的RxJava来实现一下类似EventBus事 ...

随机推荐

  1. k8s 运行单实例 mysql

    配置文件mysql.yaml --- apiVersion: v1 kind: Service metadata: name: mysql-01 spec: ports: - port: 3306 s ...

  2. Word Reversal(string)

    For each list of words, output a line with each word reversed without changing the order of the word ...

  3. Spring Boot的自动配置原理及启动流程源码分析

    概述 Spring Boot 应用目前应该是 Java 中用得最多的框架了吧.其中 Spring Boot 最具特点之一就是自动配置,基于Spring Boot 的自动配置,我们可以很快集成某个模块, ...

  4. Jquery 代码参考

    jquery 代码参考 jQuery(document).ready(function($){}); jQuery(window).on('load', function(){}); $('.vide ...

  5. HTML / CSS技巧 – 可滚动的 tbody(漂亮表格)

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  6. hdu5251最小矩形覆盖

    题意(中问题直接粘吧)矩形面积 Problem Description 小度熊有一个桌面,小度熊剪了很多矩形放在桌面上,小度熊想知道能把这些矩形包围起来的面积最小的矩形的面积是多少.   Input ...

  7. 初探 Git Submodules

    之前一直想将一个 Git 仓库放到另一个 Git 仓库,有 Maven 多模块项目(Maven Multimodule Project)和 Gradle 多项目构建(Gradle Multiproje ...

  8. Maven关于web.xml中Servlet和Servlet映射的问题

    在配置Servlet时,有两个地方需要配置. 一个是<servlet>,另一个是<servlet-Mapping>,这两个一个是配置Servlet,一个是配置其映射信息. &l ...

  9. SpringBoot配置切换

    切换需求 有时候在本地测试是使用8080端口,可是上线使用的又是80端口. 此时就可以通过多配置文件实现多配置支持与灵活切换. 多配置文件 3个配置文件: 核心配置文件:application.pro ...

  10. 【微信小程序】--bindtap参数传递,配合wx.previewImage实现多张缩略图预览

    本文为原创随笔,纯属个人理解.如有错误,欢迎指出. 如需转载请注明出处 在微信小程序中预览图片分为 a.预览本地相册中的图片. b.预览某个wxml中的多张图片. 分析:实质其实是一样的.都是给wx. ...