TypeScript中的Service Worker

TypeScript中的Service Worker

本文解释了在TypeScript中如何使用Service Worker。

我们将解释TypeScript中的Service Worker,并提供实际示例。

YouTube Video

TypeScript中的Service Worker

Service Worker是介于浏览器和网络之间的“请求代理”。它支持请求拦截、缓存控制、离线支持以及后台处理(同步和推送)。使用TypeScript可以提供类型安全,并提高可维护性。

TypeScript设置

tsconfig.json(启用WebWorker类型)

让我们看看如何在tsconfig.json中启用WebWorker类型的示例。

 1{
 2  "compilerOptions": {
 3    "target": "ES2020",
 4    "module": "ES2020",
 5    "lib": ["ES2020", "WebWorker"],
 6    "moduleResolution": "Bundler",
 7    "strict": true,
 8    "noEmitOnError": true,
 9    "outDir": "out",
10    "skipLibCheck": true
11  },
12  "include": ["sw.ts"]
13}
  • 通过在lib数组中添加WebWorker,可以使用ServiceWorkerGlobalScope等类型。
  • DOMWebWorker 有不同的类型,因此通常会将浏览器(主应用)和 Service Workertsconfig.json 设置分开。
  • Service Worker 文件最终会输出到与作用域匹配的路径(通常是站点根目录 /sw.js)。
  • 出于安全原因,Service Worker只能在HTTPS(或localhost)上运行。

浏览器端注册代码

register-sw.ts

1// register-sw.ts
2async function registerServiceWorker() {
3  if (!('serviceWorker' in navigator)) return;
4
5  try {
6    const registration = await navigator.serviceWorker.register(
7      '/sw.js', { scope: '/' }
8    );
  • 此过程会注册一个 Service Workerscope 指的是 Service Worker 能够控制的路径范围。例如,如果你将 /sw.js 放在根目录下,并将 scope 设置为根目录(/),则可以控制整个站点的所有资源。另一方面,如果你指定了某个特定目录,如 /app/,则只有该目录下的内容会被控制。
1    // If there's a waiting worker, notify the user.
2    if (registration.waiting) {
3      promptUserToUpdate(registration);
4    }
  • waiting 表示新 Service Worker 已经被安装并正在等待激活的状态。在这个阶段,现有页面仍然由旧的 Service Worker 控制,因此通常会提示用户确认,在获得用户同意后,调用 skipWaiting() 以立即激活新的 Service Worker。这样可以无需等待下一次页面刷新即可反映最新的处理流程。
 1    // When a new SW is installing, monitor its state changes
 2    registration.addEventListener('updatefound', () => {
 3      const newWorker = registration.installing;
 4      if (!newWorker) return;
 5      newWorker.addEventListener('statechange', () => {
 6        if (newWorker.state === 'installed' &&
 7            navigator.serviceWorker.controller) {
 8          // New content available, prompt the user
 9          promptUserToUpdate(registration);
10        }
11      });
12    });
  • updatefound 会在开始安装新的 Service Worker 时被触发。当该事件发生时,新的 worker 会被设置到 registration.installing,因此通过监听它的 statechange 可以检测到安装何时完成(installed)。此外,如果存在 navigator.serviceWorker.controller,则意味着旧的 Service Worker 已经在控制页面,这时可以通知用户有新版本可用。
1    // When the active worker changes (e.g., after skipWaiting), reload if desired
2    navigator.serviceWorker.addEventListener('controllerchange', () => {
3      // Optionally reload to let the new SW take over
4      window.location.reload();
5    });
6  } catch (err) {
7    console.error('Service Worker registration failed: ', err);
8  }
9}
  • controllerchange 事件会在新 Service Worker 开始控制当前页面的瞬间触发。此时刷新页面会立即应用新的缓存策略和处理程序。然而,自动刷新页面可能会影响用户体验,因此最佳做法是在获得用户同意后再刷新页面
1function promptUserToUpdate(reg: ServiceWorkerRegistration) {
2  // Show UI to user. If user accepts:
3  if (reg.waiting) {
4    reg.waiting.postMessage({ type: 'SKIP_WAITING' });
5  }
6}
7
8registerServiceWorker();
  • 通过让 Service Worker 接收到来自客户端的 postMessage({ type: 'SKIP_WAITING' }) 并调用 self.skipWaiting(),可以推动更新的进行。

sw.ts中的作用域声明

接下来,来看一个实现应用壳(App Shell)缓存的典型Service Worker示例。

在TypeScript中使用Service Worker时,为self指定正确的类型很有用。

