การแทนที่พื้นหลังหรือหน้ากิจกรรมด้วย 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 อื่นหรือไปยังเอกสารนอกหน้าจอ - ไม่ควรลงทะเบียนเครื่องมือฟังเหตุการณ์เพื่อตอบสนองต่อสัญญาที่ส่งคืนหรือภายในแฮนเดิลเหตุการณ์
- เนื่องจากไม่สามารถใช้งานร่วมกับ
XMLHttpRequest()ได้ คุณจึงต้องแทนที่การเรียกใช้อินเทอร์เฟซนี้ด้วยการเรียกใช้fetch() - เนื่องจากจะสิ้นสุดเมื่อไม่ได้ใช้งาน คุณจึงต้องคงสถานะของแอปพลิเคชันไว้แทนที่จะอาศัยตัวแปรส่วนกลาง การสิ้นสุด 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
chrome.offscreen.createDocument({
url: chrome.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 ไม่ได้ หากต้องการแก้ไขปัญหานี้ ให้ทำอย่างใดอย่างหนึ่งต่อไปนี้ ก่อนอื่น คุณสามารถแทนที่ด้วยการเรียกไปยังกลไกการจัดเก็บข้อมูลอื่นได้ เนมสเปซ chrome.storage.local จะรองรับกรณีการใช้งานส่วนใหญ่ แต่ก็มีตัวเลือกอื่นๆ ให้ใช้งานด้วย
นอกจากนี้ คุณยังย้ายการเรียกไปยังเอกสารนอกหน้าจอได้ด้วย ตัวอย่างเช่น หากต้องการย้ายข้อมูลที่จัดเก็บไว้ก่อนหน้านี้ใน localStorage ไปยังกลไกอื่น ให้ทำดังนี้
- สร้างเอกสารนอกหน้าจอด้วยกิจวัตรการแปลงและตัวแฮนเดิล
runtime.onMessage - เพิ่มกิจวัตรการแปลงลงในเอกสารนอกหน้าจอ
- ใน Service Worker ของส่วนขยาย ให้ตรวจสอบ
chrome.storageสำหรับข้อมูลของคุณ - หากไม่พบข้อมูล ให้สร้างเอกสารนอกหน้าจอและโทรหา
runtime.sendMessage()เพื่อเริ่มกระบวนการแปลง - ในตัวแฮนเดิล
runtime.onMessageที่คุณเพิ่มลงในเอกสารนอกหน้าจอ ให้เรียกใช้กิจวัตร Conversion
นอกจากนี้ ยังมีรายละเอียดบางอย่างเกี่ยวกับวิธีการทำงานของ Web Storage API ในส่วนขยายด้วย ดูข้อมูลเพิ่มเติมได้ที่พื้นที่เก็บข้อมูลและคุกกี้
ลงทะเบียน Listener แบบพร้อมกัน
เราไม่รับประกันว่าการลงทะเบียน Listener แบบไม่พร้อมกัน (เช่น ภายใน Promise หรือ Callback) จะทำงานใน Manifest V3 ลองพิจารณาโค้ดต่อไปนี้
chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
chrome.browserAction.setBadgeText({ text: badgeText });
chrome.browserAction.onClicked.addListener(handleActionClick);
});
วิธีนี้ใช้ได้กับหน้าพื้นหลังแบบถาวรเนื่องจากหน้าเว็บทำงานอยู่ตลอดเวลาและไม่มีการเริ่มต้นใหม่ ใน Manifest V3 ระบบจะเริ่มต้น Service Worker อีกครั้งเมื่อมีการเรียกใช้เหตุการณ์ ซึ่งหมายความว่าเมื่อเหตุการณ์ทริกเกอร์ ระบบจะไม่ลงทะเบียน Listener (เนื่องจากมีการเพิ่ม Listener แบบไม่พร้อมกัน) และจะพลาดเหตุการณ์
แต่ให้ย้ายการลงทะเบียน Listener เหตุการณ์ ไปที่ระดับบนสุดของสคริปต์แทน วิธีนี้จะช่วยให้ Chrome ค้นหาและเรียกใช้ตัวแฮนเดิลการคลิกของส่วนขยายได้ทันที แม้ว่าส่วนขยายจะยังดำเนินการตรรกะการเริ่มต้นไม่เสร็จก็ตาม
chrome.action.onClicked.addListener(handleActionClick);
chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
chrome.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; chrome.runtime.onMessage.addListener(({ type, name }) => { if (type === "set-name") { savedName = name; } }); chrome.browserAction.onClicked.addListener((tab) => { chrome.tabs.sendMessage(tab.id, { name: savedName }); });
สำหรับไฟล์ Manifest V3 ให้แทนที่ตัวแปรร่วมด้วยการเรียกใช้ Storage API
chrome.runtime.onMessage.addListener(({ type, name }) => { if (type === "set-name") { chrome.storage.local.set({ name }); } }); chrome.action.onClicked.addListener(async (tab) => { const { name } = await chrome.storage.local.get(["name"]); chrome.tabs.sendMessage(tab.id, { name }); });
เปลี่ยนตัวจับเวลาเป็นการปลุก
การใช้การดำเนินการที่ล่าช้าหรือเป็นระยะๆ โดยใช้วิธี setTimeout() หรือ setInterval() เป็นเรื่องปกติ แต่ API เหล่านี้อาจทำงานไม่สำเร็จใน Service Worker เนื่องจากระบบจะยกเลิกตัวจับเวลาทุกครั้งที่ Service Worker สิ้นสุดการทำงาน
// 3 minutes in milliseconds const TIMEOUT = 3 * 60 * 1000; setTimeout(() => { chrome.action.setIcon({ path: getRandomIconPath(), }); }, TIMEOUT);
แต่ให้ใช้ Alarms API แทน เช่นเดียวกับตัวฟังอื่นๆ คุณควรลงทะเบียนตัวฟังการปลุกในระดับบนสุดของสคริปต์
async function startAlarm(name, duration) { await chrome.alarms.create(name, { delayInMinutes: 3 }); } chrome.alarms.onAlarm.addListener(() => { chrome.action.setIcon({ path: getRandomIconPath(), }); });
ให้ Service Worker ทำงานต่อไป
Service Worker เป็นแบบที่ขับเคลื่อนด้วยเหตุการณ์ตามคำจำกัดความและจะสิ้นสุดเมื่อไม่มีการใช้งาน วิธีนี้จะช่วยให้ Chrome เพิ่มประสิทธิภาพและลดการใช้หน่วยความจำของส่วนขยายได้ ดูข้อมูลเพิ่มเติมได้ในเอกสารประกอบเกี่ยวกับวงจรของ Service Worker ในกรณีพิเศษ คุณอาจต้องใช้มาตรการเพิ่มเติมเพื่อให้มั่นใจว่า Service Worker จะทำงานได้นานขึ้น
เปิด Service Worker ไว้จนกว่าการดำเนินการที่ใช้เวลานานจะเสร็จสิ้น
ในระหว่างการดำเนินการของ Service Worker ที่ใช้เวลานานซึ่งไม่ได้เรียกใช้ API ของส่วนขยาย Service Worker อาจปิดตัวลงกลางการดำเนินการ ตัวอย่างเช่น
- คำขอ
fetch()อาจใช้เวลานานกว่า 5 นาที (เช่น การดาวน์โหลดขนาดใหญ่ในการเชื่อมต่อที่อาจไม่ดี) - การคำนวณแบบอะซิงโครนัสที่ซับซ้อนซึ่งใช้เวลามากกว่า 30 วินาที
หากต้องการขยายอายุการใช้งานของ Service Worker ในกรณีเหล่านี้ คุณสามารถเรียกใช้ API ส่วนขยายแบบง่ายเป็นระยะๆ เพื่อรีเซ็ตตัวนับการหมดเวลา โปรดทราบว่าการดำเนินการนี้สงวนไว้สำหรับกรณีพิเศษเท่านั้น และในสถานการณ์ส่วนใหญ่ มักจะมีวิธีที่ดีกว่าและเป็นไปตามแพลตฟอร์มในการบรรลุผลลัพธ์เดียวกัน
ตัวอย่างต่อไปนี้แสดงฟังก์ชันตัวช่วย waitUntil() ที่ทำให้ Service Worker ทำงานต่อไปจนกว่า Promise ที่ระบุจะได้รับการแก้ไข
async function waitUntil(promise) {
const keepAlive = setInterval(chrome.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 chrome.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 chrome.storage.local.get('last-heartbeat'))['last-heartbeat'];
}