האמון הוא טוב, התצפית טובה יותר: 'הצטלבות' בגרסה 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 של צד שלישי.

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

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

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

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

ב-Intersection Observer v2 מוצג הרעיון של מעקב אחרי 'החשיפה' בפועל של רכיב יעד, כפי שאדם היה מגדיר אותה. כשמגדירים אפשרות ב-constructor של 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.

  • אם ליעד יש מטריצת טרנספורמציה יעילה שאינה תרגום דו-ממדי או התאמה פרופורציונלית בדו-ממד, היעד נחשב לבלתי נראה.

  • אם לשקיפות בפועל של היעד, או של רכיב כלשהו ברשת הבלוקים שמכילה אותו, יש ערך שונה מ-1.0, היעד נחשב בלתי גלוי.

  • אם הוחלו מסננים על היעד או על רכיב כלשהו ברשת הבלוקים שמכילה אותו, היעד נחשב לא מוסתר.

  • אם לא ניתן להבטיח שהיעד לא מוסתר לחלוטין על ידי תוכן אחר בדף, היעד נחשב לא גלוי.

פירוש הדבר הוא שההטמעות הנוכחיות הן די שמרניות ומבטיחות חשיפה. לדוגמה, החלת מסנן אפור כמעט בלתי מורגש כמו filter: grayscale(0.01%) או הגדרת שקיפות כמעט בלתי נראית באמצעות opacity: 0.99 יגרמו לכך שהרכיב יהיה בלתי נראה.

בהמשך מופיעה דוגמה קצרה לקוד שממחישה את התכונות החדשות של ה-API. אפשר לראות את הלוגיקה של מעקב הקליקים בפעולה בקטע השני של הדמו (אבל עכשיו, נסו 'לצפות' בסרטון של הגורים). חשוב להפעיל שוב את 'מצב הטריק' כדי להפוך מיד לבעלי תוכן דיגיטלי מפוקפק, ולראות איך Intersection Observer v2 מונע מעקב אחר קליקים לא חוקיים על מודעות. הפעם, גרסה 2 של Intersection Observer מחזירה לנו! 🎉

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.