การสื่อสารแบบ 2 ทางกับ Service Worker

Andrew Guan
Andrew Guan
Demián Renzulli
Demián Renzulli

ในบางกรณี เว็บแอปอาจต้องสร้างช่องทางการสื่อสารแบบ 2 ทางระหว่างหน้าเว็บกับ Service Worker

ตัวอย่างเช่น ใน PWA ของพอดแคสต์ คุณสามารถสร้างฟีเจอร์ที่ให้ผู้ใช้ดาวน์โหลดตอนต่างๆ เพื่อฟังแบบออฟไลน์ และอนุญาตให้ Service Worker แจ้งความคืบหน้าให้หน้าเว็บทราบเป็นประจำ เพื่อให้เทรดหลักอัปเดต UI ได้

ในคู่มือนี้ เราจะสำรวจวิธีต่างๆ ในการใช้การสื่อสารแบบ 2 ทางระหว่างบริบท Window และ Service Worker โดยสำรวจ API ต่างๆ, ไลบรารี Workbox รวมถึงกรณีขั้นสูงบางกรณี

แผนภาพแสดง Service Worker และหน้าเว็บที่แลกเปลี่ยนข้อความ

การใช้ Workbox

workbox-window คือชุดโมดูลของไลบรารี Workbox ที่มีไว้เพื่อเรียกใช้ในบริบทของหน้าต่าง คลาส Workbox มีเมธอด messageSW() เพื่อส่งข้อความไปยัง Service Worker ที่ลงทะเบียนของอินสแตนซ์และรอการตอบกลับ

โค้ดหน้าเว็บต่อไปนี้จะสร้างอินสแตนซ์ Workbox ใหม่และส่งข้อความไปยัง Service Worker เพื่อรับเวอร์ชัน

const wb = new Workbox('/sw.js');
wb.register();

const swVersion = await wb.messageSW({type: 'GET_VERSION'});
console.log('Service Worker version:', swVersion);

Service Worker จะใช้ Listener ข้อความที่ปลายอีกด้านหนึ่ง และตอบกลับ Service Worker ที่ลงทะเบียนไว้ดังนี้

const SW_VERSION = '1.0.0';

self.addEventListener('message', (event) => {
  if (event.data.type === 'GET_VERSION') {
    event.ports[0].postMessage(SW_VERSION);
  }
});

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

แผนภาพแสดงการสื่อสารแบบ 2 ทางระหว่างหน้าเว็บและ Service Worker โดยใช้ Workbox Window

การใช้ API ของเบราว์เซอร์

หากไลบรารี Workbox ไม่เพียงพอต่อความต้องการของคุณ ก็มี API ระดับต่ำกว่าหลายรายการที่พร้อมใช้งานเพื่อใช้การสื่อสาร"แบบ 2 ทาง" ระหว่างหน้าเว็บกับ Service Worker ซึ่งมีความคล้ายคลึงและแตกต่างกันดังนี้

ความคล้ายคลึงกัน:

  • ในทุกกรณี การสื่อสารจะเริ่มต้นที่ฝั่งหนึ่งผ่านอินเทอร์เฟซ postMessage() และได้รับที่อีกฝั่งหนึ่งโดยการใช้แฮนเดิล message
  • ในทางปฏิบัติ API ทั้งหมดที่มีอยู่ช่วยให้เราใช้ Use Case เดียวกันได้ แต่บาง API อาจช่วยให้การพัฒนาในบางสถานการณ์ง่ายขึ้น

ความแตกต่าง:

  • โดยมีวิธีระบุอีกฝั่งของการสื่อสารที่แตกต่างกัน บางวิธีใช้การอ้างอิงที่ชัดเจนไปยังบริบทอื่น ขณะที่บางวิธีสามารถสื่อสารโดยนัยผ่านออบเจ็กต์พร็อกซีที่สร้างอินสแตนซ์ในแต่ละฝั่ง
  • การรองรับเบราว์เซอร์จะแตกต่างกันไปในแต่ละแพลตฟอร์ม
