Processar eventos com service workers

Tutorial que aborda conceitos de service workers de extensão

Visão geral

Este tutorial apresenta os service workers da extensão do Chrome. Como parte deste tutorial, você vai criar uma extensão que permite que os usuários naveguem rapidamente para as páginas de referência da API do Chrome usando a omnibox. Você aprenderá o seguinte:

  • Registre o service worker e importe os módulos.
  • Depure seu service worker de extensão.
  • Gerenciar o estado e processar eventos.
  • Acionar eventos periódicos.
  • Comunique-se com scripts de conteúdo.

Antes de começar

Este guia pressupõe que você tenha experiência básica em desenvolvimento na Web. Recomendamos que você analise Extensões 101 e Hello World para uma introdução ao desenvolvimento de extensões.

Criar a extensão

Comece criando um novo diretório chamado quick-api-reference para armazenar os arquivos de extensão ou Faça o download do código-fonte no nosso repositório de exemplos do GitHub (link em inglês).

Etapa 1: registrar o service worker

Crie o arquivo de manifesto na raiz do projeto e adicione o seguinte código:

manifest.json:

{
  "manifest_version": 3,
  "name": "Open extension API reference",
  "version": "1.0.0",
  "icons": {
    "16": "images/icon-16.png",
    "128": "images/icon-128.png"
  },
  "background": {
    "service_worker": "service-worker.js"
  }
}

As extensões registram o service worker no manifesto, que usa apenas um arquivo JavaScript. Não é necessário chamar navigator.serviceWorker.register(), como você faria em uma página da Web.

Crie uma pasta images e faça o download dos ícones nela.

Confira as primeiras etapas do tutorial sobre o tempo de leitura para saber mais sobre os metadados e os ícones da extensão no manifesto.

Etapa 2: importar vários módulos do service worker

Nosso service worker implementa dois recursos. Para facilitar a manutenção, vamos implementar cada recurso em um módulo separado. Primeiro, precisamos declarar o service worker como um módulo ES no manifesto, o que permite importar módulos nele:

manifest.json:

{
 "background": {
    "service_worker": "service-worker.js",
    "type": "module"
  },
}

Crie o arquivo service-worker.js e importe dois módulos:

import './sw-omnibox.js';
import './sw-tips.js';

Crie esses arquivos e adicione um registro do console a cada um deles.

sw-omnibox.js:

console.log("sw-omnibox.js");

sw-tips.js:

console.log("sw-tips.js");

Consulte Como importar scripts para aprender outras maneiras de importar vários arquivos em um service worker.

Opcional: como depurar o service worker

Vou explicar como encontrar os registros do worker do serviço e saber quando ele foi encerrado. Primeiro, siga as instruções para carregar uma extensão descompactada.

Após 30 segundos, será exibido "service worker (inativo)". ou seja, o service worker foi encerrado. Clique em "service worker (inativo)" para inspecioná-la. A animação a seguir mostra isso.

Você notou que inspecionar o worker de serviço o ativou? A abertura do service worker no DevTools vai mantê-lo ativo. Para garantir que a extensão se comporte corretamente quando o service worker for encerrado, feche o DevTools.

Agora, interrompa a extensão para saber onde localizar erros. Uma maneira de fazer isso é excluir ".js" da importação './sw-omnibox.js' no arquivo service-worker.js. O Chrome não vai conseguir registrar o service worker.

Volte para chrome://extensions e atualize a extensão. Você vai encontrar dois erros:

Service worker registration failed. Status code: 3.

An unknown error occurred when fetching the script.

Consulte Como depurar extensões para mais maneiras de depurar o worker de serviço da extensão.

Etapa 4: inicializar o estado

O Chrome vai encerrar os workers de serviço se eles não forem necessários. Usamos a API chrome.storage para manter o estado entre as sessões do service worker. Para acessar o armazenamento, precisamos solicitar permissão no manifesto:

manifest.json:

{
  ...
  "permissions": ["storage"],
}

Primeiro, salve as sugestões padrão no armazenamento. Podemos inicializar o estado quando a extensão for instalada pela primeira vez, detectando o evento runtime.onInstalled():

sw-omnibox.js:

...
// Save default API suggestions
chrome.runtime.onInstalled.addListener(({ reason }) => {
  if (reason === 'install') {
    chrome.storage.local.set({
      apiSuggestions: ['tabs', 'storage', 'scripting']
    });
  }
});

