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-канал.