Service Worker на TypeScript
В этой статье объясняются Service Worker на TypeScript.
Мы объясним работу Service Worker на TypeScript с практическими примерами.
YouTube Video
Service Worker на TypeScript
Service Worker — это «прокси запросов», стоящий между браузером и сетью. Он позволяет перехватывать запросы, управлять кэшем, поддерживать офлайн-режим и выполнять фоновые задачи (синхронизация и push-уведомления). Использование TypeScript обеспечивает типобезопасность и облегчает поддержку.
Настройка TypeScript
tsconfig.json
(Включение типов WebWorker)
Рассмотрим пример включения типа WebWorker в tsconfig.json
.
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}
- Добавив
WebWorker
в массивlib
, вы сможете использовать типы вродеServiceWorkerGlobalScope
. DOM
иWebWorker
имеют различные типы, поэтому обычно настройкиtsconfig.json
для браузера (основного приложения) иService Worker
разделяются.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. Когда это событие происходит, новый воркер назначается в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();
- Передав
postMessage({ type: 'SKIP_WAITING' })
от клиента вService Worker
и вызвавself.skipWaiting()
, вы можете инициировать обновление.
Объявление области действия в sw.ts
Далее рассмотрим типичный пример Service Worker с кэшированием оболочки приложения.
При использовании Service Worker на TypeScript полезно явно указать правильный тип для self
.
1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
- В TypeScript по умолчанию
self
рассматривается какany
, поэтому без дополнительного указания типов вы не получите автодополнение или проверку типов для специфичных API Service Worker, таких какskipWaiting()
илиclients
. - Указание типа
ServiceWorkerGlobalScope
включает автодополнение, предотвращает ошибки и обеспечивает более безопасную разработку, отделённую от обычных DOM-скриптов.
Базовый Service Worker (установка/активация/запросы)
В нем показано управление версиями кэша, предварительное кэширование при установке, удаление старых кэшей при активации и стратегии кэширования (cache-first — для статики, network-first — для 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
можно перехватывать запросы и формировать ответ. Можно реализовать стратегии cache-first или network-first, которые полезны для поддержки офлайн-режима и улучшения производительности.
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
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}
- Этот код демонстрирует реализацию cache-first. Если есть кэш — он возвращается; если нет — выполняется сетевой запрос и результат сохраняется в кэш. Это подходит для статических ресурсов, которые редко меняются (например, изображения или CSS).
network-first
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}
- Этот код демонстрирует реализацию network-first. Если получен сетевой ответ — он сохраняется в кэш; если нет — возвращается кэшированная версия. Это подходит для ресурсов, где необходимы свежие данные, например, новости или ответы 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}
- Этот код сразу возвращает кэш, если он есть, а новые данные подгружает в фоне и обновляет кэш. Это обеспечивает быстрый отклик пользователю и использовать обновлённый контент при следующем обращении; подходит для интерфейса и легких данных.
Оптимизация процесса обновления (уведомления и безопасная перезагрузка)
Обновления 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()
, вы можете получить контроль немедленно, без перезагрузки.
Показать интерфейс обновления на клиенте и перезагрузить страницу по действию пользователя
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});
- Клиент получает
SW_UPDATED
через событиеmessage
и отображает уведомление о доступности обновления в интерфейсе. Когда пользователь решает перезагрузить, выполняетсяwindow.location.reload()
, обновляя устаревшие HTML, CSS и другие ресурсы на странице до последних версий. Это гарантирует, что кэш и управление черезService Worker
, переключённые с помощьюclients.claim()
, применяются ко всей странице.
Офлайн-резерв (Fallback)
Подготовьте /offline.html
для критических навигаций и обеспечьте минимальный интерфейс, который дает смысл даже без изображений или шрифтов. Если вызов API не удается — покажите последнее кэшированное состояние (если оно есть) и попробуйте повторить запрос в фоне для улучшения UX.
Пример реализации
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});
- Добавьте
/offline.html
в предварительный кэш при событииinstall
, чтобы вернуть хотя бы минимальную страницу при отсутствии сети. - В событии
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
обеспечивает один-к-одному обмен, а рассылка — один-ко-многим.- Можно просто реализовать важные функции: уведомления об обновлениях (
SW_UPDATED
), управление кэшем (CACHE_CLEARED
), проверку работоспособности (PING
).
Резюме
- Использование TypeScript добавляет типобезопасность в вызовы Service Worker API и обмен сообщениями, значительно повышая эффективность разработки и удобство поддержки.
- Понимание событий жизненного цикла (
install
,activate
,fetch
) и правильный выбор стратегии кэширования (например, cache-first или network-first) обеспечивают лучший пользовательский опыт. - Для работы важно понимать управление версиями кэша и процессы обновления (
updatefound
,waiting
,SKIP_WAITING
и др.). - Использование типизированных сообщений для взаимодействия между клиентом и
Service Worker
помогает предотвратить ошибки реализации и сделать систему легко расширяемой и поддерживаемой в долгосрочной перспективе.
Вы можете следовать этой статье, используя Visual Studio Code на нашем YouTube-канале. Пожалуйста, также посмотрите наш YouTube-канал.