Migrar para um service worker

Como substituir páginas de plano de fundo ou de eventos por um service worker

Um service worker substitui a página de segundo plano ou de evento da extensão para garantir que o código de segundo plano permaneça fora da linha de execução principal. Isso permite que as extensões sejam executadas apenas quando necessário, economizando recursos.

As páginas de segundo plano são um componente fundamental das extensões desde o lançamento delas. Em termos simples, as páginas de segundo plano fornecem um ambiente que existe de forma independente de qualquer outra janela ou guia. Isso permite que as extensões observem e ajam em resposta a eventos.

Nesta página, descrevemos as tarefas para converter páginas de segundo plano em service workers de extensão. Para mais informações sobre service workers de extensão em geral, consulte o tutorial Processar eventos com service workers e a seção Sobre service workers de extensão.

Diferenças entre scripts em segundo plano e service workers de extensão

Em alguns contextos, os service workers de extensão são chamados de "scripts em segundo plano". Embora os service workers de extensão sejam executados em segundo plano, chamá-los de scripts em segundo plano é um pouco enganoso, porque implica recursos idênticos. As diferenças são descritas a seguir.

Mudanças nas páginas de plano de fundo

Os service workers têm várias diferenças em relação às páginas em segundo plano.

  • Eles funcionam fora da linha de execução principal, ou seja, não interferem no conteúdo da extensão.
  • Eles têm recursos especiais, como interceptar eventos de busca na origem da extensão, como os de um pop-up da barra de ferramentas.
  • Eles podem se comunicar e interagir com outros contextos usando a interface Clients.

Mudanças que você precisa fazer

Você precisará fazer alguns ajustes no código para considerar as diferenças entre o funcionamento dos scripts em segundo plano e dos service workers. Para começar, a forma como um service worker é especificado no arquivo de manifesto é diferente de como os scripts em segundo plano são especificados. Além disso:

  • Como eles não podem acessar o DOM ou a interface window, você precisa mover essas chamadas para uma API diferente ou para um documento fora da tela.
  • Os listeners de eventos não devem ser registrados em resposta a promessas retornadas ou dentro de callbacks de eventos.
  • Como elas não são compatíveis com versões anteriores de XMLHttpRequest(), você precisa substituir as chamadas para essa interface por chamadas para fetch().
  • Como eles são encerrados quando não estão em uso, é preciso manter os estados do aplicativo em vez de depender de variáveis globais. O encerramento de service workers também pode encerrar timers antes que eles sejam concluídos. Você precisará substituí-los por alarmes.

Nesta página, descrevemos essas tarefas em detalhes.

Atualizar o campo "background" no manifesto

No Manifest V3, as páginas em segundo plano são substituídas por um service worker. As mudanças no manifesto estão listadas abaixo.

  • Substitua "background.scripts" por "background.service_worker" em manifest.json. O campo "service_worker" usa uma string, não uma matriz de strings.
  • Remova "background.persistent" de manifest.json.
Manifest V2
{
  ...
  "background": {
    "scripts": [
      "backgroundContextMenus.js",
      "backgroundOauth.js"
    ],
    "persistent": false
  },
  ...
}
Manifesto V3
{
  ...
  "background": {
    "service_worker": "service_worker.js",
    "type": "module"
  }
  ...
}

O campo "service_worker" usa uma única string. Você só vai precisar do campo "type" se usar módulos ES (com a palavra-chave import). O valor será sempre "module". Para mais informações, consulte Noções básicas sobre service workers de extensão.

Mover chamadas de DOM e janela para um documento fora da tela

Algumas extensões precisam de acesso aos objetos DOM e de janela sem abrir visualmente uma nova janela ou guia. A API Offscreen oferece suporte a esses casos de uso abrindo e fechando documentos não exibidos empacotados com a extensão, sem interromper a experiência do usuário. Exceto pela transmissão de mensagens, os documentos fora da tela não compartilham APIs com outros contextos de extensão, mas funcionam como páginas da Web completas para as extensões interagirem.

Para usar a API Offscreen, crie um documento fora da tela no service worker.

browser.offscreen.createDocument({
  url: browser.runtime.getURL('offscreen.html'),
  reasons: ['CLIPBOARD'],
  justification: 'testing the offscreen API',
});

