דפוסי התראות נפוצים

מאט גאונט

נבחן כמה דפוסי הטמעה נפוצים של דחיפת אתרים.

לצורך כך יש להשתמש בכמה ממשקי API שונים הזמינים ב-Service Worker.

אירוע סגירת התראה

בקטע האחרון ראינו איך אפשר להאזין לאירועי notificationclick.

יש גם אירוע notificationclose שנקרא אם המשתמש סוגר את אחת מההתראות (כלומר, במקום ללחוץ על ההתראה, המשתמש לוחץ על החץ לצד השני או מחליק את ההתראה).

האירוע הזה משמש בדרך כלל לניתוח נתונים כדי לעקוב אחרי מעורבות המשתמשים בהתראות.

self.addEventListener('notificationclose', function (event) {
  const dismissedNotification = event.notification;

  const promiseChain = notificationCloseAnalytics();
  event.waitUntil(promiseChain);
});

הוספת נתונים להתראה

כשמתקבלת הודעת דחיפה, מקובל שיהיו נתונים שימושיים רק אם המשתמש לחץ על ההתראה. לדוגמה, כתובת ה-URL שאמורה להיפתח כשלוחצים על התראה.

הדרך הקלה ביותר לקבל נתונים מאירוע Push ולצרף אותם להתראות היא להוסיף פרמטר data לאובייקט האפשרויות שמועבר אל showNotification(), באופן הבא:

const options = {
  body:
    'This notification has data attached to it that is printed ' +
    "to the console when it's clicked.",
  tag: 'data-notification',
  data: {
    time: new Date(Date.now()).toString(),
    message: 'Hello, World!',
  },
};
registration.showNotification('Notification with Data', options);

בתוך handler של קליקים, אפשר לגשת לנתונים באמצעות event.notification.data.

const notificationData = event.notification.data;
console.log('');
console.log('The notification data has the following parameters:');
Object.keys(notificationData).forEach((key) => {
  console.log(`  ${key}: ${notificationData[key]}`);
});
console.log('');

פתיחת חלון

אחת התגובות הנפוצות ביותר להתראה היא לפתוח חלון או כרטיסייה לכתובת URL ספציפית. אפשר לעשות זאת באמצעות ה-API של clients.openWindow().

באירוע notificationclick, נריץ קוד כזה:

const examplePage = '/demos/notification-examples/example-page.html';
const promiseChain = clients.openWindow(examplePage);
event.waitUntil(promiseChain);

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

מיקוד בחלון קיים

כשהדבר אפשרי, כדאי להתמקד בחלון במקום לפתוח חלון חדש בכל פעם שהמשתמש לוחץ על התראה.

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

בהמשך לדוגמה הקודמת, נשנה את הקוד כדי לראות אם השדה /demos/notification-examples/example-page.html כבר פתוח.

const urlToOpen = new URL(examplePage, self.location.origin).href;

const promiseChain = clients
  .matchAll({
    type: 'window',
    includeUncontrolled: true,
  })
  .then((windowClients) => {
    let matchingClient = null;

    for (let i = 0; i < windowClients.length; i++) {
      const windowClient = windowClients[i];
      if (windowClient.url === urlToOpen) {
        matchingClient = windowClient;
        break;
      }
    }

    if (matchingClient) {
      return matchingClient.focus();
    } else {
      return clients.openWindow(urlToOpen);
    }
  });

event.waitUntil(promiseChain);

בואו נעבור על הקוד.

תחילה מנתחים את הדף לדוגמה באמצעות ממשק ה-API של כתובת האתר. זה טריק מגניב שמצאתי מג'ף פוסניק. קריאה ל-new URL() עם האובייקט location תחזיר כתובת URL מוחלטת אם המחרוזת שמועברת היא יחסית (כלומר, / יהפוך ל-https://example.com/).

אנחנו הופכים את כתובת ה-URL למוחלטת כדי שנוכל להתאים אותה לכתובת ה-URL של החלון בהמשך.

const urlToOpen = new URL(examplePage, self.location.origin).href;

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

const promiseChain = clients.matchAll({
  type: 'window',
  includeUncontrolled: true,
});

האפשרויות שמועברות אל matchAll מציינות לדפדפן שאנחנו רוצים לחפש רק לקוחות מסוג "window" (כלומר, פשוט לחפש כרטיסיות וחלונות ולא לכלול עובדי אינטרנט). includeUncontrolled מאפשר לנו לחפש את כל הכרטיסיות מהמקור שלכם שלא נמצאות בשליטת ה-Service Worker הנוכחי, כלומר ה-Service Worker שמריץ את הקוד הזה. באופן כללי, תמיד תרצו שincludeUncontrolled יהיה TRUE כשמתקשרים אל matchAll().