1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
  • 在 TypeScript 中,self 默认被视为 any,因此如果不添加类型声明,将无法获得像 skipWaiting()clients 这类 Service Worker 专用 API 的类型补全和类型检查。
  • 指定为ServiceWorkerGlobalScope可以启用自动补全,避免误用,并实现与常规DOM脚本分离的更安全开发。

基础Service Worker(安装/激活/请求拦截)

演示了简单的缓存版本管理、安装时预缓存、激活时删除旧缓存,以及请求拦截时的缓存策略(静态资源用缓存优先,API请求用网络优先)。

sw.ts(最小配置+缓存框架)

 1const CACHE_NAME = 'app-shell-v1';
 2const STATIC_ASSETS = [
 3  '/',
 4  '/index.html',
 5  '/styles.css',
 6  '/main.js',
 7  '/fallback.png'
 8];
 9
10self.addEventListener('install', (event: ExtendableEvent) => {
11  // Pre-cache application shell
12  event.waitUntil(
13    caches.open(CACHE_NAME)
14      .then(cache => cache.addAll(STATIC_ASSETS))
15      // Activate immediately (optional: coordinate with client)
16      .then(() => self.skipWaiting())
17  );
18});
  • install 事件期间,应用的**静态资源(App Shell)**会被预缓存。通过调用 self.skipWaiting(),新的 Service Worker 会立即激活,无需等待下次访问即可使用最新缓存。
 1self.addEventListener('activate', (event: ExtendableEvent) => {
 2  // Clean up old caches and take control of clients immediately
 3  event.waitUntil(
 4    caches.keys().then(keys =>
 5      Promise.all(keys
 6        .filter(key => key !== CACHE_NAME)
 7        .map(key => caches.delete(key)))
 8    ).then(() => self.clients.claim())
 9  );
10});
  • activate 事件中,旧版本缓存会被删除,确保 Service Worker 保持最新状态。另外,通过调用 self.clients.claim(),新的 Service Worker 可以在不重新加载页面的情况下控制所有客户端。
 1self.addEventListener('fetch', (event: FetchEvent) => {
 2  const request = event.request;
 3  const url = new URL(request.url);
 4
 5  // Navigation requests (SPA) -> network-first with fallback to cached index.html
 6  if (request.mode === 'navigate') {
 7    event.respondWith(
 8      fetch(request).catch(() => caches.match('/index.html') as Promise<Response>)
 9    );
10    return;
11  }
12
13  // Simple API routing: network-first for /api/
14  if (url.pathname.startsWith('/api/')) {
15    event.respondWith(networkFirst(request));
16    return;
17  }
18
19  // Static assets: cache-first
20  event.respondWith(cacheFirst(request));
21});
  • fetch事件中可以拦截请求并控制响应内容。可以实现如缓存优先或网络优先等策略,这对于离线支持与性能优化都很有帮助。
1self.addEventListener('message', (event: ExtendableMessageEvent) => {
2  const data = (event as any).data;
3  if (!data) return;
4
5  if (data.type === 'SKIP_WAITING') {
6    // Force the waiting service worker to become active
7    self.skipWaiting();
8  }
9});
  • 如果收到 SKIP_WAITING,调用 self.skipWaiting() 可以立即激活等待中的 Service Worker。这样一来,无需刷新页面,新版本会从下一个请求起被应用。

实用缓存策略概述

cache-first

缓存优先策略会先检查缓存,如果有则立即返回。如果没有,则从网络获取并缓存结果。适用于静态文件。

 1async function cacheFirst(request: Request): Promise<Response> {
 2  const cache = await caches.open(CACHE_NAME);
 3  const cached = await cache.match(request);
 4  if (cached) {
 5    return cached;
 6  }
 7  const response = await fetch(request);
 8  if (response && response.ok) {
 9    cache.put(request, response.clone());
10  }
11  return response;
12}
  • 此代码演示了缓存优先的实现方法。如果有缓存则直接返回,否则从网络获取并保存到缓存。适用于变化不频繁的静态资源,如图片或CSS文件。

network-first

网络优先会先尝试网络请求,失败时再回退到缓存。适用于对实时性要求高的API数据。

 1async function networkFirst(request: Request): Promise<Response> {
 2  const cache = await caches.open(CACHE_NAME);
 3  try {
 4    const response = await fetch(request);
 5    if (response && response.ok) {
 6      cache.put(request, response.clone());
 7    }
 8    return response;
 9  } catch (err) {
10    const cached = await cache.match(request);
11    if (cached) return cached;
12    return new Response(JSON.stringify({ error: 'offline' }), {
13      status: 503,
14      headers: { 'Content-Type': 'application/json' }
15    });
16  }
17}
  • 此代码演示了网络优先的实现方法。如果网络响应成功,会将其保存到缓存;如果失败,则返回缓存版本。适用于需要最新数据的资源,如新闻或API响应。

