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

หน้านี้จะอธิบายถึงงานเหล่านี้โดยละเอียด

อัปเดตช่อง "background" ในไฟล์ Manifest

ในไฟล์ Manifest V3 หน้าพื้นหลังจะถูกแทนที่โดยโปรแกรมทำงานของบริการ การเปลี่ยนแปลงไฟล์ 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 รองรับกรณีการใช้งานเหล่านี้โดยการเปิดและปิดเอกสารที่ไม่แสดงแพ็กเกจซึ่งมีส่วนขยายโดยไม่รบกวนประสบการณ์ของผู้ใช้ เอกสารนอกหน้าจอจะไม่แชร์ API กับบริบทส่วนขยายอื่นๆ ยกเว้นการส่งข้อความ แต่ทํางานเป็นหน้าเว็บแบบเต็มเพื่อให้ส่วนขยายโต้ตอบด้วย

หากต้องการใช้ Offscreen API ให้สร้างเอกสาร Offscreen จาก 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');

สื่อสารระหว่างเอกสารนอกหน้าจอและผู้ปฏิบัติงานบริการส่วนขยายโดยใช้การส่งข้อความ

แปลง localStorage เป็นประเภทอื่น

คุณใช้อินเทอร์เฟซ Storage ของแพลตฟอร์มเว็บ (เข้าถึงได้จาก window.localStorage) ใน Service Worker ไม่ได้ ในการแก้ไขปัญหานี้ ให้ทําอย่างใดอย่างหนึ่งต่อไปนี้ ก่อนอื่น คุณสามารถแทนที่การเรียกใช้ด้วยเมชานิซึมการจัดเก็บข้อมูลอื่น เนมสเปซ chrome.storage.local จะใช้กับกรณีการใช้งานส่วนใหญ่ได้ แต่ก็มีตัวเลือกอื่นๆ ให้เลือกด้วย

นอกจากนี้ คุณยังย้ายการเรียกไปยังเอกสารนอกหน้าจอได้ด้วย ตัวอย่างเช่น หากต้องการย้ายข้อมูลที่จัดเก็บไว้ก่อนหน้าใน localStorage ไปยังกลไกอื่น ให้ทำดังนี้

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

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

ลงทะเบียน Listener แบบซิงค์

การลงทะเบียน Listener แบบไม่พร้อมกัน (เช่น ในสัญญาหรือ Callback) ไม่มีการรับประกันว่าจะทำงานในไฟล์ Manifest V3 ลองดูโค้ดต่อไปนี้

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.browserAction.setBadgeText({ text: badgeText });
  chrome.browserAction.onClicked.addListener(handleActionClick);
});

ซึ่งใช้ได้กับหน้าเว็บที่ทำงานอยู่เบื้องหลังอย่างต่อเนื่องเนื่องจากหน้าเว็บทำงานอยู่ตลอดเวลาและไม่เคยรีนิไทซ์ ใน Manifest V3 ระบบจะเริ่มต้น Service Worker อีกครั้งเมื่อมีการเรียกเหตุการณ์ ซึ่งหมายความว่าเมื่อเหตุการณ์เริ่มทำงาน ผู้ฟังจะไม่ได้รับการลงทะเบียน (เนื่องจากมีการเพิ่มแบบไม่พร้อมกัน) และกิจกรรมจะพลาดไป

แต่ให้ย้ายการลงทะเบียนโปรแกรมรับฟังเหตุการณ์ไปยังระดับบนสุดของสคริปต์แทน วิธีนี้ช่วยให้ 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);

เก็บสถานะ

โปรแกรมทำงานของบริการเป็นการทำงานชั่วคราว ซึ่งหมายความว่าผู้ใช้มีแนวโน้มที่จะเริ่ม เรียกใช้ และยุติการทำงานซ้ำๆ ระหว่างเซสชันเบราว์เซอร์ของผู้ใช้ นอกจากนี้ ยังหมายความว่าข้อมูลจะไม่พร้อมใช้งานทันทีในตัวแปรร่วม เนื่องจากระบบได้แยกบริบทก่อนหน้านี้ออกแล้ว วิธีแก้ปัญหานี้คือใช้ 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() ที่ช่วยให้บริการของ Firebase ทำงานต่อไปจนกว่าพรอมต์หนึ่งๆ จะดำเนินการเสร็จสมบูรณ์

async function waitUntil(promise) = {
  const keepAlive = setInterval(chrome.runtime.getPlatformInfo, 25 * 1000);
  try {
    await promise;
  } finally {
    clearInterval(keepAlive);
  }
}

waitUntil(someExpensiveCalculation());

คงสถานะ Service Worker ไว้อย่างต่อเนื่อง

ในบางกรณีที่พบได้น้อย จำเป็นต้องขยายอายุการใช้งานแบบไม่มีกำหนด เราพบว่าองค์กรและการศึกษาเป็น Use Case ที่ใหญ่ที่สุด และเราอนุญาตกรณีการใช้งานนี้โดยเฉพาะ แต่เราไม่รองรับกรณีการใช้งานนี้โดยทั่วไป ในกรณีพิเศษเหล่านี้ คุณสามารถทำให้ 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'];
}