แผนภาพแสดงการสื่อสารแบบ 2 ทางระหว่างหน้าเว็บกับ Service Worker และ API ของเบราว์เซอร์ที่พร้อมใช้งาน

Broadcast Channel API

Browser Support

  • Chrome: 54.
  • Edge: 79.
  • Firefox: 38.
  • Safari: 15.4.

Source

Broadcast Channel API ช่วยให้สามารถสื่อสารขั้นพื้นฐานระหว่างบริบทการท่องเว็บผ่านออบเจ็กต์ BroadcastChannel

ในการติดตั้งใช้งาน ก่อนอื่นแต่ละบริบทจะต้องสร้างอินสแตนซ์ออบเจ็กต์ BroadcastChannel ที่มีรหัสเดียวกัน รวมถึงส่งและรับข้อความจากออบเจ็กต์ดังกล่าว ดังนี้

const broadcast = new BroadcastChannel('channel-123');

ออบเจ็กต์ BroadcastChannel จะแสดงอินเทอร์เฟซ postMessage() เพื่อส่งข้อความไปยังบริบทการฟังใดก็ได้ ดังนี้

//send message
broadcast.postMessage({ type: 'MSG_ID', });

บริบทของเบราว์เซอร์ใดๆ สามารถฟังข้อความผ่านเมธอด onmessage ของออบเจ็กต์ BroadcastChannel ได้ดังนี้

//listen to messages
broadcast.onmessage = (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //process message...
  }
};

ดังที่เห็น ไม่มีการอ้างอิงที่ชัดเจนถึงบริบทที่เฉพาะเจาะจง จึงไม่จำเป็นต้องได้รับการอ้างอิงไปยัง Service Worker หรือไคลเอ็นต์ที่เฉพาะเจาะจงก่อน

แผนภาพแสดงการสื่อสารแบบ 2 ทางระหว่างหน้าเว็บกับ Service Worker โดยใช้ออบเจ็กต์ Broadcast Channel

ข้อเสียคือ ณ ขณะที่เขียนบทความนี้ API ได้รับการรองรับจาก Chrome, Firefox และ Edge แต่เบราว์เซอร์อื่นๆ เช่น Safari ยังไม่รองรับ

Client API

Browser Support

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

Source

Client API ช่วยให้คุณได้รับข้อมูลอ้างอิงไปยังออบเจ็กต์ WindowClient ทั้งหมดที่แสดงถึงแท็บที่ใช้งานอยู่ซึ่ง Service Worker ควบคุมอยู่

เนื่องจากหน้านี้ควบคุมโดย Service Worker รายเดียว จึงฟังและส่งข้อความไปยัง Service Worker ที่ทำงานอยู่โดยตรงผ่านอินเทอร์เฟซ serviceWorker ดังนี้

//send message
navigator.serviceWorker.controller.postMessage({
  type: 'MSG_ID',
});

//listen to messages
navigator.serviceWorker.onmessage = (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //process response
  }
};

ในทำนองเดียวกัน Service Worker จะฟังข้อความโดยใช้ Listener onmessage ดังนี้

//listen to messages
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //Process message
  }
});

หากต้องการสื่อสารกลับไปยังไคลเอ็นต์ Service Worker จะได้รับอาร์เรย์ของออบเจ็กต์ WindowClient โดยการเรียกใช้เมธอดต่างๆ เช่น Clients.matchAll() และ Clients.get() จากนั้นจะpostMessage()รายการใดก็ได้ต่อไปนี้

//Obtain an array of Window client objects
self.clients.matchAll(options).then(function (clients) {
  if (clients && clients.length) {
    //Respond to last focused tab
    clients[0].postMessage({type: 'MSG_ID'});
  }
});
แผนภาพแสดง Service Worker ที่สื่อสารกับอาร์เรย์ของไคลเอ็นต์

Client API เป็นตัวเลือกที่ดีในการสื่อสารกับแท็บที่ใช้งานอยู่ทั้งหมดจาก Service Worker ได้อย่างง่ายดายด้วยวิธีที่ค่อนข้างตรงไปตรงมา เบราว์เซอร์หลักทั้งหมดรองรับ API นี้ แต่อาจใช้เมธอดบางอย่างไม่ได้ ดังนั้นโปรดตรวจสอบการรองรับของเบราว์เซอร์ก่อนนำไปใช้ในเว็บไซต์

