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

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 ไปยังกลไกอื่น ให้ทำดังนี้

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

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;

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

Service Worker ของ Manifest V3
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 สิ้นสุดการทำงาน

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

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

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