stale-while-revalidate

stale-while-revalidate 策略会先返回缓存,同时在后台从网络更新缓存内容。该策略兼顾响应速度与内容新鲜度。

 1async function staleWhileRevalidate(request: Request, cacheName = CACHE_NAME): Promise<Response> {
 2  const cache = await caches.open(cacheName);
 3  const cachedResponse = await cache.match(request);
 4  const networkFetch = fetch(request).then(networkResponse => {
 5    if (networkResponse && networkResponse.ok) {
 6      cache.put(request, networkResponse.clone());
 7    }
 8    return networkResponse;
 9  }).catch(() => undefined);
10
11  // Return cached immediately if exists, otherwise wait network
12  return cachedResponse || (await networkFetch) || new Response('offline', { status: 503 });
13}
  • 此代码会有缓存时立即返回,同时在后台从网络获取数据以更新缓存。它为用户提供快速响应,下次访问时则用更新后的内容,适合UI界面或轻量级数据的交付。

优化更新流程(更新通知和安全刷新)

Service Worker 的更新并不会立刻生效;新版本会一直处于等待状态,直到所有现有标签页被关闭。

在这里,我们实现了当新版本准备就绪时通知客户端,并根据用户操作安全刷新页面的机制。

当新版本准备就绪时,通过 Service Worker 一侧通知客户端。

 1// In sw.ts: after 'activate' or when new version is ready, broadcast a message
 2async function notifyClientsUpdated() {
 3  const all = await self.clients.matchAll({ type: 'window' });
 4  for (const client of all) {
 5    client.postMessage({ type: 'SW_UPDATED' });
 6  }
 7}
 8
 9// e.g., call this at the end of 'activate'
10self.addEventListener('activate', (event) => {
11  event.waitUntil((async () => {
12    if ('navigationPreload' in self.registration) {
13      await self.registration.navigationPreload.enable();
14    }
15    // cache cleanup
16    const cacheNames = await caches.keys();
17    await Promise.all(
18      cacheNames.map((name) => {
19        if (name !== CACHE_NAME) {
20          return caches.delete(name);
21        }
22      })
23    );
24
25    await self.clients.claim();
26    await notifyClientsUpdated();
27  })());
28});
  • 在此代码中,notifyClientsUpdated会在activate事件结束时被调用,通知所有已连接的客户端新版本已就绪。clients.claim() 是一种能立即让当前打开的页面(客户端)受到新激活 Service Worker 控制 的方法。通常,Service Worker 只会在页面下次加载时开始控制页面,但使用 clients.claim() 可以立即让页面受到控制,无需刷新。

在客户端显示更新提示UI,并根据用户操作进行刷新

1// in app startup
2navigator.serviceWorker.addEventListener('message', (e) => {
3  if (e.data?.type === 'SW_UPDATED') {
4    // Show a non-intrusive toast or banner: "New version available"
5    // When user clicks "Reload", call:
6    window.location.reload();
7  }
8});
  • 客户端通过message事件接收SW_UPDATED,并在界面上显示更新通知。当用户选择重新加载时,将执行 window.location.reload(),把页面上的旧 HTML、CSS 和其它资源更新到最新版本。这样可以确保通过 clients.claim() 切换后的 Service Worker 的缓存和控制在整个页面生效。

离线回退

为关键导航准备/offline.html,即使没有图片或字体也能提供基本信息的极简界面。如API调用失败,尽可能显示上次缓存状态,并尝试在后台重新获取,以提升用户体验。

实现示例

 1// sw.ts
 2const CACHE_NAME = 'app-cache-v1';
 3
 4// Cache offline.html during install
 5self.addEventListener('install', (event) => {
 6  event.waitUntil((async () => {
 7    const cache = await caches.open(CACHE_NAME);
 8    await cache.addAll(['/offline.html']);
 9  })());
10});
11
12// Handle fetch requests
13self.addEventListener('fetch', (event) => {
14  const request = event.request;
15
16  // Navigation requests (e.g., page transitions)
17  if (request.mode === 'navigate') {
18    event.respondWith((async () => {
19      try {
20        // Try to fetch from the network as usual
21        return await fetch(request);
22      } catch (err) {
23        // On failure, return offline fallback page
24        const cache = await caches.open(CACHE_NAME);
25        return await cache.match('/offline.html') as Response;
26      }
27    })());
28  }
29});
  • install事件中预缓存/offline.html,以便网络不可用时至少能返回一个最简页面。
  • fetch 事件中,可以通过 request.mode === 'navigate' 监控导航请求,从而专门针对页面切换。
  • 网络请求失败时回退到/offline.html,保证离线时也能显示。