אנחנו מתעדים את ההבטחה שמוחזרת בתור promiseChain, כדי שנוכל להעביר אותה ל-event.waitUntil() מאוחר יותר ולשמור על ה-Service Worker במצב פעיל.

כשפג התוקף של ההבטחה matchAll(), אנחנו מבצעים איטרציה דרך לקוחות החלון שמוחזרים ומשווים את כתובות ה-URL שלהם לכתובת ה-URL שאנחנו רוצים לפתוח. אם אנחנו מוצאים התאמה, אנחנו מתמקדים בלקוח הזה כדי להציג את החלון הזה לתשומת הלב של המשתמשים. המיקוד מתבצע באמצעות הקריאה matchingClient.focus().

אם לא מצאנו לקוח תואם, נפתח חלון חדש, כמו בחלק הקודם.

.then((windowClients) => {
  let matchingClient = null;

  for (let i = 0; i < windowClients.length; i++) {
    const windowClient = windowClients[i];
    if (windowClient.url === urlToOpen) {
      matchingClient = windowClient;
      break;
    }
  }

  if (matchingClient) {
    return matchingClient.focus();
  } else {
    return clients.openWindow(urlToOpen);
  }
});

מיזוג התראות

ראינו שהוספת תג להתראות מבטאת את ההסכמה להתנהגות שבה כל התראה קיימת עם אותו תג מוחלפת.

אבל אפשר לייעל את התהליך בעזרת כיווץ ההתראות באמצעות ממשק ה-API של התראות. ניקח לדוגמה אפליקציית צ'אט, שבה המפתח עשוי לרצות לקבל התראה חדשה עם הודעה שדומה ל-"You have שתי messages from Matt", במקום להציג רק את ההודעה האחרונה.

אפשר לעשות זאת או לשנות את ההגדרות של ההתראות הקיימות בדרכים אחרות, באמצעות ה-API registration.getNotifications() ה-API שנותן גישה לכל ההתראות שגלויות כרגע מאפליקציית האינטרנט.

בואו נראה איך נוכל להשתמש ב-API הזה כדי ליישם את הדוגמה לצ'אט.

באפליקציית הצ'אט שלנו, נניח שלכל התראה יש נתונים מסוימים שכוללים שם משתמש.

הדבר הראשון שנרצה לעשות הוא למצוא התראות פתוחות למשתמש עם שם משתמש ספציפי. אנחנו נקבל את registration.getNotifications() ונעביר אותו בחזרה ונבדוק את notification.data כדי למצוא שם משתמש ספציפי:

const promiseChain = registration.getNotifications().then((notifications) => {
  let currentNotification;

  for (let i = 0; i < notifications.length; i++) {
    if (notifications[i].data && notifications[i].data.userName === userName) {
      currentNotification = notifications[i];
    }
  }

  return currentNotification;
});

השלב הבא הוא החלפת ההתראה הזו בהתראה חדשה.

באפליקציית ההודעות המזויפות הזו, נעקוב אחר מספר ההודעות החדשות על ידי הוספת ספירה לנתוני ההתראה החדשה, ונגדיל את מספר ההודעות בכל התראה חדשה.

.then((currentNotification) => {
  let notificationTitle;
  const options = {
    icon: userIcon,
  }

  if (currentNotification) {
    // We have an open notification, let's do something with it.
    const messageCount = currentNotification.data.newMessageCount + 1;

    options.body = `You have ${messageCount} new messages from ${userName}.`;
    options.data = {
      userName: userName,
      newMessageCount: messageCount
    };
    notificationTitle = `New Messages from ${userName}`;

    // Remember to close the old notification.
    currentNotification.close();
  } else {
    options.body = `"${userMessage}"`;
    options.data = {
      userName: userName,
      newMessageCount: 1
    };
    notificationTitle = `New Message from ${userName}`;
  }

  return registration.showNotification(
    notificationTitle,
    options
  );
});

אם מוצגת התראה, אנחנו צוברים את מספר ההודעות וקובעים את הכותרת ואת גוף ההודעה בהתאם. אם אין התראות, אנחנו יוצרים התראה חדשה עם הערך newMessageCount של 1.

התוצאה היא שההודעה הראשונה תיראה כך:

התראה ראשונה ללא מיזוג.

התראה שנייה תכווץ את ההתראות למצב זה:

התראה שנייה עם מיזוג.

