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
sendResponseenvia 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 retornartrue, 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
trueno listener se estiver fazendo chamadas assíncronas. Caso contrário, o popup não receberá a resposta; - Certifique-se de ter um
try/catchno 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.