JavaScript Service Worker

Service Worker 是網頁開發中最具革命性的技術之一。它是一段運行在瀏覽器背景、獨立於網頁執行緒的腳本。它是實現 PWA (Progressive Web Apps) 的核心技術。

與 Web Worker 不同,Service Worker 的生命週期與網頁無關,即使網頁關閉,Service Worker 依然可以被喚醒(例如處理推送通知)。

Service Worker 的核心能力

  • 離線瀏覽 (Offline Support):透過攔截網路請求,即便在無網路環境下也能回傳快取內容。
  • 精準快取控制:比瀏覽器預設快取更靈活地管理資源更新。
  • 背景任務:支援推送通知 (Push Notifications) 與背景同步 (Background Sync)。
HTTP 安全要求:Service Worker 具有攔截所有請求的能力,因此只能在 HTTPS 環境下執行。本機開發時 http://localhost 是唯一的例外。

生命周期 (Lifecycle) 與進階控制

Service Worker 的生命週期旨在確保網站內容的一致性。當你更新了 Service Worker 檔案,瀏覽器會偵測到差異並啟動新的生命週期。

1. 註冊 (Registration)

在主執行緒中註冊 Service Worker。

if ('serviceWorker' in navigator) {
  navigator.serviceWorker
    .register('/sw.js')
    .then((reg) => console.log('SW 註冊成功'))
    .catch((err) => console.log('SW 註冊失敗'));
}

2. 安裝 (Installation) 與 skipWaiting()

觸發 install 事件。通常在此快取靜態資源。新版 SW 安裝後會進入「等待中」(Waiting) 狀態,直到舊版 SW 的所有分頁都被關閉。

如果你希望新版 SW 立即生效,可以呼叫 self.skipWaiting()

// sw.js
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches
      .open('v2')
      .then((cache) => {
        return cache.addAll(['/', '/main.css']);
      })
      .then(() => self.skipWaiting()) // 跳過等待,立即準備啟用
  );
});

3. 啟用 (Activation) 與 clients.claim()

當舊版 SW 結束工作,新版便會進入 activate 階段。這裡是清理舊版本快取的好時機。呼叫 self.clients.claim() 可以讓新 SW 立即接管現有的所有分頁。

self.addEventListener('activate', (event) => {
  event.waitUntil(
    Promise.all([
      // 清理舊快取
      caches.keys().then((keys) => {
        return Promise.all(
          keys.map((key) => {
            if (key !== 'v2') return caches.delete(key);
          })
        );
      }),
      self.clients.claim(), // 立即接管受控分頁
    ])
  );
});

常見的快取策略 (Caching Strategies)

fetch 事件中,你可以實作不同的策略來應對各種場景:

離線優先 (Cache First)

適用於不常變動的靜態資源(圖片、字型)。

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});

網路優先 (Network First)

適用於需要即時性的 API 資料。如果網路斷線,才回傳快取中的舊資料。

self.addEventListener('fetch', (event) => {
  event.respondWith(fetch(event.request).catch(() => caches.match(event.request)));
});

Stale-While-Revalidate (過期前驗證)

這是最高級的策略:先立即回傳快取的內容(速度最快),同時在背景私下發送網路請求更新快取。下次開啟時就會看到最新內容。

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open('v2').then((cache) => {
      return cache.match(event.request).then((response) => {
        const fetchPromise = fetch(event.request).then((networkResponse) => {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
        return response || fetchPromise;
      });
    })
  );
});

實務:提示使用者有新版本

雖然 skipWaiting() 很有用,但有時直接重新整理頁面會造成使用者資料遺失。更好的做法是在前端彈出提示:

// main.js
navigator.serviceWorker.register('/sw.js').then((reg) => {
  reg.addEventListener('updatefound', () => {
    const newWorker = reg.installing;
    newWorker.addEventListener('statechange', () => {
      if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
        // 發現新版 SW,跳出「有新版本,請重新整理」的提示框
        if (confirm('有可用的新版本,是否立即更新?')) {
          window.location.reload();
        }
      }
    });
  });
});

如何進行偵錯 (Debugging)

在 Chrome 瀏覽器中,你可以透過以下方式測試 Service Worker:

  1. 開啟 DevTools > Application 頁籤。
  2. 點擊 Service Workers 側選單。
  3. Offline 勾選框:模擬斷網環境,測試快取是否生效。
  4. Update on reload:開發時勾選此項,每次重新整理都會強迫更新 Service Worker。
  5. Console:在 sw.js 裡的 console.log 會出現在獨立的調試視窗,需點擊「inspect」開啟。

總結

Service Worker 不只是快取代理,它是網頁通往原生應用體驗的門票。透過靈活的生命週期控制與快取策略,你可以建立極速載入且具備強大韌性的現代化網頁。