20/08/2023 ~ 4 min de leitura

Arquitetura de uma Extensão do Chrome


Olá de novo. Estou trabalhando com uma extensão do Chrome há algum tempo e queria compartilhar algumas reflexões sobre ela, além de algumas ideias de como arquitetá-la.

Como uma extensão do Chrome funciona

Extensões do Chrome são bem simples. Você tem um popup (aquela janela que aparece ao clicar no ícone da extensão) e um script em segundo plano. O script em segundo plano é quem faz o trabalho pesado. Ele pode ser um script simples ou um service worker. O service worker é um script executado em segundo plano, mas que também pode interceptar e modificar requisições de rede. Ele também pode armazenar requisições e respostas em cache e enviar notificações push. As extensões também podem ter uma página de configuração, que é uma página HTML simples usada para configurar a extensão.

A arquitetura da extensão

Minha extensão usa esses três elementos. O popup mostra a interface principal da extensão, onde os usuários mais interagem. A página de configuração serve apenas para definir para onde as chamadas de API devem ir. O script em segundo plano faz todas as chamadas de API, o cache e o processamento da extensão.

Enviando mensagens

Quando o usuário interage com algo, por exemplo ao fazer login, o popup precisa fazer uma chamada de API ao nosso servidor Strapi para autenticar o usuário. Eu poderia ter feito isso no próprio popup. Ele tem todo o aparato de fetch de um navegador comum. Em vez disso, o popup envia uma mensagem para o script em segundo plano.

Por exemplo, esta é minha função de login no popup:

const handleSubmit = useCallback(
  async (e: SyntheticEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const { data, error } = await chrome.runtime.sendMessage<
      ChromeMessage<LoginInfo>,
      ChromeResponse<boolean>
    >({
      type: "authenticate",
      payload: {
        identifier: formData.get("email").toString(),
        password: formData.get("password").toString(),
      },
    });
    if (data) {
      onLoginSuccess();
    } else if (error) {
      setError(true);
    }
  },
  [onLoginSuccess],
);

Todas essas tipagens ChromeMessage e ChromeResponse são criações minhas. Decidi definir os tipos de uma forma em que eu sempre saiba com qual tipo estou lidando.

Então, passo um type e um payload para o script em segundo plano. O payload pode conter qualquer coisa serializável. Todas as outras chamadas da extensão seguem esse padrão.

Recebendo mensagens

O script em segundo plano recebe a mensagem e faz a chamada de API. Este é o código do login:

async function handleAuthenticate(
  request: ChromeMessage<{ identifier: string; password: string }>,
) {
  try {
    const result = await api.authenticate(
      request.payload.identifier,
      request.payload.password,
    );
    return getFormattedData(result, null);
  } catch (e) {
    getFormattedData(null, e);
  }
}

chrome.runtime.onMessage.addListener((request, _, sendResponse) => {
  try {
    (async () => {
      switch (request.type) {
        case "authenticate": {
          sendResponse(await handleAuthenticate(request));
          break;
        }
      }
    })();
  } catch (e) {
    console.error("error", e);
  }

  return true;
});

Alguns aspectos importantes aqui:

  • chrome.runtime.onMessage.addListener é o listener das mensagens enviadas pelo popup;
  • o callback sendResponse envia dados de volta ao popup;
  • o return true é importante. Ele informa ao Chrome que o listener é assíncrono e que deve aguardar a resposta. Se você não retornar true, o popup não receberá a resposta;
  • getFormattedData é uma função que criei para formatar a resposta. É apenas uma função auxiliar;
  • api.authenticate é uma função que criei para fazer a chamada de API. Pense nela como qualquer método que você usa para fazer chamadas de API.

Cache

O script em segundo plano também armazena coisas em cache, como tokens JWT e algumas preferências do usuário. Uso a API chrome.storage para isso. A forma de fazer é exatamente a mesma das chamadas de API: envio uma mensagem para o script em segundo plano e ele faz o cache; ou chamo o script em segundo plano para obter algo que está em cache.

Considerações importantes

  • Certifique-se de ter um, e somente um, chrome.runtime.onMessage.addListener. Se tiver mais de um, as coisas ficam loucas e seus callbacks começam a se comportar de maneiras inesperadas;
  • Certifique-se de retornar true no listener se estiver fazendo chamadas assíncronas. Caso contrário, o popup não receberá a resposta;
  • Certifique-se de ter um try/catch no listener. Caso contrário, se ocorrer um erro, o popup não receberá a resposta;

Conclusão

Estou aprendendo muito com esta extensão. Talvez eu publique mais coisas aqui conforme o projeto avança.

Abraços.


Headshot of Andrey Luiz

Gosto de escrever sobre desenvolvimento de software, mas vamos ser sinceros: gosto muito de reclamar. Para tentar reclamar menos, também toco tuba e faço crochê.