העברה ל-Service Worker

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

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

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

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

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

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

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

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

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

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

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

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

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

מעדכנים את השדה '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
  },
  ...
}
מניפסט V3
{
  ...
  "background": {
    "service_worker": "service_worker.js",
    "type": "module"
  }
  ...
}

השדה "service_worker" מקבל מחרוזת אחת. השדה "type" נדרש רק אם משתמשים במודולים של ES (באמצעות מילת המפתח import). הערך שלו תמיד יהיה "module". מידע נוסף זמין במאמר יסודות של Service Worker בתוספים

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

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

כדי להשתמש ב-Offscreen 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) בקובץ שירות. כדי לפתור את הבעיה, אפשר לבצע אחת משתי הפעולות הבאות. קודם כול, אפשר להחליף אותו בקריאות למנגנון אחסון אחר. מרחב השמות chrome.storage.local יתאים לרוב תרחישי השימוש, אבל יש אפשרויות אחרות.

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

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

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

רישום של מודעות מעקב באופן סינכרוני

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

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

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

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

chrome.action.onClicked.addListener(handleActionClick);

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

החלפת XMLHttpRequest()‏ ב-fetch()‎ גלובלי

אי אפשר להפעיל את XMLHttpRequest() משירות עבודה, מתווסף או באופן אחר. מחליפים את הקריאות מהסקריפט ברקע אל 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);

שמירת מצבים

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

סקריפט רקע של 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

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

שמירה על פעילות של שירות עובד עד לסיום פעולה ממושכת

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

  • בקשת fetch() שעשויה להימשך יותר מחמש דקות (למשל, הורדה גדולה בחיבור שעשוי להיות חלש).
  • חישוב אסינכרוני מורכב שנמשך יותר מ-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());

שמירה על פעילות רציפה של עובד שירות

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