ในบางกรณี เว็บแอปอาจต้องสร้างช่องทางการสื่อสารแบบ 2 ทางระหว่างหน้าเว็บกับ Service Worker
ตัวอย่างเช่น ใน PWA ของพอดแคสต์ คุณสามารถสร้างฟีเจอร์ที่ให้ผู้ใช้ดาวน์โหลดตอนต่างๆ เพื่อฟังแบบออฟไลน์ และอนุญาตให้ Service Worker แจ้งความคืบหน้าให้หน้าเว็บทราบเป็นประจำ เพื่อให้เทรดหลักอัปเดต UI ได้
ในคู่มือนี้ เราจะสำรวจวิธีต่างๆ ในการใช้การสื่อสารแบบ 2 ทางระหว่างบริบท Window และ Service Worker โดยสำรวจ API ต่างๆ, ไลบรารี Workbox รวมถึงกรณีขั้นสูงบางกรณี
การใช้ 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 นี้มี
การใช้ API ของเบราว์เซอร์
หากไลบรารี Workbox ไม่เพียงพอต่อความต้องการของคุณ ก็มี API ระดับต่ำกว่าหลายรายการที่พร้อมใช้งานเพื่อใช้การสื่อสาร"แบบ 2 ทาง" ระหว่างหน้าเว็บกับ Service Worker ซึ่งมีความคล้ายคลึงและแตกต่างกันดังนี้
ความคล้ายคลึงกัน:
- ในทุกกรณี การสื่อสารจะเริ่มต้นที่ฝั่งหนึ่งผ่านอินเทอร์เฟซ
postMessage()และได้รับที่อีกฝั่งหนึ่งโดยการใช้แฮนเดิลmessage - ในทางปฏิบัติ API ทั้งหมดที่มีอยู่ช่วยให้เราใช้ Use Case เดียวกันได้ แต่บาง API อาจช่วยให้การพัฒนาในบางสถานการณ์ง่ายขึ้น
ความแตกต่าง:
- โดยมีวิธีระบุอีกฝั่งของการสื่อสารที่แตกต่างกัน บางวิธีใช้การอ้างอิงที่ชัดเจนไปยังบริบทอื่น ขณะที่บางวิธีสามารถสื่อสารโดยนัยผ่านออบเจ็กต์พร็อกซีที่สร้างอินสแตนซ์ในแต่ละฝั่ง
- การรองรับเบราว์เซอร์จะแตกต่างกันไปในแต่ละแพลตฟอร์ม
Broadcast Channel API
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 หรือไคลเอ็นต์ที่เฉพาะเจาะจงก่อน
ข้อเสียคือ ณ ขณะที่เขียนบทความนี้ API ได้รับการรองรับจาก Chrome, Firefox และ Edge แต่เบราว์เซอร์อื่นๆ เช่น Safari ยังไม่รองรับ
Client API
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'});
}
});
Client API เป็นตัวเลือกที่ดีในการสื่อสารกับแท็บที่ใช้งานอยู่ทั้งหมดจาก Service Worker ได้อย่างง่ายดายด้วยวิธีที่ค่อนข้างตรงไปตรงมา เบราว์เซอร์หลักทั้งหมดรองรับ API นี้ แต่อาจใช้เมธอดบางอย่างไม่ได้ ดังนั้นโปรดตรวจสอบการรองรับของเบราว์เซอร์ก่อนนำไปใช้ในเว็บไซต์
ช่องทางของข้อความ
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 จะได้รับพอร์ต บันทึกการอ้างอิงไปยังพอร์ต และใช้พอร์ตเพื่อส่งข้อความไปยังอีกฝั่งหนึ่ง
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 รายการเพื่อจัดการกับสถานการณ์ที่เฉพาะเจาะจง ได้แก่ การขาดการเชื่อมต่อและการดาวน์โหลดที่ใช้เวลานาน
การซิงค์ในเบื้องหลัง
แอปแชทอาจต้องการตรวจสอบว่าข้อความจะไม่สูญหายเนื่องจากการเชื่อมต่อไม่ดี 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 จากเว็บ ดังนี้
การดึงข้อมูลในเบื้องหลัง
สำหรับงานที่ค่อนข้างสั้น เช่น การส่งข้อความหรือรายการ 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 ทิศทาง)
บ่อยครั้งที่ผู้ใช้อาจต้องการบริบทเดียวเพื่อสื่อสารกับอีกฝ่ายโดยไม่ต้องรับคำตอบ ดูคำแนะนำต่อไปนี้เพื่อดูวิธีใช้เทคนิคแบบทิศทางเดียวในหน้าเว็บจากและไปยัง Service Worker รวมถึง Use Case และตัวอย่างเวอร์ชันที่ใช้งานจริง
- คู่มือการแคชที่จำเป็น: การเรียกใช้ Service Worker จากหน้าเว็บเพื่อแคชทรัพยากรล่วงหน้า (เช่น ในสถานการณ์การดึงข้อมูลล่วงหน้า)
- อัปเดตการออกอากาศ: การเรียกหน้าเว็บจาก Service Worker เพื่อแจ้งข้อมูลอัปเดตที่สำคัญ (เช่น มีเว็บแอปเวอร์ชันใหม่พร้อมใช้งาน)