No documento fora da tela, execute qualquer ação que você teria executado anteriormente em um script em segundo plano. Por exemplo, você pode copiar o texto selecionado na página de host.

let textEl = document.querySelector('#text');
textEl.value = data;
textEl.select();
document.execCommand('copy');

Comunique-se entre documentos fora da tela e service workers de extensão usando a transmissão de mensagens.

Converter localStorage em outro tipo

A interface Storage da plataforma da Web (acessível em window.localStorage) não pode ser usada em um service worker. Para resolver isso, faça uma destas duas coisas: Primeiro, você pode substituí-lo por chamadas para outro mecanismo de armazenamento. O namespace browser.storage.local atende à maioria dos casos de uso, mas outras opções estão disponíveis.

Também é possível mover as chamadas para um documento fora da tela. Por exemplo, para migrar dados armazenados anteriormente em localStorage para outro mecanismo:

  1. Crie um documento fora da tela com uma rotina de conversão e um gerenciador runtime.onMessage.
  2. Adicione uma rotina de conversão ao documento fora da tela.
  3. No service worker da extensão, verifique se há dados em browser.storage.
  4. Se os dados não forem encontrados, crie um documento fora da tela e chame runtime.sendMessage() para iniciar a rotina de conversão.
  5. No gerenciador runtime.onMessage que você adicionou ao documento fora da tela, chame a rotina de conversão.

Também há algumas nuances na forma como as APIs de armazenamento na Web funcionam em extensões. Saiba mais em Armazenamento e cookies.

Registrar listeners de forma síncrona

Não há garantia de que o registro de um listener de forma assíncrona (por exemplo, dentro de uma promessa ou callback) funcione no Manifest V3. Considere o código a seguir.

browser.storage.local.get(["badgeText"], ({ badgeText }) => {
  browser.browserAction.setBadgeText({ text: badgeText });
  browser.browserAction.onClicked.addListener(handleActionClick);
});

Isso funciona com uma página de segundo plano persistente porque ela é executada constantemente e nunca é reinicializada. No Manifest V3, o service worker será reinicializado quando o evento for enviado. Isso significa que, quando o evento for acionado, os listeners não serão registrados (já que são adicionados de forma assíncrona) e o evento será perdido.

Em vez disso, mova o registro do listener de eventos para o nível superior do script. Isso garante que o Chrome possa encontrar e invocar imediatamente o gerenciador de cliques da ação, mesmo que a extensão não tenha terminado de executar a lógica de inicialização.

browser.action.onClicked.addListener(handleActionClick);

browser.storage.local.get(["badgeText"], ({ badgeText }) => {
  browser.action.setBadgeText({ text: badgeText });
});

Substituir XMLHttpRequest() por fetch() global

XMLHttpRequest() não pode ser chamado de um service worker, extensão ou outro. Substitua as chamadas do script de segundo plano para XMLHttpRequest() por chamadas para global fetch().

XMLHttpRequest()
const xhr = new XMLHttpRequest();
console.log('UNSENT', xhr.readyState);

xhr.open('GET', '/api', true);
console.log('OPENED', xhr.readyState);

xhr.onload = () => {
    console.log('DONE', xhr.readyState);
};
xhr.send(null);
fetch()
const response = await fetch('https://www.example.com/greeting.json'')
console.log(response.statusText);

Persistir estados

Os service workers são efêmeros, o que significa que eles provavelmente vão iniciar, executar e encerrar repetidamente durante a sessão do navegador de um usuário. Isso também significa que os dados não estão imediatamente disponíveis em variáveis globais, já que o contexto anterior foi desfeito. Para contornar isso, use as APIs Storage como fonte de verdade. Um exemplo vai mostrar como fazer isso.

O exemplo a seguir usa uma variável global para armazenar um nome. Em um service worker, essa variável pode ser redefinida várias vezes durante a sessão do navegador de um usuário.

Script de segundo plano do Manifest V2
let savedName = undefined;

browser.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    savedName = name;
  }
});

browser.browserAction.onClicked.addListener((tab) => {
  browser.tabs.sendMessage(tab.id, { name: savedName });
});

No Manifest V3, substitua a variável global por uma chamada para a API Storage.

Service worker do Manifest V3
browser.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    browser.storage.local.set({ name });
  }
});

