Zaufanie jest dobre, obserwacja jest lepsza: obserwacja Intersection Observer (wersja 2)

Intersection Observer w wersji 2 umożliwia nie tylko obserwowanie przecięć, ale też wykrywanie, czy element przecinający był widoczny w momencie przecięcia.

Interfejs Intersection Observer w wersji 1 jest jednym z tych interfejsów API, które są powszechnie lubiane. Teraz, gdy Safari go obsługuje, można go w końcu stosować we wszystkich głównych przeglądarkach. Aby szybko odświeżyć sobie informacje o tym interfejsie API, obejrzyj wykład Surmy Supercharged Microtip o IntersectionObserver v1, który jest umieszczony poniżej. Możesz też przeczytać obszerny artykuł Surmy. Interfejs Intersection Observer w wersji 1 jest używany w wielu przypadkach, np. do opóźnionego wczytywania obrazów i filmów, powiadamiania o dotarciu elementów do pozycji position: sticky, wywoływania zdarzeń analitycznych i wielu innych.

Pełne informacje znajdziesz w dokumentacji Intersection Observer na stronie MDN. Poniżej przedstawiamy interfejs Intersection Observer API w najprostszej postaci:

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'));

Co stanowi wyzwanie w aplikacji Intersection Observer v1?

Po pierwsze, IntersectionObserver w wersji 1 jest świetny, ale nie jest idealny. W niektórych szczególnych przypadkach interfejs API może nie działać prawidłowo. Przyjrzyjmy się temu bliżej. Interfejs API Intersection Observer v1 może poinformować, kiedy element jest przewinięty do widoku okna, ale nie poinformuje, czy element jest zasłonięty przez jakikolwiek inny element strony (czyli czy jest zasłonięty) ani czy wyświetlanie elementu nie zostało zmodyfikowane przez efekty wizualne takie jak transform, opacity, filter itp., które skutecznie mogą sprawić, że element będzie niewidoczny.

W przypadku elementu w dokumencie najwyższego poziomu te informacje można określić, analizując DOM za pomocą JavaScriptu, np. za pomocą DocumentOrShadowRoot.elementFromPoint(), a potem pogłębiając dane. Nie można natomiast uzyskać tych samych informacji, jeśli dany element znajduje się w ramce iframe firmy zewnętrznej.

Dlaczego widoczność jest tak ważna?

Internet jest niestety miejscem, które przyciąga nieuczciwych podmiotów o złych zamiarach. Na przykład podejrzany wydawca, który wyświetla reklamy typu „pay-per-click” w witrynie z treściami, może być zachęcany do zmuszania użytkowników do klikania reklam w celu zwiększenia zarobków z reklam (przynajmniej przez krótki czas, dopóki sieć reklamowa go nie wykryje). Zwykle takie reklamy są wyświetlane w ramkach iframe. Jeśli wydawca chce zachęcić użytkowników do klikania takich reklam, może sprawić, że ramki iframe reklam będą całkowicie przezroczyste, stosując regułę CSS iframe { opacity: 0; } i nakładając je na coś atrakcyjnego, np. na uroczego kota, który zachęca użytkowników do kliknięcia. Jest to tzw. clickjacking. Taki atak typu clickjacking możesz zobaczyć w górnej części tego demonstracji (spróbuj „obejrzeć” film z kotem i aktywować „tryb oszutek”). Zauważysz, że reklama w elemencie iframe „uważa”, że uzyskała prawidłowe kliknięcia, mimo że kliknięcie przez Ciebie (pozornie nieświadomie) było całkowicie przejrzyste.

Wmanewrowanie użytkownika do kliknięcia reklamy przez nadanie jej przezroczystego wyglądu i nałożenie na coś atrakcyjnego.

Jak IntersectionObserver v2 rozwiązuje ten problem?

Intersection Observer v2 wprowadza koncepcję śledzenia rzeczywistej „widoczności” elementu docelowego w odniesieniu do tego, jak rozumie go człowiek. Po ustawieniu opcji w konstruktorze IntersectionObserver instancje przecinających się IntersectionObserverEntry będą zawierać nowe pole logiczne o nazwie isVisible. Wartość true dla parametru isVisible jest mocną gwarancją implementacji podstawowej, że element docelowy jest całkowicie widoczny i nie jest zasłonięty przez inne treści. Nie ma też zastosowanych efektów wizualnych, które mogłyby zmienić lub zniekształcić jego wyświetlanie na ekranie. Wartość false oznacza natomiast, że implementacja nie gwarantuje tej gwarancji.