客户端与 Service Worker 之间的消息通信

因为 Service Worker 独立于页面生命周期运行,双向消息通信对于状态通知和命令执行非常重要。为消息指定类型可以防止发送错误消息,支持代码补全,使你的实现更加健壮。

代码示例

  1. 消息类型定义
1type SwToClient =
2  | { type: 'SW_READY' }
3  | { type: 'SW_UPDATED' }
4  | { type: 'CACHE_CLEARED' }
5  | { type: 'PING'; ts: number };
6
7type ClientToSw =
8  | { type: 'CLEAR_CACHE' }
9  | { type: 'PING'; ts: number };
  • SwToClient是从Service Worker发送到客户端的消息类型。
  • ClientToSw是从客户端发送到Service Worker的消息类型。
  • 这样可以明确双向通信中可以交换的事件类型。
  1. Service Worker端的处理
 1self.addEventListener('message', (event) => {
 2  const data = event.data as ClientToSw;
 3  if (data?.type === 'CLEAR_CACHE') {
 4    event.waitUntil((async () => {
 5      const keys = await caches.keys();
 6      await Promise.all(keys.map((k) => caches.delete(k)));
 7      await broadcast({ type: 'CACHE_CLEARED' });
 8    })());
 9  } else if (data?.type === 'PING') {
10    event.source?.postMessage({ type: 'PING', ts: data.ts } as SwToClient);
11  }
12});
  • Service Worker收到客户端消息后根据类型进行分支处理。
  • 收到CLEAR_CACHE时,会清除缓存并用CACHE_CLEARED通知所有客户端。
  • 收到PING时,向原始客户端回复带有时间戳的PING消息。
  1. 从Service Worker通知所有客户端
1async function broadcast(msg: SwToClient) {
2  const clients = await self.clients.matchAll({ includeUncontrolled: true });
3  for (const c of clients) c.postMessage(msg);
4}
  • 使用clients.matchAll获取所有窗口标签页。
  • 给每个客户端发送postMessage即可进行广播。
  • 可用于更新通知(如SW_UPDATED)和错误通知。
  1. 客户端的处理
1navigator.serviceWorker.controller?.postMessage({
2  type: 'PING',
3  ts: Date.now()
4} as ClientToSw);
  • 通过客户端发送 PING 并收到 Service Worker 的响应,可以验证双向通信是否正常。这样可以更轻松地测试连接状态和消息处理。
 1navigator.serviceWorker.addEventListener('message', (e) => {
 2  const msg = e.data as SwToClient;
 3  switch (msg.type) {
 4    case 'SW_READY':
 5      console.log('Service Worker is ready');
 6      // Example: hide loading spinner or enable offline UI
 7      break;
 8    case 'SW_UPDATED':
 9      console.log('A new version of the Service Worker is available');
10      // Example: show update notification or reload prompt
11      const shouldReload = confirm('A new version is available. Reload now?');
12      if (shouldReload) {
13        window.location.reload();
14      }
15      break;
16    case 'CACHE_CLEARED':
17      console.log('Cache cleared');
18      // Example: show confirmation message to user
19      alert('Cache has been successfully cleared.');
20      break;
21    case 'PING':
22      console.log(`Received PING response, ts=${msg.ts}`);
23      break;
24  }
25});
  • {^ i18n_speak クライアント側では Service Worker から送信されるメッセージを受信し、種類に応じて処理を分岐します。SW_READY は初期化完了、SW_UPDATED は新バージョン検出、CACHE_CLEARED はキャッシュ削除完了、PING は通信確認を示します。各メッセージに応じて、UI の更新やリロード、通知表示などを行います。^}

类型化消息传递的优点

  • 类型化消息传递让可发送和接收的消息一目了然,自动补全和类型检查提升了安全性。
  • postMessage实现一对一通信,广播(broadcast)实现一对多通信
  • 可以轻松实现如更新通知(SW_UPDATED)、缓存管理(CACHE_CLEARED)和健康检查(PING)等关键功能。

总结

  • 使用TypeScript为Service Worker的API调用和消息传递提供了类型安全,大幅提升开发效率与可维护性。
  • 理解installactivatefetch生命周期事件,根据不同场景选择合适的缓存策略(如缓存优先网络优先),能带来更佳的用户体验。
  • 在运维层面,了解缓存版本管理与更新流程(updatefoundwaitingSKIP_WAITING等)非常重要。
  • 通过为客户端与 Service Worker 之间的通信采用类型化消息,可以防止错误实现,并建立一个长期易于扩展和维护的系统。

您可以在我们的YouTube频道上使用Visual Studio Code跟随上述文章进行学习。 请也查看我们的YouTube频道。

YouTube Video