ย้ายข้อมูลไปยัง 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 อื่นหรือไปยังเอกสารนอกหน้าจอ
  • ไม่ควรลงทะเบียนเครื่องมือฟังเหตุการณ์เพื่อตอบสนองต่อสัญญาที่ส่งคืนหรือภายใน Callback ของเหตุการณ์
  • เนื่องจากไม่สามารถใช้งานร่วมกับ XMLHttpRequest() ได้ คุณจึงต้องแทนที่การเรียกใช้อินเทอร์เฟซนี้ด้วยการเรียกใช้ fetch()
  • เนื่องจากจะสิ้นสุดเมื่อไม่ได้ใช้งาน คุณจึงต้องคงสถานะของแอปพลิเคชันไว้แทนที่จะอาศัยตัวแปรส่วนกลาง การสิ้นสุด 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ตัวแฮนเดิลที่คุณเพิ่มลงในเอกสารนอกหน้าจอ ให้เรียกใช้กิจวัตร Conversion

นอกจากนี้ ยังมีรายละเอียดบางอย่างเกี่ยวกับวิธีการทำงานของ Web Storage API ในส่วนขยายด้วย ดูข้อมูลเพิ่มเติมได้ที่พื้นที่เก็บข้อมูลและคุกกี้

ลงทะเบียน Listener แบบซิงโครนัส

การลงทะเบียน Listener แบบไม่พร้อมกัน (เช่น ภายใน Promise หรือการเรียกกลับ) ไม่รับประกันว่าจะทำงานใน 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 แทน เช่นเดียวกับตัวฟังอื่นๆ คุณควรลงทะเบียนตัวฟังการปลุกในระดับบนสุดของสคริปต์

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 ที่ใช้เวลานานซึ่งไม่ได้เรียกใช้ API ของส่วนขยาย Service Worker อาจปิดตัวลงกลางการดำเนินการ ตัวอย่างเช่น

  • fetch()คำขอที่อาจใช้เวลานานกว่า 5 นาที (เช่น การดาวน์โหลดขนาดใหญ่ในการเชื่อมต่อที่อาจไม่เสถียร)
  • การคำนวณแบบอะซิงโครนัสที่ซับซ้อนซึ่งใช้เวลามากกว่า 30 วินาที

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

ตัวอย่างต่อไปนี้แสดงwaitUntil()ฟังก์ชันตัวช่วยที่ทำให้ Service Worker ทำงานอยู่จนกว่า Promise ที่ระบุจะได้รับการแก้ไข

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'];
}