Migrar para um service worker

Como substituir páginas de plano de fundo ou evento por um worker de serviço

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

As páginas em segundo plano são um componente fundamental das extensões desde a introdução delas. Simplificando, as páginas em segundo plano fornecem 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 as tarefas para converter páginas em segundo plano em service workers de extensão. Para mais informações sobre os service workers de extensão em geral, consulte o tutorial Processar eventos com service workers e a seção Sobre os service workers de extensão.

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

Em alguns contextos, você vai encontrar workers de extensão de serviç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 em recursos idênticos. As diferenças são descritas a seguir.

Mudanças nas páginas em segundo plano

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 pela interface de clientes.

Mudanças que você precisa fazer

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

  • Como elas não podem acessar o DOM ou a interface window, é 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 em callbacks de eventos.
  • Como elas não são compatíveis com XMLHttpRequest(), você vai precisar substituir as chamadas para essa interface por chamadas para fetch().
  • Como elas são encerradas 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 da conclusão. Você vai precisar substituí-los por alarmes.

Esta página descreve essas tarefas em detalhes.

Atualize 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" no manifest.json. O campo "service_worker" aceita uma string, não uma matriz de strings.
  • Remova "background.persistent" 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ó vai precisar do campo "type" se usar módulos ES (usando a palavra-chave import). O valor sempre será "module". Para mais informações, consulte Noções básicas sobre o service worker de extensão.

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

Algumas extensões precisam acessar o DOM e os objetos 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 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 para que as extensões interajam.

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

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

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

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

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

Converter o localStorage em outro tipo

A interface Storage da plataforma da Web (acessível em window.localStorage) não pode ser usada em um worker de serviço. Para resolver isso, faça uma destas duas coisas. Primeiro, substitua-o por chamadas para outro mecanismo de armazenamento. O namespace chrome.storage.local vai atender à 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 chrome.storage para seus dados.
  4. Se os dados não forem encontrados, create 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 sobre como as APIs de armazenamento da Web funcionam em extensões. Saiba mais em Armazenamento e cookies.

Registrar listeners de forma síncrona

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

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 ela está constantemente em execução 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 manipulador de cliques da ação, mesmo que a extensão não tenha terminado de executar a 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 de um service worker, extensão ou de outra forma. 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 de persistência

Os service workers são efêmeros, 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, já que o contexto anterior foi desfeito. Para contornar esse problema, use as APIs de armazenamento como a fonte da verdade. Confira um exemplo de 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 de um 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 manifesto 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 em 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 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 chrome.alarms.create(name, { delayInMinutes: 3 });
}

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

Manter o worker de serviço ativo

Por definição, os service workers são orientados a eventos e são encerrados em caso de inatividade. Assim, o Chrome pode otimizar o desempenho e o consumo de memória da sua extensão. Saiba mais na documentação sobre o ciclo de vida do service worker. Casos excepcionais podem exigir medidas adicionais para garantir que um worker de serviço 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 encerrado no meio da operação. Por exemplo:

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

Para estender a vida útil do worker do serviço 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, há uma maneira melhor e mais adequada à 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(chrome.runtime.getPlatformInfo, 25 * 1000);
  try {
    await promise;
  } finally {
    clearInterval(keepAlive);
  }
}

waitUntil(someExpensiveCalculation());

Manter um worker de serviço ativo continuamente

Em casos raros, é necessário estender a vida útil indefinidamente. Identificamos o setor corporativo e a educação como os maiores casos de uso, e permitimos isso especificamente, mas não oferecemos suporte a isso em geral. Nessas circunstâncias excepcionais, é possível manter um service worker ativo chamando periodicamente uma API de extensão trivial. Essa recomendação se aplica apenas a extensões executadas 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 worker de serviço 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'];
}