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 até 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.
  • Comunicar com scripts de conteúdo.

Antes de começar

Neste guia, presumimos que você tenha experiência básica em desenvolvimento da Web. Recomendamos que você consulte 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 do nosso repositório de exemplos do GitHub.

Etapa 1: registrar o service worker

Crie o arquivo manifest 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 do Tempo de leitura para saber mais sobre os metadados e os ícones da extensão no manifesto.

Etapa 2: importar vários módulos de worker de serviço

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 no service worker:

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 saber 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 for encerrado. Primeiro, siga as instruções para carregar uma extensão descompactada.

Após 30 segundos, você verá "service worker (inativo)", o que significa que o service worker foi encerrado. Clique no link "service worker (inativo)" para inspecioná-lo. 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, quebre 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 em todas as sessões do service worker. Para o acesso ao armazenamento, precisamos solicitar a 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 é 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 as variáveis globais. Em vez disso, use chrome.storage.local, que armazena dados na máquina local.

Consulte Persistir dados em vez de usar variáveis globais para aprender 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 em caso de reinicialização de um 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 de tabulação ou espaço, o Chrome mostra uma lista de sugestões com base nas palavras-chave no armazenamento. O evento onInputChanged(), que recebe a entrada do usuário atual 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, o onInputEntered() abrirá a página de referência da API 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() são normalmente usados para realizar tarefas atrasadas ou periódicas. No entanto, essas APIs podem falhar porque o agendador cancela os timers quando o worker do serviço é 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 da extensão em um local hospedado remoto, é necessário solicitar a permissão do 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. Vamos criar um alarme que será acionado uma vez por dia para atualizar a dica. 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-se 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 worker do serviço solicitando a dica. Em seguida, adiciona um botão que vai abrir um pop-up com a dica de 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 dica 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 a 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. Digite a palavra-chave "api" na barra de endereço do navegador.
  2. Pressione "tab" ou "espaço".
  3. Digite o nome completo da API.
    • OU escolha de uma lista de pesquisas anteriores
  4. Uma nova página será aberta com a página de referência da API do Chrome.

Ele será parecido com o seguinte:

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

Abrir a dica do dia

Clique no botão Tip localizado na barra de navegação para abrir a dica de extensão.

Abrir a dica diária em
Extensão da API rápida abrindo a dica do dia.

🎯 Possíveis melhorias

Com base no que você aprendeu hoje, tente fazer o seguinte:

  • 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: