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

Andrew Guan
Andrew Guan

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

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

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

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

การใช้ Workbox

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

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

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

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

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

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

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

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

API ช่องการออกอากาศ

การสนับสนุนเบราว์เซอร์

  • 54
  • 79
  • 38
  • 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...
  }
};

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

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

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

API ไคลเอ็นต์

การสนับสนุนเบราว์เซอร์

  • 40
  • 17
  • 44
  • 11.1

แหล่งที่มา

ไคลเอ็นต์ 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
  }
};

ในทำนองเดียวกัน โปรแกรมทำงานของบริการจะฟังข้อความโดยใช้ Listener onmessage ดังนี้

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

หากต้องการสื่อสารกลับไปหาไคลเอ็นต์ใดๆ โปรแกรมทำงานของบริการจะรับอาร์เรย์ของออบเจ็กต์ 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'});
  }
});
แผนภาพแสดงโปรแกรมทำงานของบริการที่กำลังสื่อสารกับไคลเอ็นต์จำนวนมาก

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

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

การสนับสนุนเบราว์เซอร์

  • 2
  • 12
  • 41
  • 5

แหล่งที่มา

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

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

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

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

การสนับสนุนเบราว์เซอร์

  • 49
  • 79
  • x
  • x

แหล่งที่มา

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

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

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

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

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

การสนับสนุนเบราว์เซอร์

  • 74
  • 79
  • x
  • x

แหล่งที่มา

สำหรับงานที่ค่อนข้างสั้น เช่น การส่งข้อความหรือรายการ 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}%`);
});
แผนภาพที่แสดงหน้าเว็บที่ส่งผ่านพอร์ตไปยังโปรแกรมทำงานของบริการ เพื่อสร้างการสื่อสารแบบ 2 ทาง
ระบบจะอัปเดต UI เพื่อระบุความคืบหน้าของการดาวน์โหลด (ซ้าย) ด้วยการทำงานของโปรแกรมทำงานของบริการ การดำเนินงานนี้จะสามารถทำงานต่อไปเมื่อปิดแท็บทั้งหมดแล้ว (ขวา)

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

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

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

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