הפעלה עקבית של משתמשים בכל ממשקי ה-API

מוסטק אחמד
ג'ו מדלי
ג'ו מדלי

כדי למנוע מסקריפטים זדוניים לפגוע בממשקי API רגישים כמו חלונות קופצים, מסך מלא וכו', הדפדפנים שולטים בגישה לממשקי ה-API האלה באמצעות הפעלת משתמשים. הפעלת משתמש היא המצב של סשן הגלישה ביחס לפעולות של משתמשים: מצב 'פעיל' מרמז בדרך כלל שהמשתמש נמצא כרגע באינטראקציה עם הדף או השלים אינטראקציה מאז טעינת הדף. תנועת משתמש היא מונח פופולרי אך מטעה לאותו רעיון. לדוגמה, פעולת החלקה או תנועת החלקה שביצע משתמש לא מפעילה את הדף ולכן, מבחינת הסקריפט, לא הפעלת משתמש.

בדפדפנים המובילים יש כיום דפוסי התנהגות שונים בכל הנוגע לאופן שבו הפעלת המשתמשים שולטת בממשקי ה-API שמוגבלים להפעלה. ב-Chrome, ההטמעה התבססה על מודל מבוסס-אסימונים, שמסתבר שהוא מורכב מדי מכדי להגדיר התנהגות עקבית בכל ממשקי ה-API שמוגבלים להפעלה. לדוגמה, Chrome אפשר גישה חלקית לממשקי API בהגבלת הפעלה באמצעות postMessage() ו-setTimeout() קריאות, וגם לא באמצעות הבטחות, XHR, אינטראקציה עם Gamepad וכו'. חשוב לשים לב שחלק מהבאגים פופולריים אבל ותיקים וחדשים.

בגרסה 72, המערכת של Chrome כוללת את גרסה 2 של הפעלת המשתמש, שמאפשרת להפעיל את הזמינות של המשתמשים בכל ממשקי ה-API שנמצאים רק בשלב ההפעלה. הבעיה הזו פותרת את חוסר העקביות שהוזכר למעלה (ובעיות נוספות, כמו MessageChannels), ואנחנו מאמינים שכך אפשר לפתח את חוויית ההתפתחות של משתמשים באינטרנט. בנוסף, ההטמעה החדשה מספקת הטמעה של הפניות למפרט חדש, שמטרתו לחבר בין כל הדפדפנים בטווח הארוך.

איך פועלת הפעלת משתמש גרסה 2?

ה-API החדש שומר על מצב הפעלה של שני ביט בכל אובייקט window בהיררכיית הפריימים: ביט במיקום קבוע שמעיד על מצב ההפעלה ההיסטורי של משתמש (אם פריים זיהה פעם הפעלה של משתמש) וקטע זמני עבור המצב הנוכחי (אם פריים מזהה הפעלה של משתמש תוך בערך שנייה). המקדח הדביק לעולם לא מתאפס במהלך משך החיים של הפריים לאחר הגדרתו. הקטע הזמני מוגדר לכל אינטראקציה של משתמש, והוא מתאפס אחרי מרווח תפוגה (בערך שנייה) או באמצעות קריאה ל-API שצורך הפעלה (למשל window.open()).

שימו לב שממשקי API שונים שמוגבלים להפעלה בדרכים שונות מסתמכים על הפעלת משתמשים בדרכים שונות. ממשק ה-API החדש לא משנה אף אחת מההתנהגויות האלה שהן ספציפיות ל-API. לדוגמה, בכל הפעלה של משתמש יכול להיות רק חלון קופץ אחד כי window.open() צורכת הפעלת משתמש כפי שהייתה בעבר, Navigator.prototype.vibrate() ממשיך לפעול אם פריים (או אחת מתת-הפריימים שלו) אי פעם ראו פעולת משתמש, וכן הלאה.

מה משתנה?

  • בגרסה 2 של הפעלת המשתמש אפשר לראות את הגדרת החשיפה של הפעלת משתמשים בכל המסגרות: אינטראקציה של משתמש עם פריים מסוים תפעיל עכשיו את כל הפריימים שמכילים (ורק את הפריימים האלה) ללא קשר למקור שלהן. (ב-Chrome 72 יש לנו פתרון זמני להרחבת החשיפה לכל המסגרות מאותו מקור. אנחנו נסיר את הפתרון הזה ברגע שתהיה לנו דרך להעביר באופן מפורש את ההפעלה של המשתמשים למסגרות משנה).
  • כשממשק API שמוגבל על ידי הפעלה מופעל ממסגרת מופעלת, אבל מחוץ לקוד של מטפל באירועים, הוא יפעל כל עוד מצב ההפעלה של המשתמש הוא 'פעיל' (למשל, הוא לא פג תוקף ולא נוצל). לפני ההפעלה של המשתמש v2, היא הייתה נכשלת ללא תנאי.
  • אינטראקציות מרובות של משתמשים שאינן בשימוש במרווחי הזמן של התפוגה, שמשולבות להפעלה אחת שתואמת לאינטראקציה האחרונה.

דוגמאות לעקביות בממשקי API שמוגבלים להפעלה

בהמשך מופיעות שתי דוגמאות עם חלונות קופצים (שנפתחו באמצעות window.open()) שמראים איך הפעלת המשתמש v2 יוצרת התנהגות עקבית של ממשקי API שמוגבלים להפעלה.

setTimeout() שיחות מחוברות

הדוגמה הזו לקוחה מתוך ההדגמה של setTimeout(). אם handler של click ינסה לפתוח חלון קופץ תוך שנייה, סביר להניח שזה יצליח בלי קשר לאופן שבו הקוד "ירכיב" את ההשהיה. גרסה 2 של הפעלת המשתמש עומדת בציפייה הזו, לכן כל אחד מהגורמים המטפלים באירועים הבאים פותח חלון קופץ ב-click (עם עיכוב של 100 אלפיות השנייה):

function popupAfter100ms() {
  setTimeout(callWindowOpen, 100);
}

function asyncPopupAfter100ms() {
  setTimeout(popupAfter100ms, 0);
}

someButton.addEventListener('click', popupAfter100ms);
someButton.addEventListener('click', asyncPopupAfter100ms);

ללא הפעלת משתמש בגרסה 2, הגורם המטפל באירועים השני נכשל בכל הדפדפנים שבדקנו. (גם השיטה הראשונה נכשלת במקרים מסוימים).

שיחות postMessage() בכמה דומיינים

הנה דוגמה מההדגמה של postMessage(). נניח ש-handler של click בתת-מסגרת ממקורות שונים שולח שתי הודעות ישירות למסגרת ההורה. למסגרת ההורה צריכה להיות אפשרות לפתוח חלון קופץ עם קבלת אחת מההודעות האלה (אבל לא את שתיהן):

// Parent frame code
window.addEventListener('message', e => {
  if (e.data === 'open_popup' && e.origin === child_origin)
    window.open('about:blank');
});

// Child frame code:
someButton.addEventListener('click', () => {
  parent.postMessage('hi_there', parent_origin);
  parent.postMessage('open_popup', parent_origin);
});

בלי הפעלת המשתמש בגרסה 2, מסגרת ההורה לא יכולה לפתוח חלון קופץ כשמקבלים את ההודעה השנייה. גם ההודעה הראשונה תיכשל אם היא "מקושרת" למסגרת אחרת בין מקורות (כלומר, אם המקלט הראשון מעביר את ההודעה לאחרת).

זה עובד עם 'הפעלת משתמש' בגרסה 2, גם בצורה המקורית וגם עם השרשור.