信任有益,观察更好:Intersection Observer v2

Intersection Observer v2 不仅增加了观察交叉路口本身的功能,还增加了检测相交元素在交叉路口时是否可见的功能。

Intersection Observer v1 就是其中可能深受大众喜爱的 API 之一,现在,Safari 也支持它,它最终也在所有主流浏览器中可供广泛使用。如需快速回顾该 API,建议您观看下面嵌入的 Intersection Observer v1 的 Supercharged Microtip您还可以阅读 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(例如通过 DocumentOrShadowRoot.elementFromPoint())分析 DOM 来确定此信息,然后进行更深入的分析。相反,如果相关元素位于第三方 iframe 中,则无法获取相同的信息。

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

遗憾的是,互联网是一个吸引意图更差的不良分子的地方。 例如,在内容网站上投放按点击付费广告的可疑发布商可能会诱骗用户点击广告,以增加发布商的广告收入(至少在短期内,直到广告联盟抓住他们为止)。此类广告通常在 iframe 中投放。 现在,如果发布商想要让用户点击此类广告,可以应用 CSS 规则 iframe { opacity: 0; },并将 iframe 叠加到具有吸引力的内容(例如用户真正想要点击的可爱猫咪视频)之上,使广告 iframe 完全透明。这称为点击劫持。您可以在此演示的上半部分(尝试“观看”猫咪视频并激活“诱骗模式”)了解此类点击劫持攻击的实际应用。您会发现,iframe 中的广告“认为”获得了合法的点击,即使您在(非自愿)点击了广告时它完全透明。

通过将广告的样式设为透明,并将其叠加到具有吸引力的其他内容上来诱骗用户点击广告。

Intersection Observer v2 如何解决此问题?

Intersection Observer v2 引入了跟踪目标元素的实际“可见性”的概念,正如人类所定义。通过在 IntersectionObserver 构造函数中设置一个选项,与 IntersectionObserverEntry 实例相交便会包含一个名为 isVisible 的新布尔值字段。isVisibletrue 值能够有力地保证目标元素不会被其他内容完全遮盖,并且不会应用任何会改变或扭曲其屏幕上显示的视觉效果。相反,false 值表示实现无法保证这一点。

spec的一个重要细节是,实现可以报告假负例(也就是说,即使目标元素完全可见且未经修改,也会将 isVisible 设置为 false)。出于性能或其他原因,浏览器只能使用边界框和直线几何图形;而不会尝试通过 border-radius 等修改来实现像素级完美的结果。

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

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

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

请务必注意,当 trackVisibilitytrue 时,delay 必须至少为 100(即每 100 毫秒不超过一条通知)。如前所述,计算可见性的成本很高,而这项要求是防止性能下降和耗电量下降。负责任的开发者将使用最大可容忍值作为延迟。

根据当前spec,可见性的计算方式如下:

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

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

  • 如果目标或其所在区块链中的任何元素的有效不透明度不是 1.0,则该目标会被视为不可见。

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

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

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

下面是一个简短的代码示例,展示了新的 API 功能。您可以在演示的第二部分查看其点击跟踪逻辑的实际运用(不过,现在,请尝试“观看”狗狗视频)。请务必再次启用“欺骗模式”,以立即将自己变成可疑的发布商,并了解 Intersection Observer v2 如何阻止系统跟踪非法广告点击。 这一次 Intersection Observer v2 就是我们的后盾!🎉

Intersection Observer v2 用于防止意外点击广告。

<!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 用户发布。