Ważną cechą specyfikacji jest to, że implementacja może zgłaszać wyniki fałszywie negatywne (czyli ustawić isVisible na false, nawet jeśli element docelowy jest w pełni widoczny i niezmodyfikowany). Ze względu na wydajność lub z innych powodów przeglądarki ograniczają się do pracy z obcięciami i geometrią prostokątną; nie próbują uzyskać idealnych wyników w przypadku modyfikacji takich jak border-radius.

Fałszywie pozytywne wyniki nie są dozwolone pod żadnym pozorem (czyli ustawienie wartości isVisible na true, gdy element docelowy nie jest całkowicie widoczny i niezmodyfikowany).

Jak wygląda nowy kod w praktyce?

Konstruktor IntersectionObserver przyjmuje teraz 2 dodatkowe właściwości konfiguracji: delay i trackVisibility. delay to liczba wskazująca minimalne opóźnienie (w milisekundach) między powiadomieniami od obserwatora dotyczącymi danego celu. Wartość trackVisibility jest wartością logiczną wskazującą, czy obserwator będzie śledzić zmiany widoczności celu.

Pamiętaj, że gdy trackVisibility ma wartość true, wartość delay musi wynosić co najmniej 100 (czyli nie może pojawiać się więcej niż jedno powiadomienie co 100 ms). Jak już wspomnieliśmy, widoczność jest kosztowna. Stanowi to zabezpieczenie przed obniżeniem wydajności i zużyciem baterii. Odpowiedzialny deweloper powinien użyć największej dopuszczalnej wartości opóźnienia.

Zgodnie z obecną specyfikacją widoczność jest obliczana w ten sposób:

  • Jeśli atrybut trackVisibility obserwatora ma wartość false, cel jest uważany za widoczny. Odpowiada to obecnemu działaniu wersji 1.

  • Jeśli obiekt docelowy ma efektywną macierz przekształceń inną niż tłumaczenie 2D lub proporcjonalne skalowanie 2D, cel jest uznawany za niewidoczny.

  • Jeśli element docelowy lub dowolny element w jego łańcuchu bloków ma skuteczną przezroczystość inną niż 1,0, jest uważany za niewidoczny.

  • Jeśli cel lub dowolny element w jego zawartym łańcuchu bloków ma zastosowane jakiekolwiek filtry, element docelowy jest uważany za niewidoczny.

  • Jeśli implementacja nie gwarantuje, że miejsce docelowe jest całkowicie wykluczone z innej zawartości strony, jest on uważany za niewidoczny.

Oznacza to, że obecne implementacje są dość konserwatywne, jeśli chodzi o gwarantowanie widoczności. Na przykład zastosowanie niemal niezauważalnego filtra skali szarości, takiego jak filter: grayscale(0.01%), lub ustawienie niemal niewidocznej przezroczystości za pomocą ustawienia opacity: 0.99 sprawi, że element będzie niewidoczny.

Poniżej znajdziesz krótki przykładowy kod, który ilustruje nowe funkcje interfejsu API. W drugiej części prezentacji możesz zobaczyć, jak działa śledzenie kliknięć (ale teraz spróbuj „obejrzeć” film z szczeniakiem). Pamiętaj, aby ponownie aktywować „tryb sztuczny”, aby od razu przekształcić się w podejrzanego wydawcę i sprawdzić, jak Intersection Observer v2 zapobiega śledzeniu nielegalnych kliknięć reklam. Tym razem wracamy do Intersection Observer v2. 🎉

Obserwatorzy skrzyżowań w wersji 2, który zapobiega niezamierzonemu kliknięciu reklamy.

<!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'));

Podziękowania

Dziękujemy Simeonowi Vincentowi, Yoavowi WeissowiMathiasowi Bynensowi za sprawdzenie tego artykułu, a także Stefanowi Zagerowi za sprawdzenie i wdrażanie tej funkcji w Chrome. Obraz główny: Sergey Semin, Unsplash.