העברה ל-Service Worker

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

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

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

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

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

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

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

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

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

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

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

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

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

עדכון השדה 'רקע' במניפסט

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

  • מחליפים את "background.scripts" ב-"background.service_worker" בmanifest.json. חשוב לשים לב שהשדה "service_worker" מקבל מחרוזת ולא מערך של מחרוזות.
  • צריך להסיר את "background.persistent" מה-manifest.json.
מניפסט מגרסה 2
{
  ...
  "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 וחלון בלי לפתוח באופן חזותי חלון חדש או כרטיסייה חדשה. ב-Outscreen 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. יוצרים מסמך מחוץ למסך עם תרחיש המרות ו-handler של runtime.onMessage.
  2. מוסיפים תרחיש המרה למסמך שלא מוצג במסך.
  3. ב-Service Worker של התוסף בודקים את chrome.storage אם יש נתונים.
  4. אם הנתונים לא נמצאו, יוצרים מסמך מחוץ למסך ומתקשרים אל 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) יאותחל מחדש כשהאירוע יישלח. פירוש הדבר הוא שכאשר האירוע יופעל, המאזינים לא יירשמו (כי הם נוספים באופן אסינכרוני) והאירוע יחמיץ.

במקום זאת, צריך להעביר את הרישום של event 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);

מדינות קבועות

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

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

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

במניפסט מגרסה 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, כי הטיימרים מבוטלים בכל פעם ש-Service Worker מסתיים.

סקריפט רקע של מניפסט מגרסה V2
// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

במקום זאת, כדאי להשתמש ב-Notifications 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)

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

השארת קובץ שירות פעיל עד להשלמת פעולה ממושכת

במהלך פעולות ממושכות של קובץ השירות (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());

שמירה על קובץ שירות פעיל באופן רציף

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