Criação de plugin para Google Chrome - Uma introdução

4 de abril de 2022

Introdução

Ao criar um plugin (extensão) para navegadores baseados em Chromium, é necessário entender que são como páginas web rodando localmente no navegador em que está instalado. Rodam em um ambiente separado, mas com capacidade de interagirem com o navegador, com abas (sites) abertos e até mesmo com outros plugins instalados.

As mesmas tecnologias usadas para criação de um site são empregadas ao criar um plugin, sendo o mínimo necessário apenas Javascript. Para uma melhor apresentação e maior interação, HTML e CSS são utilizados.

É possível realizar leitura do conteúdo de uma determinada página, salvar dados locais e até mesmo compartilhados entre outros navegadores em que você está logado.

No nosso exemplo vamos criar um plugin para inverter as cores de uma página qualquer.

O que é necessário?

O arquivo mais importante para começar seu plugin é o arquivo manifest.json, o qual guarda informações importantes sobre o projeto, como nome, versão, arquivo de execução principal, permissões etc.

Arquivo manifesto

Como dito antes, é o arquivo mais importante. Nele você vai especificar alguns dados relacionados ao seu plugin. Os 3 dados necessários são: name, manifest_version e version.

{
    "name": "Nome da extensão",
    "manifest_version": 3,
    "version": "0.0.1"
}

O detalhe importante aqui é a versão do manifesto. Existe a versão 2 e versão 3. Estaremos utilizando a versão 3. Futuramente pretendo explicar um pouco a diferença entre as versões.

Um arquivo json com esses dados já são um plugin válido que já pode ser colocado no Chrome. Para testar, entre em chrome://extensions, ative o modo de desenvolvedor e clique no botão Carregar sem compactação. Após isso, deve existir um plugin na página com o nome que colocou e a versão. Obviamente esse plugin não faz nada, mas logo veremos como dar vida a isso.

Background

A próxima chave que vamos conhecer é a background que abriga o conjunto chave/valor a seguir:

{
    "name": "Nome da extensão",
    "manifest_version": 3,
    "version": "0.0.1",
    "background": {
        "service_worker": "background.js"
    }
}

Definimos então nosso primeiro arquivo javascript que é a base para o plugin. O nome do arquivo do nosso service worker é colocado aqui. Com isso podemos criar um arquivo com o mesmo nome no diretório atual.

Para ver a execução, coloce um

console.log('Hello Plugin')

no arquivo do service worker. Para visualizar o console, volte à página de extensões e clique no ícone de atualizar para o plugin carregar as novas informações. Após isso, deve aparecer um link escrito “service worker”. Clique nesse link e será aberto o Chrome Dev Tools do plugin. No Chrome Dev Tools, clique na aba do Console e lá deve encontrar a mensagem que deixou no arquivo do service worker.

Permissões

As permissões dão a possibilidade de adicionarmos funcionalidades que não seriam possíveis sem elas, como injeção de scripts, armazenar informações etc.

Por exemplo, para que seja possível usarmos as funções de guardar dados, é necessário adicionar a permissão storage. Para isso, incluímos uma chave permissions em nosso manifesto que recebe uma lista de strings.

{
    "name": "Nome da extensão",
    "manifest_version": 3,
    "version": "0.0.1",
    "background": {
        "service_worker": "background.js"
    },
    "permissions": ["storage"]
}

Com a permissão de storage, podemos utilizar duas funções dentro do arquivo background.js: set e get. Eles funcionam da seguinte forma:

// Assim salvamos uma chave key com um valor qualquer
// O callback é chamado após o armazenamento, sendo então dispensável.
// a propriedade storage só estará disponível se a permissão falada anteriormente tiver sido colocada.
chrome.storage.local.set({key: value}, callback) // pode ser local ou sync

// A forma de recuperar a chave é parecida, utilizamos a função get(),
// onde o primeiro parâmetro é uma lista de strings com as chaves que deseja pegar.
// o segundo parâmetro (opcional) é uma função que tem um objeto com o resultado.
chrome.storage.local.get(['key'], ({key}) => {}) // também pode ser local ou sync.

Existe uma versão com retorno Promise tanto para set() como para get(), basta remover o callback.

Action

A action permite realizar configurações e operações relacionadas ao plugin na barra de ferramentas (barra onde ficam os plugins, ao lado da barra de endereço). É possível adicionar título que aparece ao passar o mouse por cima do plugin, adicionar ação de popup (criado com tecnologias web), definir o ícone, colocar texto por cima do ícone (badge) etc.

Para começar, adicionamos uma nova chave no arquivo manifesto, a chave action. Essa chave é de um objeto que aceita diversas chaves, como default_icon, default_title, default_popup etc.

