รูปแบบการแจ้งเตือนทั่วไป

เราจะมาดูรูปแบบการใช้งานทั่วไปบางส่วนสำหรับพุชจากเว็บ

การดำเนินการนี้จะเกี่ยวข้องกับการใช้ API ต่างๆ 2-3 รายการที่มีให้ใช้งานในโปรแกรมทำงานของบริการ

เหตุการณ์การปิดการแจ้งเตือน

ในส่วนสุดท้าย เราได้เห็นวิธีที่เราสามารถฟังเหตุการณ์ notificationclick

นอกจากนี้ยังมีเหตุการณ์ notificationclose ที่เรียกใช้หากผู้ใช้ปิดการแจ้งเตือนรายการใดรายการหนึ่งของคุณ (กล่าวคือ ผู้ใช้คลิกกากบาทหรือเลื่อนการแจ้งเตือนออกไปแทนการคลิกการแจ้งเตือน)

โดยปกติแล้ว เหตุการณ์นี้จะใช้สำหรับข้อมูลวิเคราะห์เพื่อติดตามการมีส่วนร่วมของผู้ใช้กับการแจ้งเตือน

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

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

การเพิ่มข้อมูลในการแจ้งเตือน

เมื่อได้รับข้อความ Push เป็นเรื่องปกติที่จะมีข้อมูลที่เป็นประโยชน์เฉพาะในกรณีที่ผู้ใช้คลิกการแจ้งเตือน เช่น URL ที่ควรเปิดเมื่อมีการคลิกการแจ้งเตือน

วิธีที่ง่ายที่สุดในการนำข้อมูลจากเหตุการณ์พุชมาแนบไปกับการแจ้งเตือนคือการเพิ่มพารามิเตอร์ 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);

ภายในเครื่องจัดการคลิก ระบบจะเข้าถึงข้อมูลได้ด้วย 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 ที่ต้องการ ซึ่งทำได้โดยใช้ clients.openWindow() API

ในเหตุการณ์ 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);

มาดูโค้ดกัน

ก่อนอื่น เราแยกวิเคราะห์หน้าเว็บตัวอย่างของเราโดยใช้ URL API นี่เป็นเคล็ดลับเด็ดๆ ที่ผมมาจาก Jeff Posnick การเรียก 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" เท่านั้น (กล่าวคือ ค้นหาแท็บและหน้าต่างเท่านั้น และยกเว้น Web Worker) includeUncontrolled ช่วยให้เราค้นหาแท็บทั้งหมดจากต้นทางของคุณที่ไม่ได้ควบคุมโดย Service Worker ปัจจุบันได้ ซึ่งก็คือ Service Worker ที่เรียกใช้โค้ดนี้ โดยปกติแล้ว คุณจะต้องระบุ includeUncontrolled เป็นค่าจริงเสมอเมื่อเรียกใช้ matchAll()

เรายึดคำสัญญาที่ให้ไว้เป็น promiseChain เพื่อส่งผ่านต่อไปยัง event.waitUntil() ในอนาคต และทำให้ผู้ปฏิบัติงานบริการของเรารักษาชีวิตต่อไปได้

เมื่อสัญญา 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);
  }
});

กำลังรวมการแจ้งเตือน

เราเห็นว่าการเพิ่มแท็กในการแจ้งเตือนเป็นการเลือกให้ระบบแทนที่การแจ้งเตือนที่มีอยู่ซึ่งมีแท็กเดียวกัน

อย่างไรก็ตาม คุณอาจมีความซับซ้อนมากขึ้นด้วยการยุบการแจ้งเตือนโดยใช้ Notifications API ลองใช้แอปแชท ซึ่งนักพัฒนาซอฟต์แวร์อาจต้องการการแจ้งเตือนใหม่ที่จะแสดงข้อความที่คล้ายกับ "คุณมี 2 ข้อความจาก Matt" แทนที่จะแสดงข้อความล่าสุดเท่านั้น

คุณสามารถทำได้หรือจัดการกับการแจ้งเตือนปัจจุบันในลักษณะอื่นๆ โดยใช้ 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

ผลที่ได้คือข้อความแรกจะมีลักษณะดังนี้

การแจ้งเตือนครั้งแรกโดยไม่รวม

การแจ้งเตือนครั้งที่ 2 จะยุบการแจ้งเตือนเป็นดังนี้

การแจ้งเตือนครั้งที่ 2 พร้อมกับการรวม

ข้อดีอย่างหนึ่งของวิธีนี้คือหากผู้ใช้เห็นการแจ้งเตือนปรากฏขึ้นมาอย่างใดอย่างหนึ่ง ระบบจะดูสอดคล้องกันมากกว่าการใช้ข้อความล่าสุดแทนการแจ้งเตือน

ข้อยกเว้นสำหรับกฎ

เราได้ระบุว่าคุณต้องแสดงการแจ้งเตือนเมื่อได้รับข้อความ Push ซึ่งเรื่องนี้ เป็นความจริงส่วนใหญ่ สถานการณ์หนึ่งที่คุณไม่จำเป็นต้องแสดงการแจ้งเตือน คือเมื่อผู้ใช้เปิดเว็บไซต์ของคุณและโฟกัส

ภายในกิจกรรมข้อความ 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

ภายในกิจกรรมข้อความ Push เราจะใช้ฟังก์ชันนี้เพื่อตัดสินใจว่าต้องแสดงการแจ้งเตือนหรือไม่

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) ไปยังหน้าดังกล่าว วิธีนี้ทำให้หน้าเว็บแสดงการแจ้งเตือนหรือการอัปเดตให้ผู้ใช้ทราบเพื่อแจ้งกิจกรรมดังกล่าว ซึ่งจะเป็นประโยชน์สำหรับสถานการณ์เมื่อมีการแจ้งเตือนเล็กๆ น้อยๆ ในหน้าเว็บที่ดีขึ้นและเป็นมิตรต่อผู้ใช้

สมมติว่าเราได้รับข้อความ Push และตรวจสอบว่าเว็บแอปกำลังมุ่งเน้นอยู่ในปัจจุบัน จากนั้นเราสามารถ "โพสต์ข้อความ" ในหน้าที่เปิดอยู่แต่ละหน้าได้ ดังนี้

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

ใน Listener ข้อความนี้ คุณจะทำอะไรก็ได้ที่ต้องการ แสดง UI ที่กำหนดเองในหน้าเว็บ หรือจะเพิกเฉยต่อข้อความไปเลยก็ได้

นอกจากนี้ โปรดทราบว่าหากคุณไม่กำหนด Listener ข้อความในหน้าเว็บ ข้อความจากโปรแกรมทำงานของบริการจะไม่ดำเนินการใดๆ

แคชหน้าเว็บและเปิดหน้าต่าง

สถานการณ์หนึ่งที่ไม่อยู่ในขอบเขตของคู่มือนี้แต่ก็คุ้มค่าที่จะพูดคุยกันคือ คุณสามารถปรับปรุง UX โดยรวมของเว็บแอปได้ด้วยการแคชหน้าเว็บที่คุณคาดว่าผู้ใช้จะเข้าชมหลังจากคลิกการแจ้งเตือนของคุณ

ซึ่งต้องตั้งค่า Service Worker ให้รองรับเหตุการณ์ fetch แต่หากคุณใช้ Listener เหตุการณ์ fetch อย่าลืมใช้ Listener เหตุการณ์ในเหตุการณ์ 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 Worker

ขั้นตอนถัดไป

ห้องทดลองการเขียนโค้ด