信任關係良好,觀測能力更佳:《Intersection Observer v2》

Intersection Observer 2 版除了可觀察交叉點,還能偵測交叉點時是否可見交叉元素。

Intersection Observer v1 是廣受歡迎的 API 之一,現在Safari 也支援這個 API,因此終於可以在所有主要瀏覽器中普遍使用。如要快速複習 API,建議您觀看以下嵌入的 Intersection Observer v1 的 Surma 超強 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 分析 DOM (例如透過 DocumentOrShadowRoot.elementFromPoint()),然後深入探討,來判斷這項資訊。相反地,如果問題元素位於第三方 iframe 中,就無法取得相同的資訊。

為什麼實際可視度如此重要?

很遺憾,網際網路會吸引惡意行為人,他們意圖不良。舉例來說,在內容網站上放送按次付費廣告的不明出版商,可能會受到誘因,以欺騙的方式誘使使用者點按廣告,藉此提高廣告收益 (至少在廣告聯播網發現前,短時間內會如此)。這類廣告通常會在 iframe 中放送。如今,如果發布商想讓使用者點擊這類廣告,可以套用 CSS 規則 iframe { opacity: 0; },並在廣告 iframe 上疊加吸引人的內容,例如使用者會想點選的可愛貓咪影片。這就是所謂的「點擊劫持」。您可以在這個示範的上半部看到這種點按盜用攻擊的實際運作情形 (請嘗試「觀看」貓咪影片並啟用「騙局模式」)。您會發現,即使您在 (假裝非自願) 點按 iframe 時,iframe 中的廣告也會「認為」自己收到了合法點擊。

將廣告設為透明,並疊加在吸引人的內容上,誘騙使用者點擊廣告。

Intersection Observer 2.0 如何修正這個問題?

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 第 2 版可避免無意點擊廣告。

<!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 中實作這項功能。主頁橫幅圖片由 Unsplash 上的 Sergey Semin 提供。