{
    "name": "Nome da extensão",
    "manifest_version": 3,
    "version": "0.0.1",
    "background": {
        "service_worker": "background.js"
    },
    "permissions": ["storage"],
    "action": {
        "default_title": "Meu título que aparece no hover",
        "default_popup": "popup.html"
    }
}

Em default_popup deve ser passado o endereço e nome do arquivo HTML que será aberto ao clicarmos na extensão.

Caso instale o plugin dessa forma, sem criar o popup.html, e clicar no plugin, verá uma página de erro informando que não conseguiu encontrar o arquivo. Para resolver isso, basta criar o arquivo no endereço indicado.

Exemplo

Nossa extensão servirá para inverter as cores de uma página.

Não usaremos o arquivo de background.js nesse projeto.

Teremos um plugin que abre um popup contendo um check button para ativar/desativar a inversão de cores. Assim usaremos de tudo que vimos até agora.

Manifesto

Nosso manifesto será:

manifest.json

{
    "name": "Inverte cores extension",
    "manifest_version": 3,
    "version": "0.0.1",
    "permissions": ["storage", "scripting"],
    "action": {
        "default_title": "Clique aqui para abrir",
        "default_popup": "popup.html"
    },
    "host_permissions": ["https://*/*"]
}

Aqui temos algumas mudanças do que vimos antes, trocamos o texto de name e default_title, adicionamos uma nova permissão (scripting), além de incluírmos o host_permissions com um valor que permite o plugin ser executado em qualquer site. Removemos o background.

Lembre-se de criar os arquivos do service worker e do popup.

Arquivo do popup

Adicionamos um checkbox para saber se o efeito deve estar ativado ou não na página.

popup.html

<!DOCTYPE html>
<html>
<body>
    <div style="width: 100px;">
        <input type="checkbox" id="activate" /> Inverter cor
    </div>
    <script src="popup.js"></script>
</body>
</html>

Além do checkbox, temos também o popup.js que será utilizado para informar quando o status da checkbox mudar.

Toda vez que o popup é aberto, esse arquivo será executado novamente.

popup.js

// Encontra o checkbox que está no HTML
const checkbox = document.getElementById('activate')

// Função usada dentro do listener que veremos a seguir.
async function listener() {
    const currentTab = await getCurrentTab()

    // Salva o estado atual do checkbox com base na aba,
    // possibilitando sabermos o estado para cada aba.
    // Além disso, armazenamos em invert o estado atual,
    // facilitando a alteração do estado local futuramente.
    await chrome.storage.local.set({
        [currentTab.id]: checkbox.checked,
        invert: checkbox.checked
    })
    
    // Executa o script que aplica o filtro ou não depenpendo do estado.
    chrome.scripting.executeScript({
        target: {tabId: currentTab.id },
        files: ['activate.js']
    })
}

// Ao passarmos null como argumento, informamos que queremos tudo que está armazenado.
chrome.storage.local.get(null, async (data) => {
    // adicionamos uma função de listener quando ocorrer o evento change.
    checkbox.addEventListener('change', listener, false)

    const currentTab = await getCurrentTab()

    // altera o valor de checked caso o estado já tenha sido salvo antes para essa aba.
    if (data[currentTab.id]) {
        checkbox.checked = data[currentTab.id]
    }
})

// Removemos o evento quando o popup é fechado.
window.onclose = () => {
    checkbox.removeEventListener('change', listener, false)
}

// É feito uma busca nas queries com base nas opções active e currentWindow,
// assim pegamos a aba que estamos executando o popup.
async function getCurrentTab() {
    let queryOptions = { active: true, currentWindow: true };
    let [tab] = await chrome.tabs.query(queryOptions);
    return tab;
}

Arquivo de scripting

Este arquivo é executado após a chamada de scripting que ocorre no arquivo de popup.js, dentro do listener.

Quando o arquivo é executado, já existe alguns valores salvos, e queremos verificar o que está em invert, caso invert seja verdadeiro, aplicamos o filtro, caso não seja verdadeiro, removemos o filtro.

chrome.storage.local.get(['invert'], async ({invert}) => {
    const html = document.getElementsByTagName('html')[0]
    if (invert) {
        html.style.filter = 'invert(100%)'
    } else {
        html.style.filter = ''
    }
})

Conclusão

Criamos um plugin bem simples, mas completo em questões de usabilidade. Possui ações para o plugin, utiliza armazenamento local, além de utilização de execução de scripts.


Escrito por Eduardo Santos , brasileiro em Lisboa, desenvolvedor de software fullstack em Angular e Java. Sempre aprendendo novas tecnologias e linguagens de programação diferentes. Siga-me no Twitter.