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