האמון הוא טוב, התצפית טובה יותר: 'הצטלבות' בגרסה 2

בגרסה 2 של Intersection Observer נוספת היכולת לא רק לצפות בצמתים כשלעצמם, אלא גם לזהות אם הרכיב שחוצה את הצומת היה גלוי בזמן הצטלבות.

Intersection Observer v1 הוא אחד מממשקי ה-API שכנראה כולם אוהבים, ועכשיו, אחרי שגם Safari תומך בו, אפשר להשתמש בו בכל הדפדפנים העיקריים. כדי לקבל עדכון מהיר על ה-API, מומלץ לצפות בSupercharged Microtip של Surma ב-Intersection Observer v1 שמוטמע בהמשך. אפשר גם לקרוא את המאמר המקיף של Surma. אנשים השתמשו ב-Intersection Observer v1 במגוון רחב של תרחישים לדוגמה, טעינה איטית של תמונות וסרטונים, קבלת התראות כשרכיבים מגיעים ל-position: sticky, הפעלת אירועי ניתוח נתונים ועוד.

הפרטים המלאים מופיעים במסמכי העזרה של Intersection Observer ב-MDN, אבל רצינו רק להזכיר איך נראה ממשק ה-API של Intersection Observer בגרסה 1 במקרה הבסיסי ביותר:

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 לא עומד בדרישות. בואו נבחן את זה לעומק. ה-API של Intersection Observer v1 יכול לומר לכם מתי רכיב מוצג בחלון התצוגה של הדף אחרי גלילה, אבל לא יכול לומר לכם אם הרכיב מכוסה על ידי תוכן אחר בדף (כלומר, מתי הרכיב מוסתר) או אם התצוגה החזותית של הרכיב שונתה על ידי אפקטים חזותיים כמו transform,‏ opacity,‏ filter וכו', שלמעשה יכולים להפוך אותו לבלתי נראה.

לגבי רכיב במסמך ברמה העליונה, אפשר לקבוע את המידע הזה על ידי ניתוח DOM באמצעות JavaScript, למשל באמצעות DocumentOrShadowRoot.elementFromPoint() ואז להמשיך לניתוח מעמיק יותר. לעומת זאת, אי אפשר לקבל את אותו מידע אם הרכיב הרלוונטי נמצא ב-iframe של צד שלישי.

למה חשיפה בפועל חשובה כל כך?

לצערנו, האינטרנט הוא מקום שמשוך גורמים זדוניים עם כוונות זדוניות עוד יותר. לדוגמה, בעל תוכן דיגיטלי מפוקפק שמציג מודעות בתשלום לפי קליק באתר תוכן עשוי לקבל תמריץ לפתות אנשים ללחוץ על המודעות שלו כדי להגדיל את התשלומים שהוא מקבל על המודעות (לפחות לתקופה קצרה, עד שרשת המודעות תתפוס אותו). בדרך כלל מודעות כאלה מוצגות ב-iframes. עכשיו, אם בעל האפליקציה ירצה לעודד משתמשים ללחוץ על מודעות כאלה, הוא יוכל להפוך את תגי ה-iframe של המודעות לשקופים לחלוטין על ידי החלת כלל CSS‏ iframe { opacity: 0; } והצגת תגי ה-iframe בשכבה עליונה מעל תוכן מושך, כמו סרטון חמוד של חתול שמשתמשים ירצו ללחוץ עליו. הפעולה הזו נקראת הנדסת קליקים. אפשר לראות התקפת clickjacking כזו בפעולה בקטע העליון של הדמו הזה (כדאי לנסות 'לצפות' בסרטון החתול ולהפעיל את 'מצב הטריק'). תוכלו לראות שהמודעה ב-iframe 'חושבת' שהיא קיבלה קליקים חוקיים, גם אם היא הייתה שקופה לגמרי כשלחצתם עליה (כאילו בטעות).

הטעיה של משתמש ללחיצה על מודעה על ידי עיצוב שלה כמודעה שקופה והצגה שלה בשכבה עליונה מעל משהו מושך.

איך Intersection Observer v2 פותר את הבעיה הזו?

ב-Intersection Observer v2 מוצג הרעיון של מעקב אחרי 'החשיפה' בפועל של רכיב יעד, כפי שאדם היה מגדיר אותה. אם מגדירים אפשרות במגדיר IntersectionObserver, המופעים של IntersectionObserverEntry שחופפים זה לזה יכללו שדה בוליאני חדש בשם isVisible. הערך true של isVisible הוא ערובה חזקה מההטמעה הבסיסית לכך שרכיב היעד לא מוסתר לחלוטין על ידי תוכן אחר, ושלא הוחלו עליו אפקטים חזותיים שעשויים לשנות או לעוות את הצגתו במסך. לעומת זאת, אם הערך הוא false, המשמעות היא שההטמעה לא יכולה להבטיח זאת.

פרט חשוב במפרט הוא שמותר להגדיר ב-implementation דיווח על תוצאות שליליות שגויות (כלומר, להגדיר את isVisible לערך false גם אם רכיב היעד גלוי לחלוטין ולא השתנה). מסיבות של ביצועים או מסיבות אחרות, הדפדפנים מגבילים את עצמם לעבודה עם תיבות גבול וגיאומטריה ישרה. הם לא מנסים להשיג תוצאות מדויקות ברמת הפיקסל בשינויים כמו border-radius.

עם זאת, אסור לקבל תוצאות חיוביות שגויות בשום מצב (כלומר, להגדיר את הערך של isVisible ל-true כשרכיב היעד לא גלוי לחלוטין ולא השתנה).

איך נראה הקוד החדש בפועל?

ה-constructor של IntersectionObserver מקבל עכשיו שני מאפייני תצורה נוספים: delay ו-trackVisibility. הערך delay הוא מספר שמציין את ההשהיה המינימלית, באלפיות השנייה, בין ההתראות מהמשקיף לגבי יעד נתון. הערך trackVisibility הוא ערך בוליאני שמציין אם הצופה יעקוב אחרי שינויים בחשיפה של היעד.

חשוב לציין שכאשר הערך של trackVisibility הוא true, הערך של delay חייב להיות לפחות 100 (כלומר, לא יותר מהתראה אחת בכל 100 אלפיות השנייה). כפי שצוין קודם, חישוב החשיפה הוא תהליך יקר, והדרישה הזו היא אמצעי זהירות כדי למנוע פגיעה בביצועים וצריכת סוללה. המפתח האחראי ישתמש בערך העיכוב הגדול ביותר שאפשר לסבול.

בהתאם למפרט הנוכחי, החשיפה מחושבת באופן הבא:

  • אם הערך של המאפיין trackVisibility של הצופה הוא false, היעד נחשב גלוי. ההגדרה הזו תואמת להתנהגות הנוכחית של גרסה 1.

  • אם ליעד יש מטריצת טרנספורמציה יעילה שאינה תרגום 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 Vincent, ל-Yoav Weiss ול-Mathias Bynens על בדיקת המאמר, וגם ל-Stefan Zager על בדיקת התכונה והטמעתה ב-Chrome. תמונה ראשית (Hero) של Sergey Semin ב-Unsplash.