העברה ל-Service Worker

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

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

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

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

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

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

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

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

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

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

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

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

chrome.action.onClicked.addListener(handleActionClick);

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

החלפת XMLHttpRequest() ב-GlobalFetch()

אי אפשר להפעיל את 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);
אחזור()
const response = await fetch('https://www.example.com/greeting.json'')
console.log(response.statusText);

שמירת מצבים

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

סקריפט רקע של מניפסט מגרסה 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'];
}