`Service Worker` 在 JavaScript 中

`Service Worker` 在 JavaScript 中

本文將說明 Service Worker 在 JavaScript 中的概念。

我們將由淺入深,逐步解釋 Service Worker 的基礎到實際的快取控制。

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 功能,可實現請求快取與離線支援。它是 PWA(漸進式網頁應用)的核心技術之一,能讓網頁應用有如原生 App 般的體驗。

什麼是 Service Worker

Service Worker 是運行於瀏覽器背景執行緒中的 JavaScript 檔案。它在與網頁分離的執行緒上運行,無法接觸 UI,但能攔截網路請求、管理快取並處理推播通知。

Service Worker 的關鍵特性包括:。

  • 除了 localhost 外,僅能在 HTTPS 下運作。
  • 採用以 Promise 為基礎的非同步 API。
  • 其為事件驅動,常用事件有 installactivatefetchpush 等。

註冊 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.jsService Worker 檔案)。
  • 在註冊過程中,可以用 then 處理成功,用 catch 處理錯誤。
  • registration.scope 代表受 Service Worker 影響的路徑範圍(範圍)。
  • 預設情況下,作用範圍是註冊檔案(本例中為 /sw.js)所在的目錄及其子目錄。

Service Worker 作用範圍

如果想限制作用範圍,可以在 register 的第二個參數中指定 scope

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 的註冊和資源獲取行為。

快取策略範例

以下是常見的快取策略:。

優先使用快取(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)

以下是網路優先策略的實作範例:。

 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});
  • 此代碼實現了網路優先的策略,首先嘗試從網路獲取所需資源,若失敗則從快取中取得。

僅快取樣式和 JavaScript,API 則即時存取

以下為僅快取樣式和 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 閒置。
  4. 觸發 activate 事件。
  5. 切換至新的 Service Worker。
  6. 會觸發 controllerchange 事件。

更新偵測

Service Worker 安裝後,舊版本會在下一次造訪以前繼續被使用。為了套用更新,通常會使用檢測更新並重載網頁的程式碼。

1navigator.serviceWorker.addEventListener('controllerchange', () => {
2    window.location.reload();
3});
  • 當 Service Worker 的控制器(即控制當前頁面的 Service Worker)發生變更時,會觸發 controllerchange 事件。
  • 已經開啟的頁面會繼續使用當前的 Service Worker,新安裝的 Service Worker 不會立即作用於這些頁面。因此,通常會利用 controllerchange 事件來偵測新的控制器已經啟用,並重新載入頁面以立刻套用更新。

注意事項與最佳實踐

使用 Service Worker 時,請注意以下重點:。

  • 必須使用 HTTPS 由於安全性限制,除 localhost 外,無法在 http:// 上運作。

  • 檔案名需帶 hash 快取名稱可包含檔案名、URL 與版本資訊。

  • 與用戶端通訊 可使用 postMessageService Worker 與頁面上的 JavaScript 溝通。

總結

Service Worker 是網頁應用提供離線支援與效能提升的重要技術。透過理解安裝、啟用、攔截請求等基本流程,並實作適合的快取策略,可以打造更高品質的網頁應用程式。

您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。

YouTube Video