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

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