ช่องทางของข้อความ

Browser Support

  • Chrome: 2.
  • Edge: 12.
  • Firefox: 41.
  • Safari: 5.

Source

Message Channel ต้องมีการกำหนดและส่งพอร์ตจากบริบทหนึ่งไปยังอีกบริบทหนึ่งเพื่อสร้างช่องทางการสื่อสารแบบ 2 ทาง

ในการเริ่มต้นช่องทาง หน้าเว็บจะสร้างอินสแตนซ์ออบเจ็กต์ MessageChannel และใช้เพื่อส่งพอร์ตไปยัง Service Worker ที่ลงทะเบียนไว้ หน้านี้ยังใช้ Listener onmessage เพื่อรับข้อความจากบริบทอื่นๆ ด้วย

const messageChannel = new MessageChannel();

//Init port
navigator.serviceWorker.controller.postMessage({type: 'PORT_INITIALIZATION'}, [
  messageChannel.port2,
]);

//Listen to messages
messageChannel.port1.onmessage = (event) => {
  // Process message
};
แผนภาพแสดงหน้าเว็บที่ส่งพอร์ตไปยัง Service Worker เพื่อสร้างการสื่อสารแบบ 2 ทาง

Service Worker จะได้รับพอร์ต บันทึกการอ้างอิงไปยังพอร์ต และใช้พอร์ตเพื่อส่งข้อความไปยังอีกฝั่งหนึ่ง

let communicationPort;

//Save reference to port
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'PORT_INITIALIZATION') {
    communicationPort = event.ports[0];
  }
});

//Send messages
communicationPort.postMessage({type: 'MSG_ID'});

ปัจจุบันMessageChannelรองรับเบราว์เซอร์หลักทั้งหมด

API ขั้นสูง: การซิงค์ในเบื้องหลังและการดึงข้อมูลในเบื้องหลัง

ในคู่มือนี้ เราได้สำรวจวิธีใช้เทคนิคการสื่อสารแบบ 2 ทางสำหรับกรณีที่ค่อนข้างง่าย เช่น การส่งข้อความสตริงที่อธิบายการดำเนินการที่จะทำ หรือรายการ URL ที่จะแคชจากบริบทหนึ่งไปยังอีกบริบทหนึ่ง ในส่วนนี้ เราจะมาดู API 2 รายการเพื่อจัดการกับสถานการณ์ที่เฉพาะเจาะจง ได้แก่ การขาดการเชื่อมต่อและการดาวน์โหลดที่ใช้เวลานาน

การซิงค์ในเบื้องหลัง

Browser Support

  • Chrome: 49.
  • Edge: 79.
  • Firefox: not supported.
  • Safari: not supported.

Source

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

หน้าเว็บจะลงทะเบียน sync แทนอินเทอร์เฟซ postMessage() ดังนี้

navigator.serviceWorker.ready.then(function (swRegistration) {
  return swRegistration.sync.register('myFirstSync');
});

จากนั้น Service Worker จะรอเหตุการณ์ sync เพื่อประมวลผลข้อความ

self.addEventListener('sync', function (event) {
  if (event.tag == 'myFirstSync') {
    event.waitUntil(doSomeStuff());
  }
});

ฟังก์ชัน doSomeStuff() ควรแสดงผล Promise ที่ระบุความสำเร็จ/ความล้มเหลวของสิ่งที่พยายามทำ หากดำเนินการสำเร็จ การซิงค์จะเสร็จสมบูรณ์ หากไม่สำเร็จ ระบบจะกำหนดเวลาการซิงค์อีกครั้งเพื่อลองใหม่ การซิงค์ที่ลองใหม่จะรอการเชื่อมต่อและใช้ Exponential Backoff ด้วย

