Service Worker em TypeScript

Service Worker em TypeScript

Este artigo explica os Service Workers em TypeScript.

Vamos explicar os Service Workers em TypeScript, incluindo exemplos práticos.

YouTube Video

Service Worker em TypeScript

Um Service Worker é um "proxy de requisições" que fica entre o navegador e a rede. Ele permite a interceptação de requisições, controle de cache, suporte offline e processamento em segundo plano (sincronização e push). Usar TypeScript proporciona segurança de tipos e aumenta a manutenibilidade.

Configurando o TypeScript

tsconfig.json (Habilite os tipos de WebWorker)

Vamos ver um exemplo de como habilitar o tipo WebWorker em 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}
  • Ao adicionar WebWorker ao array lib, você pode usar tipos como ServiceWorkerGlobalScope.
  • DOM e WebWorker possuem tipos diferentes, então é uma prática comum separar as configurações do tsconfig.json para o navegador (aplicação principal) e para o Service Worker.
  • Os arquivos do Service Worker são finalmente gerados em um caminho que corresponde ao escopo (geralmente na raiz do site /sw.js).
  • Por motivos de segurança, Service Workers só funcionam em HTTPS (ou em localhost).

Código de Registro no Lado do Navegador

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    );
  • Este processo registra um Service Worker. scope refere-se ao intervalo de caminhos que o Service Worker pode controlar. Por exemplo, se você colocar /sw.js diretamente na raiz e definir o scope para o diretório raiz (/), será possível controlar todos os recursos de todo o site. Por outro lado, se você especificar um diretório específico como /app/, apenas os conteúdos sob esse diretório serão controlados.
1    // If there's a waiting worker, notify the user.
2    if (registration.waiting) {
3      promptUserToUpdate(registration);
4    }
  • waiting indica o estado em que um novo Service Worker foi instalado e está aguardando para ser ativado. Neste estágio, as páginas existentes ainda são controladas pelo antigo Service Worker, por isso é comum pedir confirmação ao usuário e, após a aprovação, chamar skipWaiting() para ativar imediatamente o novo Service Worker. Isso permite aplicar os processos mais recentes sem esperar pelo próximo recarregamento da página.
 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 é disparado quando a instalação de um novo Service Worker é iniciada. Quando este evento ocorre, um novo worker é definido em registration.installing, então, monitorando a sua statechange, é possível detectar quando a instalação foi concluída (installed). Além disso, se navigator.serviceWorker.controller existir, isso significa que um Service Worker antigo já está controlando a página, portanto, essa é uma oportunidade para notificar o usuário sobre a existência de uma nova versão.
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}
  • O evento controllerchange é disparado no momento em que o novo Service Worker começa a controlar a página atual. Recarregar neste ponto aplicará imediatamente novas estratégias de cache e processamento. No entanto, o recarregamento automático pode prejudicar a experiência do usuário, por isso é preferível recarregar após obter o consentimento do usuário.
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();
  • Ao fazer com que o Service Worker receba um postMessage({ type: 'SKIP_WAITING' }) do cliente e chame self.skipWaiting(), é possível forçar uma atualização.

Declaração de Escopo em sw.ts

A seguir, vejamos um exemplo típico de Service Worker que implementa cache de app shell.

Ao usar Service Workers em TypeScript, é útil atribuir o tipo correto a self.

1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
  • No TypeScript, self é tratado como any por padrão, então sem tipagem adicional, você não terá autocompletar nem verificação de tipo para APIs específicas de Service Worker como skipWaiting() ou clients.
  • Especificar ServiceWorkerGlobalScope permite autocompletar, previne usos incorretos e permite um desenvolvimento mais seguro separado dos scripts DOM normais.

Service Worker Básico (Instalar/Ativar/Buscar)

O exemplo mostra gerenciamento simples de versão de cache, precaching na instalação, exclusão de caches antigos na ativação e estratégias de cache no fetch (cache-first para arquivos estáticos, network-first para APIs).

sw.ts (Configuração Mínima + Esqueleto de Cache)

 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});
  • Durante o evento install, os recursos estáticos (App Shell) do aplicativo são pré-cacheados. Ao chamar self.skipWaiting(), o novo Service Worker é ativado imediatamente, tornando o cache mais recente disponível sem esperar pelo próximo acesso.
 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});
  • No evento activate, versões antigas dos caches são deletadas e o Service Worker é mantido atualizado. Além disso, ao chamar self.clients.claim(), o novo Service Worker pode controlar todos os clientes sem que seja necessário recarregar a página.
 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});
  • No evento fetch, você pode interceptar requisições e controlar a resposta. Você pode implementar estratégias como cache-first ou network-first, que são úteis para suporte offline e desempenho.
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});
  • Se SKIP_WAITING for recebido, chamar self.skipWaiting() permite ativar instantaneamente o Service Worker em espera. Como resultado, a nova versão será aplicada na próxima requisição, sem a necessidade de recarregar a página.

Visão Geral das Estratégias Práticas de Cache

cache-first

Cache-first verifica o cache primeiro e retorna a resposta imediatamente se estiver disponível. Se não houver cache, busca na rede e armazena o resultado no cache. Isto se aplica a arquivos estáticos.

 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}
  • Este código demonstra uma implementação cache-first. Se houver cache, retorna. Caso contrário, busca na rede e salva no cache. É adequado para recursos estáticos que mudam raramente, como imagens ou CSS.

network-first

Network-first tenta a rede primeiro e recorre ao cache em caso de falha. É adequado para APIs onde a atualidade dos dados é importante.

 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}
  • Este código demonstra uma implementação network-first. Se receber uma resposta da rede, salva no cache; se falhar, retorna a versão em cache. É adequado para recursos que exigem dados atualizados, como notícias ou respostas de APIs.

