什么是曝光?

商品曝光人数:看到商品在首页、列表页、活动页面,以及在商品详情页下方的更多展现的人数。(不包括商品详情页的访客数量)
商品曝光次数:商品在店铺首页、列表页、活动页面,以及在商品详情页下方的更多展现的次数。(不包括商品详情页的浏览量)

通过商品曝光我们能得出商品在不同营销位的比重, 从而得出用户操作喜好

如何判断元素可视区域?

Element.getBoundingClientRect()

getBoundingClientRect 方法返回一个 对象,该 DOMRect 对象提供有关元素大小及其相对于视口的位置的信息。
如果目标元素 rect 满足 top > 0 && left > 0 && bottom >= 视窗高度 && right <= 视窗宽度
便能得出元素完全在视窗内。 在长列表下 我们可以通过监听滚动条事件, 从而获取目标元素是否暴露在用户视窗内。

在线代码示例 CodePen

这种方法实现起来简单,兼容性相对较好,这个属性频繁计算会引发页面的重绘,当元素过多时,会造成性能问题,出现卡顿,影响使用体验。

1
2
3
4
5
6
7
8
9
10
11
// 判断可视区域内
const isElementInViewport = (el) => {
let rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};

Intersection Observer API

Intersection Observer API 提供了一种异步检测目标元素与祖先元素或 viewport 相交情况变化的方法。
Intersection Observer API 会注册一个回调函数,每当被监视的元素进入或者退出另外一个元素时 (或者 viewport ),或者两个元素的相交部分大小发生变化时,该回调方法会被触发执行。这样,我们网站的主线程不需要再为了监听元素相交而辛苦劳作,浏览器会自行优化元素相交管理。

浏览器兼容性

在线代码示例 CodePen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const intersectionObserverCallBack = (entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
// 可见
var container = entry.target;
}
}
};

var observer = new IntersectionObserver(intersectionObserverCallBack);

observer.observe(element);

// 停止观察
observer.unobserve(element);

// 关闭观察器
observer.disconnect();

总结

两种方案

  • getBoundingClientRect 浏览器支持不错, 简单易用但是可能存在性能问题。
  • Intersection Observer API 没有性能问题, 但是存在一定兼容性。

好消息是我们可以使用polyfill 解决 Intersection Observer API 兼容性问题
它会在不支持的浏览器 使用 getBoundingClientRect 去重新实现一遍 Intersection Observer API。

polyfill浏览器支持:

Chrome
Firefox
Safari
6+
Edge
Internet Explorer
7+
Opera
Android
4.4+
1
2
// 安装
npm install intersection-observer

实现

曝光肯定是结合埋点一起使用, 通过采集某个商品是否出现在用户的可视区域内, 进行上报

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import IntersectionObserver from 'intersection-observer';

const attrName = 'exposure-data'

class Exposure {
private observer: IntersectionObserver | undefined;

init() {
return new IntersectionObserver((entries, observer) => {
entries.forEach((item) => {
if (item.isIntersecting) {
const el: Element = item.target
observer!.unobserve(el);
const arrtString = el.getAttribute(attrName) || null;
if(!arrtString) return
const params = JSON.parse(arrtString);
// 上报
autoSendTracker({
...params,
});
}
});
});
}
add(entry: { el: Element; binding: any }) {
entry.el.setAttribute(attrName, typeof entry.binding.value === 'string' ? entry.binding.value : JSON.stringify(entry.binding.value))
if (this.observer === undefined) {
this.observer = this.init();
}
this.observer.observe(entry.el);
}
remove(entry: { el: Element; binding?: any }) {
this.observer?.unobserve(entry.el);
this.observer?.disconnect();
}
}

export default Exposure;


const exposure = new Exposure();

// 自定义指令
const track = {
inserted: function (el: any, binding: { arg: any }) {
const { arg } = binding;
arg.split("|").forEach((item) => {
// 点击
switch (item) {
case "click":
cli.add({ el, binding });
break;

case "keyup":
keyup.add({ el, binding });
break;
case "exposure":
exposure.add({ el, binding });
break;
}
});
},
unbind(el, binding) {
const { arg } = binding;
arg.split("|").forEach((item) => {
// 点击
switch (item) {
case "click":
cli.remove({ el, binding });
break;

case "keyup":
keyup.remove({ el, binding });
break;

case "exposure":
exposure.remove({ el, binding });
break;
}
});
},
};


// 使用
<div
v-track:exposure="{
object_ids: item.skuoid,
event_type: 8,
...
}"
>
...
</div>


文中完整代码
github > npm

参考

IntersectionObserver API 使用教程