Módulos ES em service workers

Uma alternativa moderna para importScripts().

Contexto

Os módulos ES são os favoritos dos desenvolvedores há algum tempo. Além de vários outros benefícios, eles oferecem a promessa de um formato de módulo universal em que o código compartilhado pode ser lançado uma vez e executado em navegadores e em ambientes de execução alternativos, como Node.js. Embora todos os navegadores modernos ofereçam algum suporte ao módulo ES, nem todos oferecem suporte em todos os lugares em que o código pode ser executado. Especificamente, o suporte para importar módulos ES dentro de um service worker do navegador está começando a ficar mais disponível.

Este artigo detalha o estado atual do suporte a módulos ES em service workers em navegadores comuns, além de algumas armadilhas a serem evitadas e práticas recomendadas para enviar código de service worker compatível com versões anteriores.

Casos de uso

O caso de uso ideal para módulos ES em service workers é o carregamento de uma biblioteca moderna ou um código de configuração compartilhado com outros ambientes de execução que oferecem suporte a módulos ES.

Tentar compartilhar o código dessa maneira antes dos módulos ES envolvia o uso de formatos de módulo "universais" mais antigos, como o UMD, que incluía boilerplate desnecessário, e escrever código que fazia mudanças em variáveis expostas globalmente.

Os scripts importados por módulos ES podem acionar o fluxo de atualização do service worker se o conteúdo deles mudar, correspondendo ao comportamento do importScripts().

Limitações atuais

Somente importações estáticas

Os módulos ES podem ser importados de duas maneiras: estaticamente, usando a sintaxe import ... from '...', ou dinamicamente, usando o método import(). No momento, apenas a sintaxe estática é aceita em um service worker.

Essa limitação é análoga a uma restrição semelhante colocada no uso de importScripts(). As chamadas dinâmicas para importScripts() não funcionam dentro de um worker de serviço, e todas as chamadas importScripts(), que são simultâneas por natureza, precisam ser concluídas antes que o worker de serviço conclua a fase install. Essa restrição garante que o navegador saiba e possa armazenar em cache implicitamente todo o código JavaScript necessário para a implementação de um worker de serviço durante a instalação.

Eventualmente, essa restrição poderá ser suspensa, e as importações de módulo ES dinâmico poderão ser permitidas. Por enquanto, use apenas a sintaxe estática dentro de um service worker.

E quanto a outros trabalhadores?

O suporte a módulos ES em workers "dedicados", ou seja, construídos com new Worker('...', {type: 'module'}), é mais difundido e tem suporte no Chrome e no Edge desde a versão 80, bem como nas versões recentes do Safari. As importações estáticas e dinâmicas de módulos ES são compatíveis com workers dedicados.

O Chrome e o Edge oferecem suporte a módulos ES em shared workers desde a versão 83, mas nenhum outro navegador oferece suporte no momento.

Sem suporte para mapas de importação

Os mapas de importação permitem que ambientes de execução reescrevam especificadores de módulo para, por exemplo, adicionar o URL de uma CDN preferencial na qual os módulos do ES podem ser carregados.

Embora o Chrome e o Edge versão 89 e mais recentes ofereçam suporte a mapas de importação, eles não podem ser usados com workers de serviço.

Suporte ao navegador

Os módulos ES em service workers são compatíveis com o Chrome e o Edge a partir da versão 91.

O Safari adicionou suporte na versão 122 da prévia de tecnologia, e os desenvolvedores podem esperar que essa funcionalidade seja lançada na versão estável do Safari no futuro.

Exemplo de código

Este é um exemplo básico de uso de um módulo ES compartilhado no contexto window de um app da Web, além de registrar um worker de serviço que usa o mesmo módulo ES:

// Inside config.js:
export const cacheName = 'my-cache';
// Inside your web app:
<script type="module">
  import {cacheName} from './config.js';
  // Do something with cacheName.

  await navigator.serviceWorker.register('es-module-sw.js', {
    type: 'module',
  });
</script>
// Inside es-module-sw.js:
import {cacheName} from './config.js';

self.addEventListener('install', (event) => {
  event.waitUntil((async () => {
    const cache = await caches.open(cacheName);
    // ...
  })());
});

Compatibilidade com versões anteriores

O exemplo acima funcionaria bem se todos os navegadores oferecessem suporte a módulos ES em service workers, mas, no momento em que este artigo foi escrito, não era o caso.

Para acomodar navegadores que não têm suporte integrado, é possível executar o script do worker de serviço usando um bundler compatível com o módulo ES para criar um worker de serviço que inclua todo o código do módulo inline e funcione em navegadores mais antigos. Como alternativa, se os módulos que você está tentando importar já estiverem disponíveis em pacotes nos formatos IIFE ou UMD, eles poderão ser importados usando importScripts().

Depois que você tiver duas versões do service worker disponíveis, uma que usa módulos ES e a outra que não usa, será necessário detectar o que o navegador atual oferece suporte e registrar o script do service worker correspondente. As práticas melhores para detectar suporte estão em constante mudança, mas você pode acompanhar a discussão neste problema do GitHub para receber recomendações.

_Foto de Vlado Paunovic no Unsplash_