`Service Worker` ב-JavaScript

`Service Worker` ב-JavaScript

מאמר זה מסביר את המושג Service Worker ב-JavaScript.

נסביר שלב אחר שלב מהבסיס של Service Worker ועד לשליטה מעשית בחיוץ (Cache).

YouTube Video

offline.html
 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4  <meta charset="UTF-8">
 5  <title>Offline</title>
 6</head>
 7<body>
 8  <h1>You are offline</h1>
 9  <p>This is the offline fallback page.</p>
10</body>
11</html>
style.css
1body {
2  font-family: sans-serif;
3  background-color: #f0f0f0;
4  padding: 20px;
5}
6h1 {
7  color: #333;
8}
javascript-service-worker.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <div class="container">
111        <h2>HTML Sample</h2>
112        <button id="fetchBtn">Fetch Test</button>
113    </div>
114
115    <script>
116        // Override console.log to display messages in the #output element
117        (function () {
118            // Override console.log
119            const originalLog = console.log;
120            console.log = function (...args) {
121                originalLog.apply(console, args);
122                const message = document.createElement('div');
123                message.textContent = args.map(String).join(' ');
124                output.appendChild(message);
125            };
126
127            // Override console.error
128            const originalError = console.error;
129            console.error = function (...args) {
130                originalError.apply(console, args);
131                const message = document.createElement('div');
132                message.textContent = args.map(String).join(' ');
133                message.style.color = 'red'; // Color error messages red
134                output.appendChild(message);
135            };
136        })();
137
138        document.getElementById('executeBtn').addEventListener('click', () => {
139            // Prevent multiple loads
140            if (document.getElementById('externalScript')) return;
141
142            const script = document.createElement('script');
143            script.src = 'javascript-service-worker.js';
144            script.id = 'externalScript';
145            //script.onload = () => console.log('javascript-service-worker.js loaded and executed.');
146            //script.onerror = () => console.log('Failed to load javascript-service-worker.js.');
147            document.body.appendChild(script);
148        });
149    </script>
150</body>
151</html>

Service Worker ב-JavaScript

Service Worker היא תכונה של JavaScript העומדת בין הדפדפן לרשת ומאפשרת חיוץ בקשות ותמיכה בעבודה לא מקוונת (Offline). זו טכנולוגיה מרכזית של אפליקציות אינטרנט מתקדמות (PWAs) המעניקה חוויית שימוש הדומה לאפליקציות מקוריות.

מהו Service Worker?

Service Worker הוא קובץ JavaScript שפועל ב-thread רקע של הדפדפן. הוא פועל ב-thread נפרד מהעמוד, לא יכול לגשת ל-UI, אבל מסוגל ליירט בקשות רשת, לנהל חיוץ ולטפל בהתראות דחיפה (Push Notifications).

התכונות המרכזיות של Service Worker כוללות את הבאות:.

  • הוא עובד רק ב-HTTPS, למעט בסביבת Localhost.
  • הוא עושה שימוש ב-API אסינכרוני מבוסס Promise.
  • הוא מבוסס אירועים, ומשתמש באירועים כמו install, activate, fetch ו-push.

רישום Service Worker

ראשית, נכתוב את הקוד לרישום Service Worker בדפדפן.

 1if ('serviceWorker' in navigator) {
 2    window.addEventListener('load', () => {
 3        navigator.serviceWorker.register('/sw.js')
 4        .then(registration => {
 5            console.log(
 6                'Service Worker registered with scope:',
 7                registration.scope
 8            );
 9        })
10        .catch(error => {
11            console.error('Service Worker registration failed:', error);
12        });
13    });
14}

הסבר

  • השתמשו ב־navigator.serviceWorker.register() כדי לרשום את /sw.js (קובץ Service Worker).
  • ניתן להשתמש ב־then לטיפול בהצלחה ו־catch לטיפול בשגיאות בעת הרישום.
  • registration.scope מייצג את טווח הנתיבים (scope) שמושפע על ידי ה-Service Worker.
  • כברירת מחדל, ה-scope הוא התיקייה שבה נמצא הקובץ הרשום (במקרה זה, /sw.js) ותתי התיקיות שלה.