Os workers de serviço não têm acesso direto ao objeto de janela e, portanto, não podem usar window.localStorage para armazenar valores. Além disso, os service workers são ambientes de execução de curta duração. eles são encerrados repetidamente durante a sessão do navegador do usuário, o que os torna incompatíveis com variáveis globais. Em vez disso, use chrome.storage.local, que armazena dados na máquina local.

Consulte Persistência de dados em vez de uso de variáveis globais para saber mais sobre outras opções de armazenamento para service workers de extensão.

Etapa 5: registrar seus eventos

Todos os listeners de eventos precisam ser registrados de forma estática no escopo global do service worker. Em outras palavras, os listeners de eventos não podem ser aninhados em funções assíncronas. Dessa forma, o Chrome pode garantir que todos os manipuladores de eventos sejam restaurados no caso de uma reinicialização do service worker.

Neste exemplo, vamos usar a API chrome.omnibox, mas primeiro precisamos declarar o acionador de palavras-chave da omnibox no manifesto:

manifest.json:

{
  ...
  "minimum_chrome_version": "102",
  "omnibox": {
    "keyword": "api"
  },
}

Agora, registre os listeners de eventos da caixa de pesquisa no nível superior do script. Quando o usuário digita a palavra-chave da omnibox (api) na barra de endereço seguida por guia ou espaço, o Chrome exibe uma lista de sugestões com base nas palavras-chave armazenadas. O evento onInputChanged(), que usa a entrada atual do usuário e um objeto suggestResult, é responsável por preencher essas sugestões.

sw-omnibox.js:

...
const URL_CHROME_EXTENSIONS_DOC =
  'https://developer.chrome.com/docs/extensions/reference/';
const NUMBER_OF_PREVIOUS_SEARCHES = 4;

// Display the suggestions after user starts typing
chrome.omnibox.onInputChanged.addListener(async (input, suggest) => {
  await chrome.omnibox.setDefaultSuggestion({
    description: 'Enter a Chrome API or choose from past searches'
  });
  const { apiSuggestions } = await chrome.storage.local.get('apiSuggestions');
  const suggestions = apiSuggestions.map((api) => {
    return { content: api, description: `Open chrome.${api} API` };
  });
  suggest(suggestions);
});

Depois que o usuário selecionar uma sugestão, onInputEntered() vai abrir a página de referência da API do Chrome correspondente.

sw-omnibox.js:

...
// Open the reference page of the chosen API
chrome.omnibox.onInputEntered.addListener((input) => {
  chrome.tabs.create({ url: URL_CHROME_EXTENSIONS_DOC + input });
  // Save the latest keyword
  updateHistory(input);
});

A função updateHistory() recebe a entrada da omnibox e a salva em storage.local. Dessa forma, o termo de pesquisa mais recente pode ser usado posteriormente como uma sugestão da omnibox.

sw-omnibox.js:

...
async function updateHistory(input) {
  const { apiSuggestions } = await chrome.storage.local.get('apiSuggestions');
  apiSuggestions.unshift(input);
  apiSuggestions.splice(NUMBER_OF_PREVIOUS_SEARCHES);
  return chrome.storage.local.set({ apiSuggestions });
}

Etapa 6: configurar um evento recorrente

Os métodos setTimeout() ou setInterval() costumam ser usados para executar tarefas atrasadas ou periódicas tarefas. No entanto, essas APIs podem falhar porque o programador vai cancelar os timers quando o worker do serviço for encerrado. Em vez disso, as extensões podem usar a API chrome.alarms.

Comece solicitando a permissão "alarms" no manifesto. Além disso, para buscar as dicas de extensão de um local hospedado remotamente, você precisa solicitar a permissão de host:

manifest.json:

{
  ...
  "permissions": ["storage"],
  "permissions": ["storage", "alarms"],
  "host_permissions": ["https://extension-tips.glitch.me/*"],
}

A extensão vai buscar todas as dicas, escolher uma aleatoriamente e salvar no armazenamento. Criaremos um alarme que será acionado uma vez por dia para atualizar a gorjeta. Os alarmes não são salvos quando você fecha o Chrome. Portanto, precisamos verificar se o alarme existe e criar um se não existir.

sw-tips.js:

// Fetch tip & save in storage
const updateTip = async () => {
  const response = await fetch('https://extension-tips.glitch.me/tips.json');
  const tips = await response.json();
  const randomIndex = Math.floor(Math.random() * tips.length);
  return chrome.storage.local.set({ tip: tips[randomIndex] });
};

const ALARM_NAME = 'tip';