เมื่อดำเนินการเสร็จแล้ว Service Worker จะสื่อสารกลับไปยังหน้าเว็บเพื่ออัปเดต UI โดยใช้ API การสื่อสารที่สำรวจไว้ก่อนหน้านี้

Google Search ใช้การซิงค์ในเบื้องหลังเพื่อคงการค้นหาที่ล้มเหลวเนื่องจากการเชื่อมต่อไม่ดี และลองอีกครั้งในภายหลังเมื่อผู้ใช้ออนไลน์ เมื่อดำเนินการเสร็จแล้ว ระบบจะแจ้งผลลัพธ์ให้ผู้ใช้ทราบผ่านข้อความ Push จากเว็บ ดังนี้

แผนภาพแสดงหน้าเว็บที่ส่งพอร์ตไปยัง Service Worker เพื่อสร้างการสื่อสารแบบ 2 ทาง

การดึงข้อมูลในเบื้องหลัง

Browser Support

  • Chrome: 74.
  • Edge: 79.
  • Firefox: not supported.
  • Safari: not supported.

Source

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

Background Fetch API ช่วยให้คุณถ่ายโอนงานที่ใช้เวลานานไปยัง Service Worker ได้ เช่น การดาวน์โหลดภาพยนตร์ พอดแคสต์ หรือระดับของเกม

หากต้องการสื่อสารกับ Service Worker จากหน้าเว็บ ให้ใช้ backgroundFetch.fetch แทน postMessage() ดังนี้

navigator.serviceWorker.ready.then(async (swReg) => {
  const bgFetch = await swReg.backgroundFetch.fetch(
    'my-fetch',
    ['/ep-5.mp3', 'ep-5-artwork.jpg'],
    {
      title: 'Episode 5: Interesting things.',
      icons: [
        {
          sizes: '300x300',
          src: '/ep-5-icon.png',
          type: 'image/png',
        },
      ],
      downloadTotal: 60 * 1024 * 1024,
    },
  );
});

ออบเจ็กต์ BackgroundFetchRegistration ช่วยให้หน้าเว็บฟังเหตุการณ์ progress เพื่อติดตามความคืบหน้าของการดาวน์โหลดได้

bgFetch.addEventListener('progress', () => {
  // If we didn't provide a total, we can't provide a %.
  if (!bgFetch.downloadTotal) return;

  const percent = Math.round(
    (bgFetch.downloaded / bgFetch.downloadTotal) * 100,
  );
  console.log(`Download progress: ${percent}%`);
});
แผนภาพแสดงหน้าเว็บที่ส่งพอร์ตไปยัง Service Worker เพื่อสร้างการสื่อสารแบบ 2 ทาง
UI ได้รับการอัปเดตเพื่อแสดงความคืบหน้าของการดาวน์โหลด (ซ้าย) Service Worker ช่วยให้การดำเนินการทำงานต่อไปได้เมื่อปิดแท็บทั้งหมดแล้ว (ขวา)

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

ในคู่มือนี้ เราได้สำรวจกรณีการสื่อสารที่พบบ่อยที่สุดระหว่างหน้าเว็บกับ Service Worker (การสื่อสารแบบ 2 ทิศทาง)

บ่อยครั้งที่ผู้ใช้อาจต้องการบริบทเดียวเพื่อสื่อสารกับอีกฝ่ายโดยไม่ต้องรับคำตอบ ดูคำแนะนำต่อไปนี้เพื่อดูวิธีใช้เทคนิคแบบทิศทางเดียวในหน้าเว็บจากและไปยัง Service Worker รวมถึง Use Case และตัวอย่างเวอร์ชันที่ใช้งานจริง

  • คู่มือการแคชที่จำเป็น: การเรียกใช้ Service Worker จากหน้าเว็บเพื่อแคชทรัพยากรล่วงหน้า (เช่น ในสถานการณ์การดึงข้อมูลล่วงหน้า)
  • อัปเดตการออกอากาศ: การเรียกหน้าเว็บจาก Service Worker เพื่อแจ้งข้อมูลอัปเดตที่สำคัญ (เช่น มีเว็บแอปเวอร์ชันใหม่พร้อมใช้งาน)