การแทนที่หน้าพื้นหลังหรือหน้าเหตุการณ์ด้วย Service Worker
Service Worker จะแทนที่หน้าเบื้องหลังหรือหน้าเหตุการณ์ของส่วนขยายเพื่อให้โค้ดเบื้องหลังอยู่นอกเทรดหลัก ซึ่งจะช่วยให้ส่วนขยายทำงานเมื่อจำเป็นเท่านั้น จึงช่วยประหยัดทรัพยากรได้
หน้าเบื้องหลังเป็นองค์ประกอบพื้นฐานของส่วนขยายตั้งแต่เปิดตัว พูดง่ายๆ ก็คือ หน้าพื้นหลังจะสร้างสภาพแวดล้อมที่ทำงานแยกต่างหากจากหน้าต่างหรือแท็บอื่นๆ ซึ่งช่วยให้ส่วนขยายสังเกตและดำเนินการเพื่อตอบสนองต่อเหตุการณ์ได้
หน้านี้อธิบายงานสำหรับการแปลงหน้าพื้นหลังเป็น Service Worker ของส่วนขยาย ดูข้อมูลเพิ่มเติมเกี่ยวกับ Service Worker ของส่วนขยายโดยทั่วไปได้ที่บทแนะนำจัดการเหตุการณ์ด้วย Service Worker และส่วนเกี่ยวกับ Service Worker ของส่วนขยาย
ความแตกต่างระหว่างสคริปต์เบื้องหลังกับ Service Worker ของส่วนขยาย
ในบางบริบท คุณจะเห็น Service Worker ของส่วนขยายที่เรียกว่า "สคริปต์เบื้องหลัง" แม้ว่า Service Worker ของส่วนขยายจะทำงานในเบื้องหลัง แต่การเรียก Service Worker ว่าสคริปต์เบื้องหลังอาจทำให้เข้าใจผิดได้โดยนัยว่ามีความสามารถเหมือนกัน โดยความแตกต่างมีดังนี้
การเปลี่ยนแปลงจากหน้าพื้นหลัง
Service Worker มีความแตกต่างจากหน้าพื้นหลังหลายประการ
- โดยจะทำงานนอกเทรดหลัก ซึ่งหมายความว่าจะไม่รบกวนเนื้อหาของส่วนขยาย
- โดยมีความสามารถพิเศษ เช่น การสกัดกั้นเหตุการณ์การดึงข้อมูลในต้นทางของส่วนขยาย เช่น เหตุการณ์จากป๊อปอัปแถบเครื่องมือ
- โดยสามารถสื่อสารและโต้ตอบกับบริบทอื่นๆ ผ่านอินเทอร์เฟซไคลเอ็นต์
การเปลี่ยนแปลงที่คุณต้องทำ
คุณจะต้องปรับโค้ดเล็กน้อยเพื่อพิจารณาความแตกต่างระหว่างวิธีการทำงานของสคริปต์เบื้องหลังและ Service Worker เริ่มจากวิธีระบุ Service Worker ในไฟล์ Manifest จะแตกต่างจากวิธีระบุสคริปต์เบื้องหลัง คำอธิบายเพิ่มเติม
- เนื่องจากไม่สามารถเข้าถึง DOM หรืออินเทอร์เฟซ
windowคุณจึงต้องย้ายการเรียกดังกล่าวไปยัง API อื่นหรือไปยังเอกสารนอกหน้าจอ - ไม่ควรลงทะเบียน Listener เหตุการณ์เพื่อตอบสนองต่อสัญญาที่แสดงผลหรือภายใน Callback ของเหตุการณ์
- เนื่องจากอินเทอร์เฟซดังกล่าวไม่เข้ากันได้แบบย้อนหลังกับ
XMLHttpRequest()คุณจึงต้องแทนที่การเรียกอินเทอร์เฟซนี้ด้วยการเรียกfetch() - เนื่องจาก Service Worker จะสิ้นสุดการทำงานเมื่อไม่ได้ใช้งาน คุณจึงต้องคงสถานะแอปพลิเคชันไว้แทนที่จะอาศัยตัวแปรร่วม การสิ้นสุด Service Worker ยังสามารถสิ้นสุดตัวจับเวลาก่อนที่จะเสร็จสมบูรณ์ได้ด้วย คุณจะต้องแทนที่ด้วยการปลุก
หน้านี้จะอธิบายงานเหล่านี้โดยละเอียด
อัปเดตช่อง "background" ในไฟล์ Manifest
ใน Manifest V3 หน้าพื้นหลังจะถูกแทนที่ด้วย Service Worker การเปลี่ยนแปลงไฟล์ Manifest มีดังนี้
- แทนที่
"background.scripts"ด้วย"background.service_worker"ในmanifest.jsonโปรดทราบว่าช่อง"service_worker"จะใช้สตริง ไม่ใช่อาร์เรย์ของสตริง - นำ
"background.persistent"ออกจากmanifest.json
{ ... "background": { "scripts": [ "backgroundContextMenus.js", "backgroundOauth.js" ], "persistent": false }, ... }
{ ... "background": { "service_worker": "service_worker.js", "type": "module" } ... }
ช่อง "service_worker" จะใช้สตริงเดียว คุณจะต้องใช้ช่อง "type" ก็ต่อเมื่อใช้โมดูล ES (โดยใช้คีย์เวิร์ด import) ค่าของช่องนี้จะเป็น "module" เสมอ ดูข้อมูลเพิ่มเติมได้ที่ข้อมูลเบื้องต้นเกี่ยวกับ Service Worker ของส่วนขยาย
ย้ายการเรียก DOM และหน้าต่างไปยังเอกสารนอกหน้าจอ
ส่วนขยายบางรายการจำเป็นต้องเข้าถึง DOM และออบเจ็กต์หน้าต่างโดยไม่ต้องเปิดหน้าต่างหรือแท็บใหม่ให้เห็น Offscreen API รองรับ Use Case เหล่านี้โดยการเปิดและปิดเอกสารที่ไม่ได้แสดงซึ่งรวมอยู่ในส่วนขยาย โดยไม่รบกวนประสบการณ์ของผู้ใช้ เอกสารนอกหน้าจอจะไม่แชร์ API กับบริบทส่วนขยายอื่นๆ ยกเว้นการส่งข้อความ แต่จะทำหน้าที่เป็นหน้าเว็บแบบเต็มเพื่อให้ส่วนขยายโต้ตอบด้วย
หากต้องการใช้ Offscreen API ให้สร้างเอกสารนอกหน้าจอจาก Service Worker
browser.offscreen.createDocument({
url: browser.runtime.getURL('offscreen.html'),
reasons: ['CLIPBOARD'],
justification: 'testing the offscreen API',
});
ในเอกสารนอกหน้าจอ ให้ดำเนินการใดๆ ที่ก่อนหน้านี้คุณจะเรียกใช้ในสคริปต์เบื้องหลัง เช่น คุณสามารถคัดลอกข้อความที่เลือกในหน้าโฮสต์ได้
let textEl = document.querySelector('#text');
textEl.value = data;
textEl.select();
document.execCommand('copy');
สื่อสารระหว่างเอกสารนอกหน้าจอกับ Service Worker ของส่วนขยายโดยใช้การส่งข้อความ
แปลง localStorage เป็นประเภทอื่น
อินเทอร์เฟซ Storage ของแพลตฟอร์มเว็บ (เข้าถึงได้จาก window.localStorage) ไม่สามารถใช้ใน Service Worker ได้ หากต้องการแก้ไขปัญหานี้ ให้ทำอย่างใดอย่างหนึ่งต่อไปนี้ วิธีแรกคือคุณสามารถแทนที่ด้วยการเรียกใช้กลไกพื้นที่เก็บข้อมูลอื่น เนมสเปซ browser.storage.local จะรองรับกรณีการใช้งานส่วนใหญ่ แต่ก็มีตัวเลือกอื่นๆ ให้เลือกใช้
นอกจากนี้ คุณยังย้ายการโทรไปยังเอกสารนอกหน้าจอได้ด้วย ตัวอย่างเช่น หากต้องการย้ายข้อมูลที่จัดเก็บไว้ใน localStorage ก่อนหน้านี้ไปยังกลไกอื่น ให้ทำดังนี้
- สร้างเอกสารนอกหน้าจอที่มีรูทีนการแปลงและแฮนเดิล
runtime.onMessage - เพิ่มรูทีนการแปลงลงในเอกสารนอกหน้าจอ
- ใน Service Worker ของส่วนขยาย ให้ตรวจสอบ
browser.storageเพื่อดูข้อมูลของคุณ - หากไม่พบข้อมูล ให้สร้างเอกสารนอกหน้าจอและเรียกใช้
runtime.sendMessage()เพื่อเริ่มกิจวัตรการแปลง - ในแฮนเดิล
runtime.onMessageที่คุณเพิ่มลงในเอกสารนอกหน้าจอ ให้เรียกใช้รูทีนการแปลง
นอกจากนี้ยังมีรายละเอียดปลีกย่อยบางอย่างเกี่ยวกับวิธีการทำงานของ API พื้นที่เก็บข้อมูลบนเว็บในส่วนขยาย ดูข้อมูลเพิ่มเติมในพื้นที่เก็บข้อมูลและคุกกี้
ลงทะเบียน Listener แบบซิงโครนัส
การลงทะเบียน Listener แบบอะซิงโครนัส (เช่น ภายใน Promise หรือ Callback) ไม่รับประกันว่าจะทำงานในไฟล์ Manifest V3 ลองพิจารณาโค้ดต่อไปนี้
browser.storage.local.get(["badgeText"], ({ badgeText }) => {
browser.browserAction.setBadgeText({ text: badgeText });
browser.browserAction.onClicked.addListener(handleActionClick);
});
วิธีนี้ใช้ได้กับหน้าเบื้องหลังแบบถาวรเนื่องจากหน้าเว็บทำงานอย่างต่อเนื่องและไม่เคยเริ่มต้นใหม่ ในไฟล์ Manifest V3 ระบบจะเริ่มต้น Service Worker อีกครั้งเมื่อมีการส่งเหตุการณ์ ซึ่งหมายความว่าเมื่อเหตุการณ์เริ่มทำงาน ระบบจะไม่ลงทะเบียน Listener (เนื่องจากมีการเพิ่มแบบอะซิงโครนัส) และจะพลาดเหตุการณ์นั้นไป
แต่ให้ย้ายการลงทะเบียน Listener เหตุการณ์ไปที่ระดับบนสุดของสคริปต์แทน วิธีนี้ช่วยให้ Chrome ค้นหาและเรียกใช้ตัวแฮนเดิลการคลิกของการดำเนินการได้ทันที แม้ว่าส่วนขยายจะยังดำเนินการตรรกะการเริ่มต้นไม่เสร็จก็ตาม
browser.action.onClicked.addListener(handleActionClick);
browser.storage.local.get(["badgeText"], ({ badgeText }) => {
browser.action.setBadgeText({ text: badgeText });
});
แทนที่ XMLHttpRequest() ด้วย fetch() ส่วนกลาง
ไม่สามารถเรียกใช้ XMLHttpRequest() จาก Service Worker, ส่วนขยาย หรืออื่นๆ ได้ แทนที่การเรียกจากสคริปต์เบื้องหลังไปยัง XMLHttpRequest() ด้วยการเรียกไปยัง global fetch()
const xhr = new XMLHttpRequest(); console.log('UNSENT', xhr.readyState); xhr.open('GET', '/api', true); console.log('OPENED', xhr.readyState); xhr.onload = () => { console.log('DONE', xhr.readyState); }; xhr.send(null);
const response = await fetch('https://www.example.com/greeting.json'') console.log(response.statusText);
สถานะคงอยู่
Service Worker มีอายุสั้น ซึ่งหมายความว่ามีแนวโน้มที่จะเริ่ม ทำงาน และสิ้นสุดซ้ำๆ ในระหว่างเซสชันเบราว์เซอร์ของผู้ใช้ นอกจากนี้ยังหมายความว่าข้อมูลจะไม่พร้อมใช้งานในตัวแปรร่วมทันทีเนื่องจากบริบทก่อนหน้าถูกทำลายไปแล้ว หากต้องการหลีกเลี่ยงปัญหานี้ ให้ใช้ Storage API เป็นแหล่งข้อมูลที่เชื่อถือได้ ตัวอย่างจะแสดงวิธีดำเนินการนี้
ตัวอย่างต่อไปนี้ใช้ตัวแปรส่วนกลางเพื่อจัดเก็บชื่อ ใน Service Worker ตัวแปรนี้อาจรีเซ็ตหลายครั้งตลอดเซสชันเบราว์เซอร์ของผู้ใช้
let savedName = undefined; browser.runtime.onMessage.addListener(({ type, name }) => { if (type === "set-name") { savedName = name; } }); browser.browserAction.onClicked.addListener((tab) => { browser.tabs.sendMessage(tab.id, { name: savedName }); });
สำหรับไฟล์ Manifest V3 ให้แทนที่ตัวแปรร่วมด้วยการเรียกใช้ Storage API
browser.runtime.onMessage.addListener(({ type, name }) => { if (type === "set-name") { browser.storage.local.set({ name }); } }); browser.action.onClicked.addListener(async (tab) => { const { name } = await browser.storage.local.get(["name"]); browser.tabs.sendMessage(tab.id, { name }); });
แปลงตัวจับเวลาเป็นการปลุก
การใช้การดำเนินการที่ล่าช้าหรือเป็นระยะโดยใช้วิธี setTimeout() หรือ setInterval() เป็นเรื่องปกติ อย่างไรก็ตาม API เหล่านี้อาจทำงานไม่สำเร็จใน Service Worker เนื่องจากระบบจะยกเลิกตัวจับเวลาเมื่อใดก็ตามที่ Service Worker สิ้นสุดลง
// 3 minutes in milliseconds const TIMEOUT = 3 * 60 * 1000; setTimeout(() => { browser.action.setIcon({ path: getRandomIconPath(), }); }, TIMEOUT);
แต่ให้ใช้ Alarms API แทน เช่นเดียวกับ Listener อื่นๆ คุณควรลงทะเบียน Listener การปลุกในระดับบนสุดของสคริปต์
async function startAlarm(name, duration) { await browser.alarms.create(name, { delayInMinutes: 3 }); } browser.alarms.onAlarm.addListener(() => { browser.action.setIcon({ path: getRandomIconPath(), }); });
ทำให้ Service Worker ทำงานอยู่เสมอ
Service Worker จะทำงานตามเหตุการณ์ตามคำจำกัดความและจะสิ้นสุดเมื่อไม่มีการใช้งาน วิธีนี้จะช่วยให้ Chrome เพิ่มประสิทธิภาพและการใช้หน่วยความจำของส่วนขยายได้ ดูข้อมูลเพิ่มเติมได้ในเอกสารประกอบเกี่ยวกับวงจรของ Service Worker กรณีพิเศษอาจต้องใช้มาตรการเพิ่มเติมเพื่อให้มั่นใจว่า Service Worker จะยังคงทำงานอยู่เป็นเวลานานขึ้น
ทำให้ Service Worker ทำงานต่อไปจนกว่าการดำเนินการที่ใช้เวลานานจะเสร็จสิ้น
ในระหว่างการดำเนินการของ Service Worker ที่ใช้เวลานานซึ่งไม่ได้เรียกใช้ Extension API นั้น Service Worker อาจปิดตัวลงระหว่างการดำเนินการ ตัวอย่างเช่น
- คำขอ
fetch()อาจใช้เวลานานกว่า 5 นาที (เช่น การดาวน์โหลดขนาดใหญ่ในการเชื่อมต่อที่อาจไม่ดี) - การคำนวณแบบอะซิงโครนัสที่ซับซ้อนซึ่งใช้เวลานานกว่า 30 วินาที
หากต้องการยืดอายุการใช้งาน Service Worker ในกรณีเหล่านี้ คุณสามารถเรียกใช้ API ส่วนขยายเล็กๆ น้อยๆ เป็นระยะเพื่อรีเซ็ตตัวนับการหมดเวลา โปรดทราบว่าวิธีนี้สงวนไว้สำหรับกรณีพิเศษเท่านั้น และในสถานการณ์ส่วนใหญ่มักจะมีวิธีที่ดีกว่าและเป็นสำนวนเฉพาะของแพลตฟอร์มเพื่อให้ได้ผลลัพธ์เดียวกัน
ตัวอย่างต่อไปนี้แสดงฟังก์ชันตัวช่วย waitUntil() ที่ทำให้ Service Worker ทำงานต่อไปจนกว่าสัญญาที่ระบุจะได้รับการแก้ไข
async function waitUntil(promise) {
const keepAlive = setInterval(browser.runtime.getPlatformInfo, 25 * 1000);
try {
await promise;
} finally {
clearInterval(keepAlive);
}
}
waitUntil(someExpensiveCalculation());
ทำให้ Service Worker ทำงานอย่างต่อเนื่อง
ในบางกรณีที่เกิดขึ้นไม่บ่อยนัก อาจจำเป็นต้องขยายอายุการใช้งานออกไปอย่างไม่มีกำหนด เราพบว่าองค์กรและการศึกษาเป็นกรณีการใช้งานที่ใหญ่ที่สุด และเราอนุญาตให้ดำเนินการนี้ได้ในกรณีดังกล่าวโดยเฉพาะ แต่เราไม่รองรับการดำเนินการนี้โดยทั่วไป ในสถานการณ์พิเศษเหล่านี้ คุณสามารถทำให้ Service Worker ทำงานต่อไปได้โดยการเรียกใช้ API ส่วนขยายเล็กๆ น้อยๆ เป็นระยะๆ โปรดทราบว่าคำแนะนำนี้ใช้ได้กับส่วนขยายที่ทำงานในอุปกรณ์ที่มีการจัดการสำหรับกรณีการใช้งานขององค์กรหรือการศึกษาเท่านั้น ไม่อนุญาตให้ใช้ในกรณีอื่นๆ และทีมส่วนขยาย Chrome ขอสงวนสิทธิ์ในการดำเนินการกับส่วนขยายเหล่านั้นในอนาคต
ใช้ข้อมูลโค้ดต่อไปนี้เพื่อทำให้ Service Worker ทำงานอยู่เสมอ
/**
* Tracks when a service worker was last alive and extends the service worker
* lifetime by writing the current time to extension storage every 20 seconds.
* You should still prepare for unexpected termination - for example, if the
* extension process crashes or your extension is manually stopped at
* chrome://serviceworker-internals.
*/
let heartbeatInterval;
async function runHeartbeat() {
await browser.storage.local.set({ 'last-heartbeat': new Date().getTime() });
}
/**
* Starts the heartbeat interval which keeps the service worker alive. Call
* this sparingly when you are doing work which requires persistence, and call
* stopHeartbeat once that work is complete.
*/
async function startHeartbeat() {
// Run the heartbeat once at service worker startup.
runHeartbeat().then(() => {
// Then again every 20 seconds.
heartbeatInterval = setInterval(runHeartbeat, 20 * 1000);
});
}
async function stopHeartbeat() {
clearInterval(heartbeatInterval);
}
/**
* Returns the last heartbeat stored in extension storage, or undefined if
* the heartbeat has never run before.
*/
async function getLastHeartbeat() {
return (await browser.storage.local.get('last-heartbeat'))['last-heartbeat'];
}