ה-scope של Service Worker

אם ברצונך להגביל את ה-scope, תוכל לציין את ה-scope באמצעות הארגומנט השני של register.

1navigator.serviceWorker.register('/sw.js', { scope: '/app/' })
2.then(registration => {
3    console.log(
4        'Service Worker registered with scope:',
5        registration.scope
6    );
7});

הסבר

  • בעזרת הגדרה זו, רק דפים תחת /app/ יהיו בשליטה של ה-Service Worker.

יצירת קובץ ה־Service Worker

לאחר מכן, צרו קובץ בשם sw.js ויישמו בו את האירועים הבסיסיים.

1// sw.js
2const CACHE_NAME = 'my-cache-v1';
3const urlsToCache = [
4    '/',
5    '/index.html',
6    '/styles.css',
7    '/script.js',
8    '/offline.html'
9];

קוד זה מגדיר רשימה של משאבים שיש לאחסן במטמון.

התפקידים והמנגנונים של כל אירוע

install

 1// Install event (initial caching)
 2self.addEventListener('install', event => {
 3    console.log('[ServiceWorker] Install');
 4    event.waitUntil(
 5        caches.open(CACHE_NAME).then(cache => {
 6            console.log('[ServiceWorker] Caching app shell');
 7            return cache.addAll(urlsToCache);
 8        })
 9    );
10});
  • self.addEventListener('install') מופעל כאשר Service Worker נרשם לראשונה. בשלב זה, קבצים נדרשים נשמרים מראש בחיוץ.

activate

 1// Activation event (delete old caches)
 2self.addEventListener('activate', event => {
 3    console.log('[ServiceWorker] Activate');
 4    event.waitUntil(
 5        caches.keys().then(keyList => {
 6            return Promise.all(keyList.map(key => {
 7                if (key !== CACHE_NAME) {
 8                    console.log('[ServiceWorker] Removing old cache:', key);
 9                    return caches.delete(key);
10                }
11            }));
12        })
13    );
14    return self.clients.claim();
15});
  • במהלך אירוע activate, חיווצים ישנים נמחקים כדי לייעל את השימוש במקום האחסון. רק החיוץ של הגרסה החדשה נשמר.

fetch

1// Fetch event (cache-first strategy)
2self.addEventListener('fetch', event => {
3  console.log('[ServiceWorker] Fetch', event.request.url);
4    event.respondWith(
5        caches.match(event.request).then(response => {
6            return response || fetch(event.request).catch(() => caches.match('/offline.html'));
7        })
8    );
9});

כל הבקשות ב-HTTP ניירטות — אם קיימת גרסה בחיוץ היא מוחזרת, ואם לא — הבקשה מבוצעת ברשת. בעת עבודה לא מקוונת, תוחזר עמוד חלופי (למשל, offline.html).

מאשר את הפעולה

בוא נבדוק בפועל כיצד ה-Service Worker פועל.

 1document.getElementById('fetchBtn').addEventListener('click', () => {
 2    fetch('/style.css')
 3        .then(response => response.text())
 4        .then(data => {
 5            console.log('Fetched data:', data);
 6        })
 7        .catch(error => {
 8            console.error('Fetch failed:', error);
 9        });
10});
  • כאן אנו בודקים את רישום ה-Service Worker ואת אופן הבאת המשאבים על ידי לחיצה על לחצן בדיקה.

דוגמאות לאסטרטגיות חיוץ (Caching)

הבאות הן אסטרטגיות חיוץ נפוצות:.

חיוץ תחילה (Cache First)

להלן דוגמה ליישום אסטרטגיית Cache First:.

