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等类型。 DOM和WebWorker有不同的类型,因此通常会将浏览器(主应用)和Service Worker的tsconfig.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 Worker。scope指的是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 独立于页面生命周期运行,双向消息通信对于状态通知和命令执行非常重要。为消息指定类型可以防止发送错误消息,支持代码补全,使你的实现更加健壮。
代码示例
- 消息类型定义
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的消息类型。- 这样可以明确双向通信中可以交换的事件类型。
- 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消息。
- 从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)和错误通知。
- 客户端的处理
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调用和消息传递提供了类型安全,大幅提升开发效率与可维护性。
- 理解
install、activate和fetch生命周期事件,根据不同场景选择合适的缓存策略(如缓存优先或网络优先),能带来更佳的用户体验。 - 在运维层面,了解缓存版本管理与更新流程(
updatefound、waiting、SKIP_WAITING等)非常重要。 - 通过为客户端与
Service Worker之间的通信采用类型化消息,可以防止错误实现,并建立一个长期易于扩展和维护的系统。
您可以在我们的YouTube频道上使用Visual Studio Code跟随上述文章进行学习。 请也查看我们的YouTube频道。