browser.action.onClicked.addListener(async (tab) => {
  const { name } = await browser.storage.local.get(["name"]);
  browser.tabs.sendMessage(tab.id, { name });
});

Converter timers em alarmes

É comum usar operações atrasadas ou periódicas com os métodos setTimeout() ou setInterval(). No entanto, essas APIs podem falhar em service workers, porque os timers são cancelados sempre que o service worker é encerrado.

Script de segundo plano do Manifest V2
// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  browser.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

Em vez disso, use a API Alarms. Assim como outros listeners, os listeners de alarme precisam ser registrados no nível superior do script.

Service worker do Manifest V3
async function startAlarm(name, duration) {
  await browser.alarms.create(name, { delayInMinutes: 3 });
}

browser.alarms.onAlarm.addListener(() => {
  browser.action.setIcon({
    path: getRandomIconPath(),
  });
});

Manter o service worker ativo

Os service workers são, por definição, orientados por eventos e são encerrados em caso de inatividade. Dessa forma, o Chrome pode otimizar o desempenho e o consumo de memória da extensão. Saiba mais na nossa documentação sobre o ciclo de vida do service worker. Casos excepcionais podem exigir medidas adicionais para garantir que um service worker permaneça ativo por mais tempo.

Manter um service worker ativo até que uma operação de longa duração seja concluída

Durante operações de longa duração do service worker que não chamam APIs de extensão, o service worker pode ser encerrado no meio da operação. Por exemplo:

  • Uma solicitação fetch() que pode levar mais de cinco minutos (por exemplo, um download grande em uma conexão ruim).
  • Um cálculo assíncrono complexo que leva mais de 30 segundos.

Para estender a vida útil do service worker nesses casos, chame periodicamente uma API de extensão trivial para redefinir o contador de tempo limite. Isso é reservado apenas para casos excepcionais. Na maioria das situações, geralmente há uma maneira melhor e idiomática da plataforma de alcançar o mesmo resultado.

O exemplo a seguir mostra uma função auxiliar waitUntil() que mantém o service worker ativo até que uma determinada promessa seja resolvida:

async function waitUntil(promise) {
  const keepAlive = setInterval(browser.runtime.getPlatformInfo, 25 * 1000);
  try {
    await promise;
  } finally {
    clearInterval(keepAlive);
  }
}

waitUntil(someExpensiveCalculation());

Manter um service worker ativo continuamente

Em casos raros, é necessário estender a vida útil indefinidamente. Identificamos que os maiores casos de uso são em empresas e na educação, e permitimos isso especificamente nesses casos, mas não oferecemos suporte em geral. Nessas circunstâncias excepcionais, é possível manter um service worker ativo chamando periodicamente uma API de extensão simples. Essa recomendação se aplica apenas a extensões em execução em dispositivos gerenciados para casos de uso corporativos ou educacionais. Isso não é permitido em outros casos, e a equipe de extensões do Chrome se reserva o direito de tomar medidas contra essas extensões no futuro.

Use o snippet de código abaixo para manter o service worker ativo:

/**
 * Tracks when a service worker was last alive and extends the service worker
 * lifetime by writing the current time to extension storage every 20 seconds.
 * You should still prepare for unexpected termination - for example, if the
 * extension process crashes or your extension is manually stopped at
 * chrome://serviceworker-internals. 
 */
let heartbeatInterval;

async function runHeartbeat() {
  await browser.storage.local.set({ 'last-heartbeat': new Date().getTime() });
}

/**
 * Starts the heartbeat interval which keeps the service worker alive. Call
 * this sparingly when you are doing work which requires persistence, and call
 * stopHeartbeat once that work is complete.
 */
async function startHeartbeat() {
  // Run the heartbeat once at service worker startup.
  runHeartbeat().then(() => {
    // Then again every 20 seconds.
    heartbeatInterval = setInterval(runHeartbeat, 20 * 1000);
  });
}

async function stopHeartbeat() {
  clearInterval(heartbeatInterval);
}

/**
 * Returns the last heartbeat stored in extension storage, or undefined if
 * the heartbeat has never run before.
 */
async function getLastHeartbeat() {
  return (await browser.storage.local.get('last-heartbeat'))['last-heartbeat'];
}