Add-ons maken voor alle gangbare browsers
Uitbreidingen voor (bijna) alle gangbare browsers maken
De uitbreidingsinterfaces van de gangbare browsers lijken veel op elkaar, waardoor dezelfde code werkt in zowel Chrome, Firefox, Edge,
Opera als Vivaldi. Daardoor kun je verschillende browsers snel voorzien van dezelfde add-ons. We tonen aan de hand van een voorbeeld hoe je een vertaal add-on maakt op basis van de service DeepL.
Firefox ondersteunt vanaf versie 57 alleen nog maar add-ons die aan de WebExtensions API voldoen, wat min of meer dezelfde interface is die Chrome, Edge, Opera en Vivaldi gebruiken. Alleen Safari doet wat add-ons betreft zijn eigen ding. Daardoor wordt het een ingewikkeld verhaal om een uitbreiding naar Safari te porteren.
Als voorbeeld gaan we een vertaaltool maken waarmee tekst die je selecteert op een website, naar de dienst DeepL wordt gestuurd. Dit is een redelijk nieuwe vertaaldienst die op het moment veel aandacht trekt.
Een browserextensie bestaat uit een verzameling bestanden in een zipfile waarvan de belangrijkste onderdelen – JavaScript, CSS, HTML, Icons – worden omschreven door een bestand met de naam manifest.json:
“manifest_version”: 2,
“name”: “DeeptransLate”,
“version”: “0.1”,
“author”: “c’t Magazine”, “description”: “Tekst ...”,
“icons”:{
“16”: “icon16.png”, ... },
“browser_action”:{ “default_title”:
“DeeptransLate”, “default_popup”:
“dashboard.html”, “default_icon”:{ “16”: “icon16.png”, ...
}
}
De eerste drie regels – manifest_version, name en version – zijn een vereiste voor alle browsers. Edge eist ook dat er een author genoemd wordt.
In het manifest zitten enkele struikelblokken. Zo zou het logisch lijken iets als homepage_url tegen te komen, alleen zal Edge dit zien als een ongeldig gegevensformaat. Een regel als offline_enabled:
false zou Chrome en aanverwanten op de hoogte stellen dat de uitbreiding alleen zinvol is wanneer je online bent, alleen struikelt Firefox hierover.
De add-on heeft pictogrammen nodig voor de verschillende add-on stores en overzichten (icons) en, indien gewenst, voor de toolbar in de browser en/of het snelmenu (browser_actions.default_icon). Volgens de aanbevelingen van de browsers heb je hiervoor vierkante pictogrammen van 16, 20, 24, 25, 30, 32 en 40 pixels nodig, voor de stores geldt een randlengte van 6, 32, 48, 96 en 128 pixels. In browser_action wordt ook de titel gedefinieerd en een default_popup in de vorm van een HTMLbestand, bijvoorbeeld:
<!doctype html> <title>DeeptransLate</title> <p>Hela hola, tekst.</p>
En daarmee is de opwarmronde klaar.
De tools voor het inrichten, beheer en debuggen zijn in Chrome, Opera en Vivaldi nagenoeg identiek. In chrome://extensions klik je op 'Uitgepakte extensie laden' en selecteer je manifest.json. Wanneer het manifest en de paden correct zijn, verschijnt het pictogram naast de adresbalk. Als je dit aanklikt, wordt het HTML-document in een tekstballon getoond.
Bij Firefox ga je naar about:addons, klik je op het tandwieltje en selecteer je 'Addons debuggen' in het menu. In de pagina die opent (about:debugging#addons) klik je op de knop 'Tijdelijke add-on laden'.
In Edge moet je eerst in about:flags de optie 'Functies voor ontwikkelaars van extensies inschakelen' selecteren. Klik vervolgens in het “...”-menu op 'Extensies' en selecteer 'Extensie laden'. Selecteer dan de map, niet het manifest. Net als in Firefox worden lokaal geïnstalleerde extensies alleen maar tijdelijk geladen en zijn ze na een herstart van de browser weer weg. Bovendien verbergt Edge standaard het pictogram. Deze standaardinstelling kun je in het tandwiel-menu wijzigen.
Vertaler
De regels voor default_popup kun je direct weer verwijderen. Om iets zinvols uit te voeren, heb je scripts en permissions nodig. Vul eerst het manifest aan met het volgende: { “background”:{
“scripts”:[“background.js”] }, “content_scripts”: [{ “matches”:[“<all_urls>”], “js”:[“content.js”], “css”:[“bubble.css”]
}],
“permissions”:[
“contextMenus”, “https://www.deepl.com/jsonrpc” ]
}
Er zijn achtergrond- en contentscripts. Terwijl die laatste binnen de context van de pagina draaien, blijft een achtergrondscript continu actief op de achtergrond en kan het daar reageren op gebeurtenissen zoals een muisklik op het uitvouwpictogram of een nieuw geopend tabblad. Het kan API's benaderen waarvan normale scripts alleen maar kunnen dromen: het snelmenu, bladwijzers, sneltoetsen, de adresbalk en nog veel meer. Het kan echter niet de inhoud van pagina's benaderen. Contentscripts kunnen zich tot bepaalde domeinen beperken, maar in dit geval is de speciale waarde <all_urls> geschikt. De scripts draaien via een voorinstelling op een bepaald moment nadat het DOM geladen is. Je kunt in content_scripts ook stylesheets inbouwen; met bubble.css bepaal je de vormgeving van de container waarin de vertaling later verschijnt.
In het manifestbestand stel je ook de permissions in die je later nodig hebt. Via contextMenus kun je een menu-item in het snelmenu reserveren. En de URL staat het achtergrondscript toe om via Ajax benaderingen uit te voeren – waarbij alleen Firefox eist dat er een permission is ingesteld. Maak dummy-bestanden aan voor background.js, content.js en bubble.css. Voor testdoeleinden is het handig om beide scripts elk een console.log()-verwijzing te geven. Als je in Firefox en Chromium op de optie 'Opnieuw laden' klikt, zouden deze programma's de uitbreiding nu correct moeten laden. Indien je tests wilt uitvoeren op pagina's die al open staan, moet je niet vergeten om deze ook eerst te herladen. Bij de ontwikkelaarstools van de pagina zie je nu de console output van de contentscripts. Let wel op, want uitbreidingen werken alleen op normale pagina's en niet op pagina's zoals about:blank, chrome:// extensions enzovoort.
Maar hoe zit het dan met het achtergrondscript? Om hier de output van te zien, moet je de add-ons debugger starten die in een eigen venster draait. Bij Chromium en Edge heet de link daarvoor Achtergrondpagina (in Chromium moet je daarvoor eerst de ontwikkelaarsmodus activeren). Terwijl de browser het contentscript voor elke geopende pagina opnieuw uitvoert, gebeurt er in de achtergrond niets meer na de eerste keer laden.
Edge vindt een fout in het manifest en beweert dat background een persistenteigenschap nodig heeft (in dit geval met de waarde true). Firefox heeft daar een andere mening over en stopt met deze regel ermee. Dit kun je alleen maar oplossen door de variant voor een van beide browsers in een eigen map te plaatsen en twee verschillende versies van manifest. json te maken. Aangezien voor de rest alles hetzelfde blijft, hoef je voor de andere bestanden niet twee verschillende codebases te onderhouden. Je kunt ze het beste via symbolische koppelingen op dezelfde plek
houden (mklink /H in de Windows-shell, ln in Linux/macOS/Cygwin).
Gesprekscultuur
Allereerst laten we het achtergrondscript een plek reserveren in het snelmenu en op het moment dat er geklikt wordt, zorgen dat er een bericht gestuurd wordt naar het contentscript:
‘use strict’; const sendToTab = msg => { chrome.tabs.query({active: true, currentWindow: true}, tabs => { chrome.tabs.
sendMessage(tabs[0].id, msg);
});
}; chrome.browserAction.onClicked. addListener(ev => sendToTab(‘buttonClicked’)); chrome.contextMenus.create({ title: ‘Vertaal met DeepL’, contexts:[‘all’], onclick:(info, tab) =>
sendToTab(‘menuClicked’)
});
De speciaal voor uitbreidingen gereserveerde API's draaien allemaal samen in een global object. Chrome heeft dit simpelweg chrome genoemd. Firefox opteert voor de naam browser, maar eist dit gelukkig niet, want de concurrentie zou dit niet snappen.
In de bovenstaande code wordt een sendToTab()-functie gedefinieerd die de gebruiker via acties activeert. chrome.browserAction.onClicked betreft een klik op het uitvouwpictogram, chrome.contextMenus. create() maakt een item in het snelmenu aan met een onclick-event-handler. contexts: ['all'] zorgt ervoor dat het snelmenu altijd verschijnt, dus ook wanneer er tekst geselecteerd is.
Het achtergrondscript heeft in eerste instantie geen idee welk tabblad er wordt bedoeld. Het snelmenu weet dit wel, maar omdat je het sowieso voor andere instances ook moet weten, kun je dit negeren. Om te achterhalen wat het actuele tabblad is, gaat de functie sendMessage() daarom met chrome.tabs.query() aan de slag. De bijbehorende callback-functie bevat een lijst met tabbladen die zijn aangetroffen, en als het goed is staat daar precies één entry in.
chrome.tabs heeft verschillende manieren om met tabbladen om te gaan. Een daarvan is sendMessage(), die naast het bericht zelf de tab-ID als adres nodig heeft. Een andere manier zou chrome.tabs. connect() kunnen zijn, waarmee een permanente verbinding wordt opgezet.
Om te zorgen dat het bericht goed aankomt, moet content.js eerst een postvak inrichten:
‘use strict’; chrome.runtime.onMessage.addListener ((msg, sender, respond) => {
console.log(msg);
// ...
});
Contentscripts lijken op JavaScript, maar verschillen op enkele punten, met name in de benadering van delen van chrome. runtime, een interface naar interne uitbreidingen. De callback-functie van onMessage. addListener() bevat naast de inhoud van het bericht ook informatie over de afzender en een callback-functie.
Indien het contentscript het bericht bevat dat de gebruiker op een van de knoppen heeft geklikt, dan moet het de geselecteerde tekst oppakken:
// ... if (msg === ‘menuClicked’ || msg === ‘buttonClicked’){ const sel = getSelection(); respond(sel.toString().trim());
}
getSelection() houdt het geselecteerde bereik vast en opent met toString() de tekst die zich in het bereik bevindt. Het voltooide script pakt met sel.getRangeAt(0). getBoundingClientRect() de positie van de selectie, om de ballon met de vertaling op de juiste plek te kunnen weergeven. We gaan hier verder niet in op alle kleine details van het DOM en CSS. Goed om te weten is dat de van onMessage.addListener() doorgegeven respond()-functie het pakketje terugstuurt en dat de beurt nu aan background.js is.
De sendMessage()-functie die daarin gebruikt wordt, kent namelijk een derde argument: een functie die in het geval van een antwoord moet worden aangeroepen. Deze geef je voor de twee event-handlers
ev => sendToTab(‘buttonClicked’,
getSelection)
aan sendToTab() door:
const sendToTab =(msg, onResponse = null) => { chrome.tabs.query({...}, tabs => { chrome.tabs. sendMessage(tabs[0].id, msg, onResponse);
});
};
De relevante getSelection()-functie hoeft in eerste instantie alleen maar in de console te schrijven:
const getSelection = msg =>
console.log(msg);
Als je nu na het vernieuwen tekst selecteert en de add-on start, dan zou deze tekst nu in de console van het achtergrondscript moeten verschijnen.
Aanvraag en antwoord
De volgende stap is de overdracht aan DeepL. De JSON-API van DeepL verwacht query's in een standaard formaat. Onze demo gebruikt een sjabloon dat als bestand is meegeleverd. Dit bestand moet in de getSelection()-functie worden ingelezen:
const getSelection = text => { const xhr = new XMLHttpRequest(); xhr.open(‘POST’,
‘https://www.deepl.com/jsonrpc’); xhr.setRequestHeader (‘Content-Type’, ‘application/json’); xhr.addEventListener(‘load’,
serviceResponse); fetch(‘deepLRequest.txt’)
.then(resp => resp.text()) .then(json => { xhr.send(json.replace(‘TEXT’,
`”${text}”`));
});
};
De aanvraag verstuur je met Ajax. De eerste regels definiëren een POST-aanvraag voor de DeepL-server. fetch() dient voor het oproepen van het eerder beschreven bestand. Daarbij draait het om een relatief nieuwe standaardfunctie die met promises werkt. Na het inlezen extraheer je eerst de tekstinhoud (text()) die je vervolgens verwerkt. Met replace() integreer je een zoekstring. Een handige manier om de vereiste aanhalingstekens in te bouwen is met de template-string, die zich door backticks kenmerkt. Na het verzenden (xhr.send()) wacht de load-listener op een antwoord. De bijbehorende functie serviceResponse() maak je in eerste instantie als dummy aan:
const serviceResponse = ev => {
console.log(ev);
};
In de background-console zou je nu een eventobject moeten zien, waarin de JSONcode met de vertaling staat. De twee scripts wisselen nog een keer gegevens onderling uit: serviceResponse() stuurt het resultaat van de query naar het contentscript.
const serviceResponse = ev => { let resp = JSON.parse(ev.
target.response); sendToTab(resp);
}
sendToTab() bevat deze keer geen ant- woordfunctie als tweede argument. Het contentscript neemt de weergave op de pagina over – in zijn meest eenvoudige vorm:
chrome.runtime.onMessage.addListener ((msg, sender, respond) => { if (msg === ‘menuClicked’ || msg === ‘buttonClicked’){
// ... } else { const output = []; msg.result.translations.
forEach(tls => { tls.beams.forEach(tl => output.push(tl. postprocessed_sentence)); }); alert(output.join(“\n”)); }
});
Het antwoord bevat een array met translations (meestal met maar een entry) waarin arrays zitten met de daadwerkelijke vertalingen (beams). Om het zo eenvoudig mogelijk te houden, extraheert het script alle beams en plaatst ze in een alert. Het volledige script bewerkt het resultaat nog iets zodat het er een beetje beter uitziet.
Verpakken & verzenden
Chromiumbrowsers kunnen via een interface op de uitbreidingspagina zelf pakketten integreren die gedownload kunnen worden. Maar vermoedelijk heb je niet zoveel aan deze .crx- (Chrome, Vivaldi) en .nex-bestanden, omdat je de add-on in een store wilt aanbieden. Voor het plaatsen in de Chrome Web Store (https://chrome. google.com/web store/developer/dashboard) en in Opera Add-ons (https://addons.opera.com/developer/) heb je een eenvoudige zipfile en een developer-account bij Google c.q. Opera nodig.
Voor het publiceren van een Firefoxextensie, plaats je de code in een zipfile die je op https://addons.mozilla.org/nl/developers/ uploadt. Dat moet je altijd doen, ook al ben je niet van plan om de add-on via de Firefox extensiesite aan te bieden. Mozilla test het bestand namelijk eerst en stuurt na goedkeuring een gesigneerd .xpi-bestand terug, en alleen deze kun je in Firefox installeren.
Bij Edge kun je aanvragen om add-ons van andere stores te porteren. Een mogelijkheid om zelf te uploaden is er blijkbaar nog niet. De URL's van de add-onstores en verdere details voor het uploadproces van andere browsers vind je via de link op het einde van dit artikel.
Het is verrassend eenvoudig om browsers van een extensie te voorzien. De verschillen zijn niet bijzonder groot en kunnen met enkele regels code worden aangepast – mits je correct identificeert waar de verschillen zitten. In het begin moet je genoeg tijd inplannen, want vooral in het begin moet je enkele ongebruikelijke concepten en API's doorgronden. (nkr)