Migrar para um service worker

Substituir páginas de eventos ou de segundo plano por um service worker

Um service worker substitui o segundo plano ou a página de eventos da extensão para garantir que o código em 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 fundo foram um componente fundamental das extensões desde sua introdução. Simplificando, as páginas de segundo plano oferecem um ambiente independente de qualquer outra janela ou guia. Isso permite que as extensões observem e atuem em resposta a eventos.

Esta página descreve tarefas para converter páginas de segundo plano em service workers de extensão. Para ver mais informações sobre os service workers em geral, consulte o tutorial Gerenciar eventos com os service workers e a seção Sobre os service workers de extensão.

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

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

Alterações das páginas 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, o que significa que não interferem no conteúdo da extensão.
  • Elas têm recursos especiais, como interceptar eventos de busca na origem da extensão, por exemplo, os de um pop-up da barra de ferramentas.
  • Eles podem se comunicar e interagir com outros contextos pela interface de clientes.

Alterações que você precisará fazer

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

  • Como eles não podem acessar o DOM ou a interface window, será necessário 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 retornos de chamada de eventos.
  • Como elas não são compatíveis com versões anteriores de XMLHttpRequest(), substitua as chamadas para essa interface por chamadas para fetch().
  • Como eles são encerrados quando não estão em uso, é necessário manter os estados do aplicativo em vez de depender de variáveis globais. O encerramento de service workers também pode encerrar os timers antes que eles sejam concluídos. Será necessário substituí-los por alarmes.

Nesta página, descrevemos essas tarefas em detalhes.

Atualizar o campo "background" no manifesto

No Manifest V3, as páginas de 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" no manifest.json. O campo "service_worker" usa uma string, não uma matriz de strings.
  • A "background.persistent" foi removida do 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ó precisará do campo "type" se usar módulos ES (usando a palavra-chave import). O valor sempre será "module". Para mais informações, consulte Conceitos básicos do service worker de extensão.

Mover chamadas de janela e DOM 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 aceita esses casos de uso porque abre e fecha documentos não exibidos empacotados com extensão sem interromper a experiência do usuário. Exceto para a 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 com as quais as extensões podem interagir.

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

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

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

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

Comunicação entre documentos fora da tela e service workers de extensão usando a transmissão de mensagens.

Converter localStorage para 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 das duas ações a seguir. Primeiro, é possível substituí-lo por chamadas para outro mecanismo de armazenamento. O namespace chrome.storage.local atende a 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 já armazenados 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 de extensão, verifique seus dados em chrome.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.

Há também algumas nuances no funcionamento das APIs de armazenamento da Web nas extensões. Saiba mais em Armazenamento e cookies.

Registrar listeners de maneira síncrona

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

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

Isso funciona com uma página de segundo plano persistente, porque a página está em execução constantemente e nunca é reinicializada. No Manifesto V3, o service worker será reinicializado quando o evento for enviado. Isso significa que, quando o evento for disparado, os listeners não serão registrados (porque foram 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 sua ação, mesmo que a extensão não tenha concluído a execução da lógica de inicialização.

chrome.action.onClicked.addListener(handleActionClick);

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

Substituir XMLHttpRequest() por fetch() global

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

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);

Estados persistentes

Os service workers são temporários, o que significa que eles provavelmente serão iniciados, executados e encerrados 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 nas variáveis globais porque o contexto anterior foi removido. Para contornar isso, use as APIs de armazenamento como fonte da verdade. Um exemplo mostra 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 ao longo da sessão do navegador do usuário.

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

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

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

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

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

chrome.action.onClicked.addListener(async (tab) => {
  const { name } = await chrome.storage.local.get(["name"]);
  chrome.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 nos service workers porque os timers são cancelados sempre que o service worker é encerrado.

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

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

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

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

Manter o service worker ativo

Os service workers, por definição, são orientados por eventos e são encerrados em inatividade. Dessa forma, o Chrome otimiza o desempenho e o consumo de memória da sua 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 service worker de longa duração que não chamam APIs de extensão, o service worker pode ser desligado no meio da operação. Por exemplo:

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

Para prolongar o ciclo de vida do service worker nesses casos, você pode chamar periodicamente uma API de extensão trivial para redefinir o contador de tempo limite. Isso é reservado apenas para casos excepcionais e, na maioria das situações, geralmente há uma forma melhor e idiomática de chegar ao 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(chrome.runtime.getPlatformInfo, 25 * 1000);
  try {
    await promise;
  } finally {
    clearInterval(keepAlive);
  }
}

waitUntil(someExpensiveCalculation());

Manter um service worker ativo continuamente

Em casos raros, é necessário estender o ciclo de vida indefinidamente. Identificamos empresas e instituições de ensino como os maiores casos de uso e permitimos especificamente esses apps, mas não oferecemos suporte para isso em geral. Nessas circunstâncias excepcionais, é possível manter um service worker ativo chamando periodicamente uma API de extensão trivial. É importante observar que essa recomendação só se aplica a extensões em execução em dispositivos gerenciados para casos de uso empresariais 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 a seguir 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 chrome.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 chrome.storage.local.get('last-heartbeat'))['last-heartbeat'];
}