העברה ל-Service Worker

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

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

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

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

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

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

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

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

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

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

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

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

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

מעדכנים את השדה '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". מידע נוסף זמין במאמר יסודות של Service Worker של תוספים

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

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

כדי להשתמש ב-Offscreen API, צריך ליצור מסמך offscreen מתוך ה-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');

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

המרת localStorage לסוג אחר

אי אפשר להשתמש בממשק Storage של פלטפורמת האינטרנט (שאפשר לגשת אליו מ-window.localStorage) ב-service worker. כדי לפתור את הבעיה, אפשר לבצע אחת משתי הפעולות הבאות: קודם כל, אפשר להחליף אותו בקריאות למנגנון אחסון אחר. מרחב השמות browser.storage.local מתאים לרוב תרחישי השימוש, אבל יש אפשרויות אחרות.

אפשר גם להעביר את השיחות למסמך מחוץ למסך. לדוגמה, כדי להעביר נתונים שאוחסנו בעבר ב-localStorage למנגנון אחר:

  1. יוצרים מסמך מחוץ למסך עם שגרת המרה ומטפל runtime.onMessage.
  2. מוסיפים שגרה להמרת מסמך למסמך מחוץ למסך.
  3. בודקים את הנתונים ב-browser.storage של העובד בשירות התוסף.
  4. אם הנתונים לא נמצאים, יוצרים מסמך מחוץ למסך ומתקשרים אל runtime.sendMessage() כדי להתחיל את תהליך ההמרה.
  5. ב-handler‏ runtime.onMessage שהוספתם למסמך מחוץ למסך, קוראים לשגרה של ההמרה.

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

רישום של פונקציות event listener באופן סינכרוני

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

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

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

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

המרת טיימרים לשעונים מעוררים

בדרך כלל משתמשים בפעולות מושהות או תקופתיות באמצעות ה-methods‏ 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 Workers היא מבוססת-אירועים, והם יסתיימו במקרה של חוסר פעילות. כך 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

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