信任是好的,观察更好:Intersection Observer v2

Intersection Observer v2 不仅增加了观察交叉点的能力,还增加了检测交叉元素在交叉时是否可见的能力。

Intersection Observer v1 是那些可能受到普遍喜爱的 API 之一,而且,现在 Safari 也支持它,它也终于可以在所有主流浏览器中普遍使用了。为了快速回顾 API,我建议观看 SurmaSupercharged Microtip 关于 Intersection Observer v1 的视频,该视频嵌入在下方。您也可以阅读 Surma 的深入 文章。人们已经将 Intersection Observer v1 用于各种用例,例如 图像和视频的懒加载在元素达到 position: sticky 时收到通知触发分析事件等等。

有关完整详细信息,请查看 MDN 上的 Intersection Observer 文档,但作为简短的提醒,这是最基本情况下 Intersection Observer v1 API 的样子

const onIntersection = (entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      console.log(entry);
    }
  }
};

const observer = new IntersectionObserver(onIntersection);
observer.observe(document.querySelector('#some-target'));

Intersection Observer v1 的挑战是什么?

需要明确的是,Intersection Observer v1 非常棒,但它并不完美。在某些极端情况下,API 会有所不足。让我们仔细看看!Intersection Observer v1 API 可以告诉您元素何时滚动到窗口的视口中,但它不会告诉您元素是否被任何其他页面内容覆盖(即,当元素被遮挡时),或者元素的视觉显示是否已被 transformopacityfilter 等视觉效果修改,这些效果实际上可以使其不可见。

对于顶级文档中的元素,可以通过 JavaScript 分析 DOM 来确定此信息,例如通过 DocumentOrShadowRoot.elementFromPoint(),然后深入挖掘。相比之下,如果相关元素位于第三方 iframe 中,则无法获得相同的信息。

为什么实际可见性如此重要?

不幸的是,互联网是一个吸引心怀不轨的坏人的地方。例如,在内容网站上投放按点击付费广告的黑心发布商可能会被激励诱骗人们点击他们的广告,以增加发布商的广告收入(至少在广告网络抓住他们之前的一段时间内)。通常,此类广告在 iframe 中投放。现在,如果发布商想让用户点击此类广告,他们可以通过应用 CSS 规则 iframe { opacity: 0; } 并将 iframe 覆盖在一些有吸引力的东西之上,例如用户实际上想点击的可爱猫咪视频,从而使广告 iframe 完全透明。这称为点击劫持。您可以在此演示的上半部分看到这种点击劫持攻击的实际效果(尝试“观看”猫咪视频并激活“欺骗模式”)。您会注意到,即使当您(假装非自愿地)点击它时,iframe 中的广告是完全透明的,但它仍然“认为”它收到了合法的点击。

Tricking a user into clicking an ad by styling it transparent and overlaying it on top of something attractive.

Intersection Observer v2 如何解决这个问题?

Intersection Observer v2 引入了跟踪目标元素的实际“可见性”的概念,就像人类定义的那样。通过在 IntersectionObserver 构造函数中设置一个选项,交叉的 IntersectionObserverEntry 实例将包含一个名为 isVisible 的新布尔字段。isVisibletrue 值是底层实现的一个强有力保证,即目标元素完全未被其他内容遮挡,并且没有应用任何会改变或扭曲其在屏幕上的显示的视觉效果。相比之下,false 值表示实现无法做出该保证。

规范的一个重要细节是,实现被允许报告假阴性(即,即使目标元素完全可见且未修改,也将 isVisible 设置为 false)。出于性能或其他原因,浏览器将自身限制为使用边界框和直线几何;对于 border-radius 等修改,它们不会尝试实现像素完美的結果。

也就是说,在任何情况下都不允许出现假阳性(即,当目标元素不完全可见且未修改时,将 isVisible 设置为 true)。

新代码在实践中是什么样的?

IntersectionObserver 构造函数现在接受两个额外的配置属性:delaytrackVisibilitydelay 是一个数字,指示观察者针对给定目标发出通知之间的最小延迟(以毫秒为单位)。trackVisibility 是一个布尔值,指示观察者是否会跟踪目标可见性的变化。

这里需要注意的是,当 trackVisibilitytrue 时,delay 必须至少为 100(即,每 100 毫秒不超过一个通知)。如前所述,可见性计算成本很高,此要求是为了预防性能下降和电池消耗。负责任的开发人员将使用延迟的最大可容忍值。

根据当前的 规范,可见性计算如下

  • 如果观察者的 trackVisibility 属性为 false,则目标被视为可见。这对应于当前的 v1 行为。

  • 如果目标具有除 2D 平移或比例 2D 放大之外的有效变换矩阵,则目标被视为不可见。

  • 如果目标或其包含块链中的任何元素具有除 1.0 之外的有效不透明度,则目标被视为不可见。

  • 如果目标或其包含块链中的任何元素应用了任何滤镜,则目标被视为不可见。

  • 如果实现无法保证目标完全未被其他页面内容遮挡,则目标被视为不可见。

这意味着当前的实现对于保证可见性非常保守。例如,应用几乎不明显的灰度滤镜(如 filter: grayscale(0.01%))或设置几乎不可见的透明度(opacity: 0.99)都会使元素不可见。

下面是一个简短的代码示例,说明了新的 API 功能。您可以在演示的第二部分看到它的点击跟踪逻辑的实际效果(但现在,尝试“观看”小狗视频)。请务必再次激活“欺骗模式”,立即将自己转变为黑心发布商,并了解 Intersection Observer v2 如何防止非法的广告点击被跟踪。这一次,Intersection Observer v2 为我们撑腰!🎉

Intersection Observer v2 preventing an unintended click on an ad.

<!DOCTYPE html>
<!-- This is the ad running in the iframe -->
<button id="callToActionButton">Buy now!</button>
// This is code running in the iframe.

// The iframe must be visible for at least 800ms prior to an input event
// for the input event to be considered valid.
const minimumVisibleDuration = 800;

// Keep track of when the button transitioned to a visible state.
let visibleSince = 0;

const button = document.querySelector('#callToActionButton');
button.addEventListener('click', (event) => {
  if ((visibleSince > 0) &&
      (performance.now() - visibleSince >= minimumVisibleDuration)) {
    trackAdClick();
  } else {
    rejectAdClick();
  }
});

const observer = new IntersectionObserver((changes) => {
  for (const change of changes) {
    // ⚠️ Feature detection
    if (typeof change.isVisible === 'undefined') {
      // The browser doesn't support Intersection Observer v2, falling back to v1 behavior.
      change.isVisible = true;
    }
    if (change.isIntersecting && change.isVisible) {
      visibleSince = change.time;
    } else {
      visibleSince = 0;
    }
  }
}, {
  threshold: [1.0],
  // 🆕 Track the actual visibility of the element
  trackVisibility: true,
  // 🆕 Set a minimum delay between notifications
  delay: 100
}));

// Require that the entire iframe be visible.
observer.observe(document.querySelector('#ad'));

致谢

感谢 Simeon VincentYoav WeissMathias Bynens 审阅本文,以及 Stefan Zager 同样审阅并在 Chrome 中实现该功能。英雄图片由 Sergey Semin 在 Unsplash 上拍摄。