1self.addEventListener('fetch', event => {
2    event.respondWith(
3        caches.match(event.request).then(response => {
4            return response || fetch(event.request);
5        })
6    );
7});
  • הקוד הזה מממש אסטרטגיית 'קודם המטמון', כלומר המשאב המבוקש יוחזר מהמטמון אם הוא זמין; אם לא, הוא יובא מהרשת.

רשת תחילה (Network First)

להלן דוגמה ליישום אסטרטגיית Network First:.

 1self.addEventListener('fetch', event => {
 2    event.respondWith(
 3        fetch(event.request)
 4            .then(response => {
 5                return caches.open(CACHE_NAME).then(cache => {
 6                    cache.put(event.request, response.clone());
 7                    return response;
 8                });
 9            })
10            .catch(() => caches.match(event.request))
11    );
12});
  • הקוד הזה מממש אסטרטגיית 'קודם הרשת', כלומר המשאב המבוקש יובא קודם כל מהרשת, ואם זה נכשל, יישלף מהמטמון.

חיוץ סגנונות (CSS) ו-JavaScript בלבד, וגישה ל-API בזמן אמת

להלן דוגמה ליישום שבו CSS ו-JavaScript נשמרים בחיוץ, בעוד הגישה ל-API מתבצעת בזמן אמת:.

 1self.addEventListener('fetch', event => {
 2    if (event.request.url.includes('/api/')) {
 3        // Fetch API responses in real-time without caching
 4        return;
 5    }
 6
 7    // Use cache-first strategy for static files
 8    event.respondWith(
 9        caches.match(event.request).then(response => {
10            return response || fetch(event.request);
11        })
12    );
13});
  • הקוד הזה תמיד פונה ל-API בזמן אמת ומיישם אסטרטגיית 'קודם המטמון' עבור קבצים סטטיים כמו גיליונות עיצוב ו-JavaScript.

מהלך עדכון

מהלך העדכון של Service Worker הוא כך:.

  1. sw.js חדש מזוהה.
  2. אירוע install מופעל.
  3. ממתין עד ש־Service Worker הקודם נהיה לא פעיל (idle).
  4. אירוע activate מופעל.
  5. מתבצעת החלפה ל־Service Worker החדש.
  6. אירוע controllerchange מתרחש.

זיהוי עדכון

לאחר התקנת Service Worker, הגרסה הישנה ממשיכה להיות בשימוש עד לביקור הבא. כדי להחיל עדכונים, נהוג להשתמש בקוד שמזהה עדכונים ומרענן את הדף.

1navigator.serviceWorker.addEventListener('controllerchange', () => {
2    window.location.reload();
3});
  • אירוע controllerchange מתרחש כאשר הקונטרולר של ה-Service Worker, כלומר ה-Service Worker ששולט בעמוד הנוכחי, משתנה.
  • דפים שכבר פתוחים ממשיכים להשתמש ב-Service Worker הנוכחי, וה-Service Worker החדש שהותקן לא נכנס לתוקף מיידית בדפים אלו. לכן, משתמשים בטכניקה שבה אירוע controllerchange משמש לזיהוי ש-controller חדש הופעל, ואז הדף נטען מחדש כדי להחיל את העדכון מיד.

אזהרות והמלצות לעבודה נכונה

בעת עבודה עם Service Worker, חשוב לזכור את הנקודות הבאות:.

  • נדרש HTTPS בגלל מגבלות אבטחה, זה לא יעבוד על http:// אלא רק ב־localhost.

  • שמות קבצים מגובבים (Hashed) שם החיוץ יכול להכיל את שם הקובץ, כתובת (URL) ומידע על גרסה.

  • תקשורת עם לקוחות (Clients) יש להשתמש ב־postMessage לצורך תקשורת בין ה־Service Worker ל־JavaScript של העמוד.

סיכום

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

תוכלו לעקוב אחר המאמר שלמעלה באמצעות Visual Studio Code בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.

YouTube Video