ย้ายข้อมูลไปยัง 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 เริ่มจากวิธีระบุ 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
Manifest V2
{
  ...
  "background": {
    "scripts": [
      "backgroundContextMenus.js",
      "backgroundOauth.js"
    ],
    "persistent": false
  },
  ...
}
Manifest V3
{
  ...
  "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 ก่อนหน้านี้ไปยังกลไกอื่น ให้ทำดังนี้

  1. สร้างเอกสารนอกหน้าจอที่มีรูทีนการแปลงและแฮนเดิล runtime.onMessage
  2. เพิ่มรูทีนการแปลงลงในเอกสารนอกหน้าจอ
  3. ใน Service Worker ของส่วนขยาย ให้ตรวจสอบ browser.storage เพื่อดูข้อมูลของคุณ
  4. หากไม่พบข้อมูล ให้สร้างเอกสารนอกหน้าจอและเรียกใช้ runtime.sendMessage() เพื่อเริ่มกิจวัตรการแปลง
  5. ในแฮนเดิล 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()

XMLHttpRequest()
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);
fetch()
const response = await fetch('https://www.example.com/greeting.json'')
console.log(response.statusText);

สถานะคงอยู่

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

ตัวอย่างต่อไปนี้ใช้ตัวแปรส่วนกลางเพื่อจัดเก็บชื่อ ใน Service Worker ตัวแปรนี้อาจรีเซ็ตหลายครั้งตลอดเซสชันเบราว์เซอร์ของผู้ใช้

สคริปต์เบื้องหลังของ Manifest V2
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

Service Worker ของไฟล์ Manifest V3
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 สิ้นสุดลง

สคริปต์เบื้องหลังของ Manifest V2
// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  browser.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

แต่ให้ใช้ Alarms API แทน เช่นเดียวกับ Listener อื่นๆ คุณควรลงทะเบียน Listener การปลุกในระดับบนสุดของสคริปต์

Service Worker ของไฟล์ Manifest V3
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'];
}