ย้ายข้อมูลไปยัง Service Worker

การแทนที่หน้าเว็บพื้นหลังหรือหน้ากิจกรรมด้วย Service Worker

Service Worker จะแทนที่พื้นหลังหรือหน้ากิจกรรมของส่วนขยายเพื่อดูแลให้โค้ดพื้นหลังอยู่นอกชุดข้อความหลัก วิธีนี้ช่วยให้ส่วนขยายทำงานได้เมื่อจำเป็นเท่านั้นและเป็นการประหยัดทรัพยากร

หน้าเว็บพื้นหลังเป็นองค์ประกอบพื้นฐานของส่วนขยายมานับตั้งแต่ที่มีการเปิดตัว กล่าวให้ชัดเจนก็คือ หน้าเว็บพื้นหลังมีสภาพแวดล้อมการทำงานที่ไม่เกี่ยวข้องกับหน้าต่างหรือแท็บอื่นๆ วิธีนี้ช่วยให้ส่วนขยายสังเกตการณ์และตอบสนองต่อเหตุการณ์ได้

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

ความแตกต่างระหว่างสคริปต์ที่ทำงานอยู่เบื้องหลังและโปรแกรมทำงานของบริการส่วนขยาย

ในบางบริบท คุณจะเห็นโปรแกรมทำงานของบริการส่วนขยายที่เรียกว่า "สคริปต์พื้นหลัง" แม้ว่าโปรแกรมทำงานของบริการส่วนขยายจะทำงานในพื้นหลัง แต่การเรียกสคริปต์ที่ทำงานอยู่เบื้องหลังจะค่อนข้างทำให้เข้าใจผิดจากการบอกเป็นนัยว่ามีความสามารถเหมือนกัน ความแตกต่างจะอธิบายไว้ด้านล่าง

การเปลี่ยนแปลงจากหน้าพื้นหลัง

โปรแกรมทำงานของบริการมีความแตกต่างกับหน้าเว็บพื้นหลังอยู่หลายจุด

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

การเปลี่ยนแปลงที่คุณต้องดำเนินการ

คุณต้องทำการปรับเปลี่ยนโค้ด 2-3 อย่างเพื่อรองรับความแตกต่างระหว่างการทำงานของสคริปต์ที่ทำงานอยู่เบื้องหลังและโปรแกรมทำงานของบริการ ในการเริ่มต้น วิธีการระบุ Service Worker ในไฟล์ Manifest จะต่างจากการระบุสคริปต์พื้นหลัง คำอธิบายเพิ่มเติม

  • เนื่องจากผู้ใช้เข้าถึง DOM หรืออินเทอร์เฟซ window ไม่ได้ คุณจึงต้องย้ายการเรียกใช้ดังกล่าวไปยัง API อื่นหรือไปยังเอกสารนอกหน้าจอ
  • ไม่ควรลงทะเบียน Listener เหตุการณ์เพื่อตอบสนองต่อสัญญาที่ส่งกลับหรือโค้ดเรียกกลับภายในเหตุการณ์
  • เนื่องจากเข้ากันไม่ได้แบบย้อนหลังกับ XMLHttpRequest() คุณจึงต้องแทนที่การเรียกไปยังอินเทอร์เฟซนี้ด้วยการเรียก fetch()
  • เนื่องจากระบบจะยุติลงเมื่อไม่ได้ใช้งาน คุณจะต้องคงสถานะของแอปพลิเคชันไว้แทนที่จะใช้ตัวแปรร่วม การหยุดการทำงานของโปรแกรมทำงานของบริการยังสามารถหยุดตัวจับเวลาก่อนที่จะทำงานเสร็จได้ คุณจะต้องแทนที่ด้วยการปลุก

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

อัปเดตช่อง "พื้นหลัง" ในไฟล์ 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" เสมอ โปรดดูข้อมูลเพิ่มเติมที่ข้อมูลพื้นฐานเกี่ยวกับ Extension Service Worker

ย้ายการเรียกใช้ DOM และการเรียกใช้จากหน้าต่างไปยังเอกสารนอกหน้าจอ

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

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

สื่อสารกันระหว่างเอกสารนอกจอภาพกับโปรแกรมทำงานของบริการส่วนขยายโดยใช้การส่งข้อความ

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

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

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

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

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

ลงทะเบียน Listener พร้อมกัน

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

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

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

แต่ให้ย้ายการลงทะเบียน Listener เหตุการณ์ไปที่ระดับบนสุดของสคริปต์แทน วิธีนี้ช่วยให้ Chrome สามารถค้นหาและเรียกใช้เครื่องจัดการคลิกของการทำงานของคุณได้ทันที แม้ว่าส่วนขยายจะยังไม่ดำเนินการตามตรรกะการเริ่มต้นใช้งานจนเสร็จสมบูรณ์ก็ตาม

chrome.action.onClicked.addListener(handleActionClick);

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

แทนที่ XMLHttpRequest() ด้วยการเรียกข้อมูลส่วนกลาง()

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

คงสถานะ

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

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

สคริปต์พื้นหลังของไฟล์ 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

โปรแกรมทำงานของบริการไฟล์ 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

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

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

โปรแกรมทำงานของบริการไฟล์ 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 ไว้

โปรแกรมทำงานของบริการจะขับเคลื่อนด้วยเหตุการณ์ตามคำจำกัดความและจะยุติลงเมื่อไม่มีการใช้งาน วิธีนี้จะช่วยให้ Chrome เพิ่มประสิทธิภาพการทำงานและการใช้หน่วยความจำของส่วนขยายได้ ดูข้อมูลเพิ่มเติมในเอกสารประกอบเกี่ยวกับวงจรการทำงานของโปรแกรมทำงานของบริการ ในบางกรณีอาจต้องใช้มาตรการเพิ่มเติมเพื่อให้แน่ใจว่าโปรแกรมทำงานของบริการจะทำงานได้นานขึ้น

คงการทำงานของ Service Worker ไว้จนกว่าการดำเนินการที่ใช้เวลานานจะเสร็จสิ้น

ในระหว่างดำเนินการ Service Worker เป็นเวลานานซึ่งไม่ได้เรียกใช้ API ของส่วนขยายการโทร โปรแกรมทำงานของบริการอาจปิดลงระหว่างดำเนินการ ตัวอย่างเช่น

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

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

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

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