החלפת הרקע או דפי האירוע באמצעות קובץ שירות (service worker)
קובץ שירות (service worker) מחליף את הרקע של התוסף או את דף האירועים כדי לוודא שהקוד ברקע לא יפעל בשרשור הראשי. כך התוספים פועלים רק כשצריך, וזה חוסך משאבים.
דפי רקע הם רכיב בסיסי בתוספים מאז שהם הוצגו. במילים פשוטות, דפי רקע מספקים סביבה שקיימת באופן עצמאי ללא קשר לחלון או לכרטיסייה אחרים. כך התוספים יכולים לעקוב אחרי אירועים ולהגיב להם.
בדף הזה מתוארות משימות להמרת דפי רקע ל-service workers של תוספים. מידע נוסף על קובצי שירות (service worker) של תוספים זמין במדריך טיפול באירועים באמצעות קובצי שירות ובקטע מידע על קובצי שירות של תוספים.
ההבדלים בין סקריפטים ברקע לבין Service Workers של תוספים
בהקשרים מסוימים, סקריפטים של שירותי תוספים נקראים 'סקריפטים של הרקע'. למרות שקובצי שירות של תוספים פועלים ברקע, המונח 'סקריפטים ברקע' מטעה קצת כי הוא מרמז על יכולות זהות. ההבדלים מפורטים בהמשך.
שינויים מדפי רקע
יש כמה הבדלים בין Service Workers לבין דפי רקע.
- הן פועלות מחוץ לשרשור הראשי, כלומר הן לא מפריעות לתוכן של התוסף.
- יש להם יכולות מיוחדות כמו יירוט של אירועי אחזור במקור של התוסף, כמו אלה שמופיעים בחלון קופץ של סרגל כלים.
- הם יכולים לתקשר ולקיים אינטראקציה עם הקשרים האחרים באמצעות ממשק הלקוחות.
שינויים שצריך לבצע
תצטרכו לבצע כמה שינויים בקוד כדי להתאים את הפונקציות של סקריפטים ברקע ושל Service Workers. קודם כל, הדרך שבה מציינים קובץ שירות (service worker) בקובץ מניפסט שונה מהדרך שבה מציינים סקריפטים ברקע. בנוסף:
- מכיוון שהם לא יכולים לגשת ל-DOM או לממשק
window, תצטרכו להעביר את הקריאות האלה ל-API אחר או למסמך מחוץ למסך. - אין לרשום מאזינים לאירועים בתגובה להבטחות שהוחזרו או בתוך קריאות חוזרות (callback) של אירועים.
- מכיוון שהם לא תואמים לאחור ל-
XMLHttpRequest(), תצטרכו להחליף את השיחות לממשק הזה בשיחות ל-fetch(). - מכיוון שהם מסתיימים כשלא משתמשים בהם, תצטרכו לשמור את מצבי האפליקציה במקום להסתמך על משתנים גלובליים. הפסקת פעולה של service workers יכולה גם להפסיק טיימרים לפני שהם מסיימים את הפעולה שלהם. תצטרכו להחליף אותם בהתראות.
בדף הזה מתוארות המשימות האלה בפירוט.
עדכון השדה 'background' במניפסט
ב-Manifest V3, דפי הרקע מוחלפים בקובץ שירות (service worker). בהמשך מפורטים השינויים בקובץ המניפסט.
- מחליפים את
"background.scripts"ב-"background.service_worker"ב-manifest.json. שימו לב: השדה"service_worker"מקבל מחרוזת, ולא מערך של מחרוזות. - הסרת
"background.persistent"מה-manifest.json.
{ ... "background": { "scripts": [ "backgroundContextMenus.js", "backgroundOauth.js" ], "persistent": false }, ... }
{ ... "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 למנגנון אחר:
- יוצרים מסמך מחוץ למסך עם שגרת המרה ומטפל
runtime.onMessage. - מוסיפים שגרת המרה למסמך מחוץ למסך.
- ב-service worker של התוסף, בודקים את
chrome.storageכדי לראות את הנתונים. - אם הנתונים לא נמצאים, יוצרים מסמך מחוץ למסך ומתקשרים אל
runtime.sendMessage()כדי להתחיל את תהליך ההמרה. - ב-handler
runtime.onMessageשהוספתם למסמך מחוץ למסך, קוראים לשגרה של ההמרה.
יש גם כמה ניואנסים לגבי האופן שבו ממשקי API של אחסון אתרים פועלים בתוספים. מידע נוסף זמין במאמר אחסון וקובצי Cookie.
רישום של פונקציות event listener באופן סינכרוני
אין ערובה לכך שרישום של listener באופן אסינכרוני (לדוגמה, בתוך אובייקט promise או קריאה חוזרת) יפעל במניפסט V3. כדאי לעיין בקוד הבא.
chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
chrome.browserAction.setBadgeText({ text: badgeText });
chrome.browserAction.onClicked.addListener(handleActionClick);
});
השיטה הזו פועלת עם דף רקע מתמשך כי הדף פועל כל הזמן ואף פעם לא מתבצעת אתחול מחדש שלו. ב-Manifest V3, Service Worker יאותחל מחדש כשהאירוע יישלח. כלומר, כשהאירוע מופעל, ה-listeners לא נרשמים (כי הם מתווספים באופן אסינכרוני), והאירוע לא מתועד.
במקום זאת, מעבירים את הרישום של event listener לרמה העליונה של הסקריפט. כך Chrome יוכל למצוא ולהפעיל באופן מיידי את פונקציית ה-handler של הקליקים בפעולה, גם אם התוסף לא סיים להריץ את לוגיקת ההפעלה שלו.
chrome.action.onClicked.addListener(handleActionClick);
chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
chrome.action.setBadgeText({ text: badgeText });
});
החלפת XMLHttpRequest() ב-fetch() גלובלי
אי אפשר להתקשר אל XMLHttpRequest() מ-service worker, מתוסף או בכל דרך אחרת. מחליפים את הקריאות מ-XMLHttpRequest() בסקריפט הרקע בקריאות ל-global fetch().
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 Workers הם זמניים, כלומר סביר להניח שהם יתחילו, יפעלו ויסתיימו שוב ושוב במהלך סשן הדפדפן של המשתמש. זה גם אומר שהנתונים לא זמינים באופן מיידי במשתנים גלובליים, כי ההקשר הקודם פורק. כדי לעקוף את הבעיה הזו, צריך להשתמש בממשקי API של אחסון כמקור אמין. בהמשך מופיעה דוגמה שמראה איך עושים את זה.
בדוגמה הבאה משתמשים במשתנה גלובלי כדי לאחסן שם. ב-service worker, יכול להיות שהמשתנה הזה יאופס כמה פעמים במהלך סשן הדפדפן של המשתמש.
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.
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 worker), כי הטיימרים מבוטלים בכל פעם שקובץ השירות מסתיים.
// 3 minutes in milliseconds const TIMEOUT = 3 * 60 * 1000; setTimeout(() => { chrome.action.setIcon({ path: getRandomIconPath(), }); }, TIMEOUT);
במקום זאת, צריך להשתמש ב-Alarms API. כמו במקרים אחרים של מאזינים, מאזיני אזעקה צריכים להיות רשומים ברמה העליונה של הסקריפט.
async function startAlarm(name, duration) { await chrome.alarms.create(name, { delayInMinutes: 3 }); } chrome.alarms.onAlarm.addListener(() => { chrome.action.setIcon({ path: getRandomIconPath(), }); });
שמירה על פעילות של Service Worker
הגדרתם של Service workers היא מבוססת-אירועים, והם יופסקו במקרה של חוסר פעילות. כך Chrome יכול לבצע אופטימיזציה של הביצועים ושל צריכת הזיכרון של התוסף. אפשר לקרוא פרטים נוספים במידע בנושא מחזור החיים של Service Worker. במקרים חריגים, יכול להיות שיהיה צורך באמצעים נוספים כדי להבטיח ש-service worker יישאר פעיל למשך זמן ארוך יותר.
שמירה על פעילות של Service Worker עד לסיום פעולה ארוכת טווח
במהלך פעולות ארוכות של קובץ שירות (service worker) שלא קוראות לממשקי API של תוספים, יכול להיות שקובץ השירות ייסגר באמצע הפעולה. דוגמאות:
- בקשת
fetch()שנמשכת יותר מחמש דקות (למשל, הורדה גדולה בחיבור לא יציב). - חישוב אסינכרוני מורכב שנמשך יותר מ-30 שניות.
כדי להאריך את משך החיים של Service Worker במקרים האלה, אפשר להפעיל מדי פעם API של תוסף טריוויאלי כדי לאפס את מונה הזמן הקצוב לתפוגה. חשוב לדעת שהאפשרות הזו שמורה למקרים חריגים בלבד, וברוב המקרים יש דרך טובה יותר להשיג את אותה התוצאה, בהתאם לפלטפורמה.
בדוגמה הבאה מוצגת פונקציית העזר waitUntil() ששומרת על פעילות של קובץ שירות (service worker) עד שאובייקט ה-promise שצוין מתקיימת:
async function waitUntil(promise) {
const keepAlive = setInterval(chrome.runtime.getPlatformInfo, 25 * 1000);
try {
await promise;
} finally {
clearInterval(keepAlive);
}
}
waitUntil(someExpensiveCalculation());
שמירה על פעילות רציפה של Service Worker
במקרים נדירים, יש צורך להאריך את משך החיים ללא הגבלה. זיהינו שהתרחישים הנפוצים ביותר הם שימוש ב-Google Workspace במהדורות Enterprise ו-Education, ואנחנו מאפשרים את זה באופן ספציפי במהדורות האלה, אבל אנחנו לא תומכים בזה באופן כללי. בנסיבות החריגות האלה, אפשר לשמור על פעילות של 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'];
}