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

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

ซึ่งจะเกี่ยวข้องกับการใช้ API ต่างๆ 2-3 รายการที่มีอยู่ใน Service Worker

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

ในส่วนที่แล้ว เราได้ดูวิธีฟังเหตุการณ์ notificationclick

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

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

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

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

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

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

วิธีง่ายที่สุดในการนําข้อมูลจากเหตุการณ์ Push และแนบไปกับการแจ้งเตือนคือการเพิ่มพารามิเตอร์ data ไปยังออบเจ็กต์ options ที่ส่งไปยัง 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 แบบสัมบูรณ์เพื่อให้จับคู่กับ URL ของกรอบเวลาในภายหลังได้

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

จากนั้นเราจะได้รายการออบเจ็กต์ WindowClient ซึ่งเป็นรายการแท็บและหน้าต่างที่เปิดอยู่ในปัจจุบัน (โปรดทราบว่าแท็บเหล่านี้เป็นแท็บสำหรับต้นทางของคุณเท่านั้น)

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

ตัวเลือกที่ส่งไปยัง matchAll จะแจ้งให้เบราว์เซอร์ทราบว่าเราต้องการค้นหาเฉพาะไคลเอ็นต์ประเภท "หน้าต่าง" เท่านั้น (กล่าวคือ ค้นหาเฉพาะแท็บและหน้าต่าง และยกเว้น Web Worker) includeUncontrolled ช่วยให้เราค้นหาแท็บทั้งหมดจากต้นทางของคุณที่ไม่ได้ควบคุมโดย Service Worker ปัจจุบัน เช่น Service Worker ที่เรียกใช้โค้ดนี้ โดยทั่วไปคุณจะต้องให้ includeUncontrolled เป็นจริงเสมอเมื่อโทรหา matchAll()

เราบันทึก Promise ที่แสดงผลเป็น 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);
  }
});

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

เราพบว่าการเพิ่มแท็กในการแจ้งเตือนจะเลือกใช้ลักษณะการทำงานที่จะแทนที่การแจ้งเตือนที่มีอยู่ด้วยแท็กเดียวกัน

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

คุณทําเช่นนี้หรือจัดการการแจ้งเตือนปัจจุบันด้วยวิธีอื่นๆ ก็ได้โดยใช้ API ของ registration.getNotifications() ซึ่งจะช่วยให้คุณเข้าถึงการแจ้งเตือนทั้งหมดที่แสดงอยู่ในขณะนี้สําหรับเว็บแอป

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

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

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

ในโปรแกรมรับฟังข้อความนี้ คุณจะทําสิ่งใดก็ได้ที่ต้องการ เช่น แสดง UI ที่กําหนดเองในหน้าเว็บ หรือละเว้นข้อความไปเลย

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

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

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

ซึ่งต้องมีการตั้งค่า Service Worker เพื่อจัดการเหตุการณ์ fetch แต่หากคุณใช้ fetch event listener โปรดใช้ประโยชน์จากเหตุการณ์ push โดยการแคชหน้าเว็บและชิ้นงานที่จําเป็นก่อนแสดงการแจ้งเตือน

ความเข้ากันได้กับเบราว์เซอร์

เหตุการณ์ notificationclose

การรองรับเบราว์เซอร์

  • Chrome: 50
  • Edge: 17.
  • Firefox: 44
  • Safari: 16

แหล่งที่มา

Clients.openWindow()

การรองรับเบราว์เซอร์

  • Chrome: 40
  • ขอบ: 17
  • Firefox: 44
  • Safari: 11.1

แหล่งที่มา

ServiceWorkerRegistration.getNotifications()

การรองรับเบราว์เซอร์

  • Chrome: 40
  • Edge: 17.
  • Firefox: 44
  • Safari: 16.

แหล่งที่มา

clients.matchAll()

การรองรับเบราว์เซอร์

  • Chrome: 42
  • Edge: 17.
  • Firefox: 54
  • Safari: 11.1

แหล่งที่มา

ดูข้อมูลเพิ่มเติมได้ในข้อมูลเบื้องต้นเกี่ยวกับโพสต์ Service Worker

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

Codelabs