היתרון בגישה הזו הוא שאם המשתמש יראה את ההתראות שמופיעות אחת על השנייה, הוא ייראה רציף יותר מאשר תחליף את ההתראה בהודעה האחרונה.

יוצא מן הכלל

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

באירוע Push, תוכלו לבדוק אם אתם צריכים להציג התראה או לא, על ידי בדיקת לקוחות החלון וחיפוש חלון ממוקד.

הקוד לקבלת כל החלונות ולחיפוש חלון עם מיקוד נראה כך:

function isClientFocused() {
  return clients
    .matchAll({
      type: 'window',
      includeUncontrolled: true,
    })
    .then((windowClients) => {
      let clientIsFocused = false;

      for (let i = 0; i < windowClients.length; i++) {
        const windowClient = windowClients[i];
        if (windowClient.focused) {
          clientIsFocused = true;
          break;
        }
      }

      return clientIsFocused;
    });
}

אנחנו משתמשים ב-clients.matchAll() כדי לקבל את כל לקוחות החלון שלנו, ולאחר מכן אנחנו בודקים אותם ובודקים את הפרמטר focused.

בתוך אירוע הדחיפה, נשתמש בפונקציה הזו כדי להחליט אם צריך להציג התראה:

const promiseChain = isClientFocused().then((clientIsFocused) => {
  if (clientIsFocused) {
    console.log("Don't need to show a notification.");
    return;
  }

  // Client isn't focused, we need to show a notification.
  return self.registration.showNotification('Had to show a notification.');
});

event.waitUntil(promiseChain);

שליחת הודעה לדף מאירוע דחיפה

ראינו שאפשר לדלג על הצגת התראה אם המשתמש נמצא כרגע באתר. אבל מה קורה אם אתם בכל זאת רוצים ליידע את המשתמש על כך שאירוע מסוים התרחש, אבל ההתראה נחשבת ליותר מדי גדולה?

אחת הגישה היא לשלוח הודעה מה-Service Worker אל הדף, כך שדף האינטרנט יוכל להציג למשתמש התראה או עדכון שיודיעו לו על האירוע. זו אפשרות שימושית במצבים שבהם התראה עדינה בדף היא טובה יותר וידידותית יותר למשתמש.

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

const promiseChain = isClientFocused().then((clientIsFocused) => {
  if (clientIsFocused) {
    windowClients.forEach((windowClient) => {
      windowClient.postMessage({
        message: 'Received a push message.',
        time: new Date().toString(),
      });
    });
  } else {
    return self.registration.showNotification('No focused windows', {
      body: 'Had to show a notification instead of messaging each page.',
    });
  }
});

event.waitUntil(promiseChain);

בכל אחד מהדפים, אנחנו מוסיפים מאזינים לאירועי הודעה כדי להאזין להודעות:

navigator.serviceWorker.addEventListener('message', function (event) {
  console.log('Received a message from service worker: ', event.data);
});

בכלי הזה ניתן לבצע כל מה שרוצים, להציג ממשק משתמש מותאם אישית בדף או להתעלם לגמרי מההודעה.

חשוב גם לציין שאם לא מגדירים event listener בדף האינטרנט, ההודעות מה-Service Worker לא יעשו דבר.

שמירת דף במטמון ופתיחת חלון

תרחיש אחד שלא נכלל במדריך הזה אבל כדאי לדון בו הוא שאפשר לשפר את חוויית המשתמש הכוללת באפליקציית האינטרנט על ידי שמירה במטמון של דפי אינטרנט שאתם מצפים שהמשתמשים ייכנסו אליהם אחרי שילחצו על ההתראה.

לשם כך צריך להגדיר את ה-Service Worker כך שיטפל באירועי fetch, אבל אם מטמיעים event listener של fetch, חשוב לנצל אותו לטובת האירוע push על ידי שמירה במטמון של הדף והנכסים שנדרשים לפני הצגת ההתראה.

תאימות דפדפן

האירוע של notificationclose

תמיכה בדפדפן

  • 50
  • 17
  • 44
  • 16

מקור

Clients.openWindow()

תמיכה בדפדפן

  • 40
  • 17
  • 44
  • 11.1

מקור

ServiceWorkerRegistration.getNotifications()

תמיכה בדפדפן

  • 40
  • 17
  • 44
  • 16

מקור

clients.matchAll()

תמיכה בדפדפן

  • 42
  • 17
  • 54
  • 11.1

מקור

מידע נוסף זמין בפוסט הזה בנושא Service work.

השלבים הבאים

שיעורי Lab