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

Andrew Guan
Andrew Guan

ในบางกรณี เว็บแอปอาจต้องสร้างช่องทางการสื่อสารแบบ 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);

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

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

การใช้ Browser API

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

ความคล้ายคลึง

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

ความแตกต่าง

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

Broadcast Channel API

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

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

แหล่งที่มา

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 โดยใช้ออบเจ็กต์ช่องการออกอากาศ

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

Client API

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

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

แหล่งที่มา

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

ในทํานองเดียวกัน บริการเวิร์กเกอร์จะรับฟังข้อความด้วยการใช้ onmessage listener ดังนี้

//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 นี้ แต่บางเมธอดอาจไม่พร้อมใช้งาน ดังนั้นโปรดตรวจสอบการรองรับของเบราว์เซอร์ก่อนนำไปใช้งานในเว็บไซต์

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

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

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

แหล่งที่มา

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

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

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

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

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

  • Chrome: 49.
  • Edge: 79
  • Firefox: ไม่รองรับ
  • Safari: ไม่รองรับ

แหล่งที่มา

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

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

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

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

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

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

  • Chrome: 74
  • Edge: 79
  • Firefox: ไม่รองรับ
  • Safari: ไม่รองรับ

แหล่งที่มา

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

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 และตัวอย่างการใช้งานจริงได้จากคู่มือต่อไปนี้