stale-while-revalidate

stale-while-revalidate retorna o cache primeiro e simultaneamente o atualiza da rede em segundo plano. Isto equilibra velocidade de resposta e atualidade dos dados.

 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}
  • Este código retorna o cache imediatamente se estiver disponível, enquanto busca novos dados da rede em segundo plano para atualizar o cache. Oferece respostas rápidas ao usuário e utiliza conteúdo atualizado para o próximo acesso, tornando-o adequado para UIs ou entrega de dados leves.

Otimização do Fluxo de Atualização (Notificação de Atualização e Reload Seguro)

As atualizações do Service Worker não são imediatas; a nova versão permanecerá em espera até que as abas existentes sejam fechadas.

Aqui, implementamos um sistema para notificar o cliente quando a nova versão está pronta e recarregar a página de forma segura, baseada na ação do usuário.

Notifique o cliente pelo lado do Service Worker quando a nova versão estiver pronta.

 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});
  • Neste código, notifyClientsUpdated é chamada no final do evento activate para notificar todos os clientes conectados de que a nova versão está pronta. clients.claim() é um método que imediatamente traz as páginas (clientes) atualmente abertas sob o controle do novo Service Worker ativado. Normalmente, um Service Worker começa a controlar a página somente no próximo carregamento, mas ao usar clients.claim(), você consegue controlar a página imediatamente sem recarregá-la.

Exiba a interface de atualização no cliente e recarregue com a ação do usuário

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});
  • O cliente recebe SW_UPDATED através do evento message e exibe uma notificação de atualização na interface. Quando o usuário escolhe recarregar, window.location.reload() é executado, atualizando o HTML, CSS e outros recursos antigos da página para a versão mais recente. Isso garante que o cache e o controle pelo Service Worker, com clients.claim(), sejam refletidos em toda a página.

Fallback Offline

Prepare o /offline.html para navegação crítica e forneça uma interface mínima que seja compreensível mesmo sem imagens ou fontes. Se uma chamada de API falhar, mostre o último estado em cache se possível e tente buscar novamente em segundo plano para melhorar a experiência do usuário.

Exemplo de Implementação

 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});
  • Faça o precache de /offline.html durante o evento install para que pelo menos uma página mínima seja exibida caso a rede esteja indisponível.
  • No evento fetch, você pode monitorar requisições de navegação com request.mode === 'navigate' e direcionar especificamente as transições de página.
  • Aponte para /offline.html quando a rede falhar, garantindo que será exibido mesmo offline.

Mensagens entre o cliente e o Service Worker

Como o Service Worker opera independentemente do ciclo de vida da página, a mensageria bidirecional é importante para notificar estados e executar comandos. Especificar tipos para as mensagens ajuda a prevenir o envio incorreto de mensagens, permite o autocompletar do código e torna sua implementação mais robusta.

Exemplo de Código

  1. Definição dos Tipos de Mensagem
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 é o tipo de mensagem enviada do Service Worker para o cliente.
  • ClientToSw é o tipo de mensagem enviada do cliente para o Service Worker.
  • Isso permite esclarecer os tipos de eventos que podem ser trocados por meio de comunicação bidirecional.
  1. Processamento do Lado do 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});
  • O Service Worker recebe mensagens do cliente e ramifica o processamento de acordo com o tipo.
  • Para CLEAR_CACHE, ele remove o cache e depois notifica todos os clientes com CACHE_CLEARED.
  • Para PING, ele responde ao cliente original com uma mensagem PING incluindo o timestamp.
  1. Notificar Todos os Clientes a partir do 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}
  • Use clients.matchAll para obter todas as abas do navegador.
  • Enviando postMessage para cada uma, você pode fazer broadcast de mensagens.
  • Isso pode ser usado para notificações de atualização (como SW_UPDATED) e notificações de erro.
  1. Processamento do Lado do Cliente
1navigator.serviceWorker.controller?.postMessage({
2  type: 'PING',
3  ts: Date.now()
4} as ClientToSw);
  • Ao enviar um PING do cliente e receber uma resposta do Service Worker, você pode verificar se a comunicação bidirecional está funcionando corretamente. Isso facilita o teste dos estados de conexão e do tratamento de mensagens.
 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 の更新やリロード、通知表示などを行います。^}

Benefícios de Mensagens Tipadas

  • Usar mensagens tipadas deixa claro quais mensagens podem ser enviadas e recebidas, e o autocompletar e a checagem de tipos aumentam a segurança.
  • postMessage permite comunicação um-para-um e broadcast permite comunicação um-para-muitos.
  • Você pode facilmente implementar recursos essenciais como notificações de atualização (SW_UPDATED), gerenciamento de cache (CACHE_CLEARED) e checagens de integridade (PING).

Resumo

  • Usar TypeScript adiciona segurança de tipos às chamadas de API do Service Worker e às mensagens, melhorando muito a eficiência de desenvolvimento e manutenção.
  • Entender os eventos de ciclo de vida install, activate e fetch, e escolher a estratégia de cache correta (como cache-first ou network-first) para cada situação leva a uma melhor experiência do usuário.
  • Para operações, entender o gerenciamento de versão de cache e os fluxos de atualização (updatefound, waiting, SKIP_WAITING, etc.) é essencial.
  • Ao adotar mensagens tipadas para a comunicação entre cliente e Service Worker, você pode evitar implementações incorretas e estabelecer um sistema fácil de estender e manter a longo prazo.

Você pode acompanhar o artigo acima usando o Visual Studio Code em nosso canal do YouTube. Por favor, confira também o canal do YouTube.

YouTube Video