// Check if alarm exists to avoid resetting the timer.
// The alarm might be removed when the browser session restarts.
async function createAlarm() {
  const alarm = await chrome.alarms.get(ALARM_NAME);
  if (typeof alarm === 'undefined') {
    chrome.alarms.create(ALARM_NAME, {
      delayInMinutes: 1,
      periodInMinutes: 1440
    });
    updateTip();
  }
}

createAlarm();

// Update tip once a day
chrome.alarms.onAlarm.addListener(updateTip);

Etapa 7: comunicar com outros contextos

As extensões usam scripts de conteúdo para ler e modificar o conteúdo da página. Quando um usuário visita uma página de referência da API do Chrome, o script de conteúdo da extensão atualiza a página com a dica do dia. Ele envia uma mensagem para solicitar a gorjeta do dia do service worker.

Comece declarando o script de conteúdo no manifesto e adicione o padrão de correspondência correspondente à documentação de referência da API do Chrome.

manifest.json:

{
  ...
  "content_scripts": [
    {
      "matches": ["https://developer.chrome.com/docs/extensions/reference/*"],
      "js": ["content.js"]
    }
  ]
}

Crie um novo arquivo de conteúdo. O código a seguir envia uma mensagem ao service worker solicitando a gorjeta. Em seguida, adiciona um botão que abrirá um pop-over contendo a gorjeta da extensão. Esse código usa a nova API Popover da plataforma da Web.

content.js:

(async () => {
  // Sends a message to the service worker and receives a tip in response
  const { tip } = await chrome.runtime.sendMessage({ greeting: 'tip' });

  const nav = document.querySelector('.upper-tabs > nav');
  
  const tipWidget = createDomElement(`
    <button type="button" popovertarget="tip-popover" popovertargetaction="show" style="padding: 0 12px; height: 36px;">
      <span style="display: block; font: var(--devsite-link-font,500 14px/20px var(--devsite-primary-font-family));">Tip</span>
    </button>
  `);

  const popover = createDomElement(
    `<div id='tip-popover' popover style="margin: auto;">${tip}</div>`
  );

  document.body.append(popover);
  nav.append(tipWidget);
})();

function createDomElement(html) {
  const dom = new DOMParser().parseFromString(html, 'text/html');
  return dom.body.firstElementChild;
}

A etapa final é adicionar um gerenciador de mensagens ao service worker que envia uma resposta ao script de conteúdo com a gorjeta diária.

sw-tips.js:

...
// Send tip to content script via messaging
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.greeting === 'tip') {
    chrome.storage.local.get('tip').then(sendResponse);
    return true;
  }
});

Testar se funciona

Verifique se a estrutura de arquivos do projeto tem a seguinte aparência:

O conteúdo da pasta de extensão: pasta de imagens, manifest.json, service-worker.js, sw-omnibox.js, sw-tips.js
e content.js

Carregar sua extensão localmente

Para carregar uma extensão descompactada no modo de desenvolvedor, siga as etapas em Hello World.

Abrir uma página de referência

  1. Insira a palavra-chave "api" na barra de endereço do navegador.
  2. Pressione "tab" ou "espaço".
  3. Insira o nome completo da API.
    • OU escolher em uma lista de pesquisas anteriores
  4. Uma nova página será aberta para a página de referência da API Chrome.

Ele será parecido com o seguinte:

Referência rápida da API abrindo a referência da API do ambiente de execução
Extensão da API rápida que abre a API Runtime.

Abrir a dica do dia

Clique no botão "Dica" na barra de navegação para abrir a dica da extensão.

Abrir dica diária em
Extensão de API rápida que abre a dica do dia.

🎯 Possíveis melhorias

Com base no que você aprendeu hoje, tente realizar uma das seguintes ações:

  • Conheça outra maneira de implementar as sugestões da caixa de pesquisa.
  • Crie seu próprio modal personalizado para mostrar a dica de extensão.
  • Abra uma página adicional para as páginas de referência da API de extensões da Web do MDN.

Continue criando!

Parabéns por concluir este tutorial 🎉. Continue melhorando suas habilidades concluindo outros tutoriais para iniciantes:

Extensão O que você vai aprender
Tempo de leitura Para inserir um elemento em um conjunto específico de páginas automaticamente.
Gerenciador de guias Para criar um pop-up que gerencie as guias do navegador.
Modo de foco Para executar o código na página atual depois de clicar na ação da extensão.

Continue descobrindo

Para continuar seu caminho de aprendizado sobre o worker de serviço de extensão, recomendamos que você leia os seguintes artigos: