העברה ל-Service Worker

החלפת דפי רקע או דפי אירועים בקובץ שירות (service worker)

קובץ שירות (service worker) מחליף את דף הרקע או את דף האירוע של התוסף כדי לוודא שקוד הרקע לא פועל ב-thread הראשי. כך התוספים פועלים רק כשצריך, וזה חוסך במשאבים.

דפי רקע הם רכיב בסיסי של תוספים מאז שהם הושקו. במילים פשוטות, דפי רקע מספקים סביבה שפועלת בנפרד מכל חלון או כרטיסייה אחרים. כך התוספים יכולים לראות אירועים ולפעול בתגובה אליהם.

בדף הזה מתוארות המשימות להמרת דפי רקע ל-service workers של תוספים. מידע נוסף על קובצי שירות (service worker) של תוספים באופן כללי זמין במדריך טיפול באירועים באמצעות קובצי שירות (service worker) ובקטע מידע על קובצי שירות (service worker) של תוספים.

ההבדלים בין סקריפטים שפועלים ברקע לבין קובצי שירות של תוספים

בהקשרים מסוימים, קוראים ל-service workers של התוספים "סקריפטים ברקע". למרות שקובצי שירות של תוספים פועלים ברקע, השם "סקריפטים של דפי רקע" קצת מטעה כי הוא מרמז על יכולות זהות. ההבדלים מתוארים בהמשך.

שינויים מדפי רקע

יש כמה הבדלים בין קובצי Service Worker לבין דפי רקע.

  • הם פועלים מחוץ לשרשור הראשי, כלומר הם לא מפריעים לתוכן של התוסף.
  • יש להם יכולות מיוחדות, כמו יירוט אירועי אחזור במקור של התוסף, למשל אירועים מחלון קופץ של סרגל כלים.
  • הם יכולים לתקשר ולקיים אינטראקציה עם הקשרים אחרים באמצעות ממשק הלקוחות.

השינויים שצריך לבצע

תצטרכו לבצע כמה שינויים בקוד כדי להתאים אותו להבדלים בין האופן שבו פועלים סקריפטים ברקע לבין האופן שבו פועלים קובצי שירות (service workers). קודם כול, הדרך שבה מציינים קובץ Service Worker בקובץ המניפסט שונה מהדרך שבה מציינים סקריפטים של קבצים ברקע. בנוסף:

  • הם לא יכולים לגשת ל-DOM או לממשק window, ולכן צריך להעביר את הקריאות האלה ל-API אחר או למסמך מחוץ למסך.
  • אין לרשום רכיבי event listener בתגובה להבטחות (promises) שהוחזרו או בתוך קריאות חוזרות (callbacks) של אירועים.
  • מכיוון שהם לא תואמים לדורות קודמים של XMLHttpRequest(), צריך להחליף את הקריאות לממשק הזה בקריאות ל-fetch().
  • הם מסתיימים כשהם לא בשימוש, ולכן צריך לשמור את מצבי האפליקציה ולא להסתמך על משתנים גלובליים. סגירת קובצי ה-Service Worker יכולה גם להפסיק את הטיימרים לפני שהם מסתיימים. צריך להחליף אותן בהתראות.

בדף הזה מתוארות המשימות האלה בפירוט.

מעדכנים את השדה background במניפסט

ב-Manifest V3, דפי רקע מוחלפים בקובץ שירות (service worker). השינויים במניפסט מפורטים בהמשך.

  • מחליפים את "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 basics (היסודות של קובץ השירות (service worker) של התוסף).

העברת קריאות ל-DOM ולחלון למסמך מחוץ למסך

תוספים מסוימים צריכים גישה לאובייקטים של DOM וחלונות בלי לפתוח חלון או כרטיסייה חדשים באופן חזותי. Offscreen API תומך בתרחישים לדוגמה האלה על ידי פתיחה וסגירה של מסמכים שלא מוצגים, שנארזו עם התוסף, בלי להפריע לחוויית המשתמש. מלבד העברת הודעות, מסמכים מחוץ למסך לא משתפים ממשקי 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. יוצרים מסמך מחוץ למסך עם שגרת המרה ו-handler של runtime.onMessage.
  2. מוסיפים שגרת המרה למסמך מחוץ למסך.
  3. ב-service worker של התוסף, בודקים את הנתונים ב-browser.storage.
  4. אם הנתונים לא נמצאים, יוצרים מסמך מחוץ למסך וקוראים ל-runtime.sendMessage() כדי להתחיל את שגרת ההמרה.
  5. ב-handler של runtime.onMessage שהוספתם למסמך מחוץ למסך, קוראים לשגרת ההמרה.

יש גם כמה הבדלים באופן שבו ממשקי API לאחסון באינטרנט פועלים בתוספים. מידע נוסף זמין במאמר אחסון וקובצי Cookie.

רישום מאזינים באופן סינכרוני

לא מובטח שרישום מאזין באופן אסינכרוני (לדוגמה, בתוך הבטחה או קריאה חוזרת) יעבוד ב-Manifest V3. לדוגמה, נבחן את הקוד הבא.

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

השיטה הזו פועלת עם דף רקע קבוע כי הדף פועל כל הזמן ולא מאותחל מחדש. ב-Manifest V3, ה-service worker יופעל מחדש כשהאירוע יישלח. כלומר, כשהאירוע יופעל, פונקציות ה-listener לא יירשמו (כי הן נוספות באופן אסינכרוני), והאירוע יפוספס.

במקום זאת, העבירו את הרישום של רכיב ה-event listener לרמה העליונה של הסקריפט. כך Chrome יוכל למצוא ולהפעיל מיד את ה-click handler של הפעולה, גם אם התוסף לא סיים להפעיל את לוגיקת ההפעלה שלו.

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 הם זמניים, כלומר סביר להניח שהם יתחילו, יפעלו ויסתיימו שוב ושוב במהלך סשן של משתמש בדפדפן. זה גם אומר שהנתונים לא זמינים באופן מיידי במשתנים גלובליים, כי ההקשר הקודם נמחק. כדי לעקוף את הבעיה הזו, צריך להשתמש בממשקי 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 workers, כי הטיימרים מבוטלים בכל פעם שה-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() שעשויה להימשך יותר מחמש דקות (למשל, הורדה גדולה בחיבור שעלול להיות איטי).
  • חישוב אסינכרוני מורכב שנמשך יותר מ-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'];
}