`Service Worker` w JavaScript

`Service Worker` w JavaScript

Ten artykuł wyjaśnia koncepcję Service Worker w JavaScript.

Wyjaśnimy krok po kroku od podstaw Service Worker do praktycznego zarządzania pamięcią podręczną.

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 w JavaScript

Service Worker to funkcja JavaScript, która znajduje się pomiędzy przeglądarką a siecią, umożliwiając buforowanie żądań oraz obsługę trybu offline. Jest to kluczowa technologia aplikacji progresywnych (PWA), która zapewnia użytkownikom wrażenia podobne do aplikacji natywnych.

Czym jest Service Worker?

Service Worker to plik JavaScript, który działa w wątku w tle przeglądarki. Działa w oddzielnym wątku od strony, nie ma dostępu do interfejsu użytkownika, ale może przechwytywać żądania sieciowe, zarządzać pamięcią podręczną oraz obsługiwać powiadomienia push.

Kluczowe cechy Service Worker to:.

  • Działa tylko przez HTTPS, z wyjątkiem localhosta.
  • Wykorzystuje asynchroniczne API oparte na Promise.
  • Jest oparty na zdarzeniach, takich jak install, activate, fetch i push.

Rejestrowanie Service Worker

Najpierw napiszmy kod rejestrujący Service Worker w przeglądarce.

 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}

Wyjaśnienie

  • Użyj navigator.serviceWorker.register(), aby zarejestrować /sw.js (plik Service Worker).
  • Możesz użyć then do obsługi sukcesu oraz catch do obsługi błędów podczas rejestracji.
  • registration.scope oznacza zakres ścieżki (obszar), na który wpływa Service Worker.
  • Domyślnie zakresem jest katalog, w którym znajduje się zarejestrowany plik (w tym przypadku /sw.js) oraz jego podkatalogi.

Zakres działania Service Worker

Jeśli chcesz ograniczyć zakres działania, możesz określić scope jako drugi argument funkcji 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});

Wyjaśnienie

  • Przy tym ustawieniu tylko strony znajdujące się pod /app/ będą kontrolowane przez Service Worker.

Tworzenie pliku Service Worker

Następnie utwórz plik o nazwie sw.js i zaimplementuj podstawowe zdarzenia.

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];

Ten kod definiuje listę zasobów do zbuforowania.

Role i mechanizmy poszczególnych zdarzeń

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') jest wywoływany, gdy Service Worker jest rejestrowany po raz pierwszy. Na tym etapie niezbędne pliki są wstępnie buforowane.

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});
  • Podczas zdarzenia activate, stare pamięci podręczne są usuwane w celu optymalizacji przestrzeni. Zachowywana jest tylko pamięć podręczna nowej wersji.

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});

Wszystkie żądania HTTP są przechwytywane — jeśli istnieje wersja w pamięci podręcznej, jest zwracana; w przeciwnym razie zostaje pobrana z sieci. W trybie offline zwracana jest strona alternatywna (np. offline.html).

Potwierdzam operację

Sprawdźmy, jak faktycznie działa 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});
  • Tutaj sprawdzamy rejestrację Service Worker oraz sposób pobierania zasobów poprzez kliknięcie przycisku testowego.

Przykłady strategii buforowania

Oto najczęściej stosowane strategie buforowania:.

Najpierw pamięć podręczna

Oto przykład implementacji strategii Najpierw pamięć podręczna:.

1self.addEventListener('fetch', event => {
2    event.respondWith(
3        caches.match(event.request).then(response => {
4            return response || fetch(event.request);
5        })
6    );
7});
  • Ten kod implementuje strategię cache-first, gdzie żądany zasób jest zwracany z pamięci podręcznej, jeśli jest dostępny; w przeciwnym razie jest pobierany z sieci.

Najpierw sieć

Oto przykład implementacji strategii Najpierw sieć:.

 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});
  • Ten kod implementuje strategię network-first, gdzie żądany zasób jest najpierw pobierany z sieci, a jeśli to się nie powiedzie, jest pobierany z pamięci podręcznej.

Buforuj tylko style i JavaScript, uzyskuj dostęp do API w czasie rzeczywistym

Oto przykład implementacji, w której style i JavaScript są buforowane, a API są dostępne w czasie rzeczywistym:.

 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});
  • Ten kod zawsze uzyskuje dostęp do zapytań API w czasie rzeczywistym i stosuje strategię cache-first do plików statycznych, takich jak arkusze stylów i JavaScript.

Przebieg aktualizacji

Przebieg aktualizacji Service Workera wygląda następująco:.

  1. Wykryto nowy plik sw.js.
  2. Wywoływane jest zdarzenie install.
  3. Oczekuje, aż poprzedni Service Worker stanie się bezczynny.
  4. Wywoływane jest zdarzenie activate.
  5. Następuje przełączenie na nowego Service Worker'a.
  6. Zdarzenie controllerchange zostaje wywołane.

Wykrywanie aktualizacji

Po zainstalowaniu Service Worker, stara wersja będzie używana do następnej wizyty. Aby zastosować aktualizacje, często stosuje się kod wykrywający aktualizacje i przeładowujący stronę.

1navigator.serviceWorker.addEventListener('controllerchange', () => {
2    window.location.reload();
3});
  • Zdarzenie controllerchange jest wywoływane, gdy kontroler Service Workera, czyli ten, który kontroluje aktualnie otwartą stronę, ulega zmianie.
  • Już otwarte strony nadal korzystają z obecnego Service Workera, a nowo zainstalowany Service Worker nie działa na tych stronach natychmiastowo. Dlatego stosuje się technikę, w której zdarzenie controllerchange służy do wykrycia, że nowy kontroler stał się aktywny, a następnie strona jest przeładowywana, aby natychmiast zastosować aktualizację.

Ostrzeżenia i dobre praktyki

Podczas korzystania z Service Worker należy pamiętać o następujących kwestiach:.

  • Wymagany HTTPS Ze względów bezpieczeństwa nie działa przez http://, z wyjątkiem localhost.

  • Skrócone nazwy plików (hashowane) Nazwa pamięci podręcznej może zawierać nazwę pliku, adres URL oraz informacje o wersji.

  • Komunikacja z klientami Użyj postMessage do komunikacji między Service Worker a JavaScriptem strony.

Podsumowanie

Service Worker to niezbędna technologia zapewniająca obsługę trybu offline i poprawę wydajności aplikacji internetowych. Rozumiejąc podstawowy przepływ instalacji, aktywacji i obsługi żądań, oraz wdrażając odpowiednie strategie buforowania, możesz tworzyć aplikacje internetowe wyższej jakości.

Możesz śledzić ten artykuł, korzystając z Visual Studio Code na naszym kanale YouTube. Proszę również sprawdzić nasz kanał YouTube.

YouTube Video