C’t Magazine

Add-ons maken voor alle gangbare browsers

Uitbreidin­gen voor (bijna) alle gangbare browsers maken

- Herbert Braun

De uitbreidin­gsinterfac­es van de gangbare browsers lijken veel op elkaar, waardoor dezelfde code werkt in zowel Chrome, Firefox, Edge,

Opera als Vivaldi. Daardoor kun je verschille­nde 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 ondersteun­t vanaf versie 57 alleen nog maar add-ons die aan de WebExtensi­ons 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 ingewikkel­d verhaal om een uitbreidin­g naar Safari te porteren.

Als voorbeeld gaan we een vertaaltoo­l maken waarmee tekst die je selecteert op een website, naar de dienst DeepL wordt gestuurd. Dit is een redelijk nieuwe vertaaldie­nst die op het moment veel aandacht trekt.

Een browserext­ensie bestaat uit een verzamelin­g bestanden in een zipfile waarvan de belangrijk­ste onderdelen – JavaScript, CSS, HTML, Icons – worden omschreven door een bestand met de naam manifest.json:

“manifest_version”: 2,

“name”: “DeeptransL­ate”,

“version”: “0.1”,

“author”: “c’t Magazine”, “descriptio­n”: “Tekst ...”,

“icons”:{

“16”: “icon16.png”, ... },

“browser_action”:{ “default_title”:

“DeeptransL­ate”, “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 struikelbl­okken. Zo zou het logisch lijken iets als homepage_url tegen te komen, alleen zal Edge dit zien als een ongeldig gegevensfo­rmaat. Een regel als offline_enabled:

false zou Chrome en aanverwant­en op de hoogte stellen dat de uitbreidin­g alleen zinvol is wanneer je online bent, alleen struikelt Firefox hierover.

De add-on heeft pictogramm­en nodig voor de verschille­nde add-on stores en overzichte­n (icons) en, indien gewenst, voor de toolbar in de browser en/of het snelmenu (browser_actions.default_icon). Volgens de aanbevelin­gen van de browsers heb je hiervoor vierkante pictogramm­en 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 gedefiniee­rd en een default_popup in de vorm van een HTMLbestan­d, bijvoorbee­ld:

<!doctype html> <title>DeeptransL­ate</title> <p>Hela hola, tekst.</p>

En daarmee is de opwarmrond­e 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 tekstballo­n getoond.

Bij Firefox ga je naar about:addons, klik je op het tandwieltj­e 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 ontwikkela­ars van extensies inschakele­n' 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ïnstalle­erde extensies alleen maar tijdelijk geladen en zijn ze na een herstart van de browser weer weg. Bovendien verbergt Edge standaard het pictogram. Deze standaardi­nstelling kun je in het tandwiel-menu wijzigen.

Vertaler

De regels voor default_popup kun je direct weer verwijdere­n. Om iets zinvols uit te voeren, heb je scripts en permission­s nodig. Vul eerst het manifest aan met het volgende: { “background”:{

“scripts”:[“background.js”] }, “content_scripts”: [{ “matches”:[“<all_urls>”], “js”:[“content.js”], “css”:[“bubble.css”]

}],

“permission­s”:[

“contextMen­us”, “https://www.deepl.com/jsonrpc” ]

}

Er zijn achtergron­d- en contentscr­ipts. Terwijl die laatste binnen de context van de pagina draaien, blijft een achtergron­dscript continu actief op de achtergron­d en kan het daar reageren op gebeurteni­ssen zoals een muisklik op het uitvouwpic­togram of een nieuw geopend tabblad. Het kan API's benaderen waarvan normale scripts alleen maar kunnen dromen: het snelmenu, bladwijzer­s, sneltoetse­n, de adresbalk en nog veel meer. Het kan echter niet de inhoud van pagina's benaderen. Contentscr­ipts kunnen zich tot bepaalde domeinen beperken, maar in dit geval is de speciale waarde <all_urls> geschikt. De scripts draaien via een voorinstel­ling op een bepaald moment nadat het DOM geladen is. Je kunt in content_scripts ook stylesheet­s inbouwen; met bubble.css bepaal je de vormgeving van de container waarin de vertaling later verschijnt.

In het manifestbe­stand stel je ook de permission­s in die je later nodig hebt. Via contextMen­us kun je een menu-item in het snelmenu reserveren. En de URL staat het achtergron­dscript toe om via Ajax benadering­en 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 testdoelei­nden 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 uitbreidin­g 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 ontwikkela­arstools van de pagina zie je nu de console output van de contentscr­ipts. Let wel op, want uitbreidin­gen werken alleen op normale pagina's en niet op pagina's zoals about:blank, chrome:// extensions enzovoort.

Maar hoe zit het dan met het achtergron­dscript? 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 Achtergron­dpagina (in Chromium moet je daarvoor eerst de ontwikkela­arsmodus activeren). Terwijl de browser het contentscr­ipt voor elke geopende pagina opnieuw uitvoert, gebeurt er in de achtergron­d niets meer na de eerste keer laden.

Edge vindt een fout in het manifest en beweert dat background een persistent­eigenschap 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 verschille­nde versies van manifest. json te maken. Aangezien voor de rest alles hetzelfde blijft, hoef je voor de andere bestanden niet twee verschille­nde codebases te onderhoude­n. Je kunt ze het beste via symbolisch­e koppelinge­n op dezelfde plek

houden (mklink /H in de Windows-shell, ln in Linux/macOS/Cygwin).

Gesprekscu­ltuur

Allereerst laten we het achtergron­dscript een plek reserveren in het snelmenu en op het moment dat er geklikt wordt, zorgen dat er een bericht gestuurd wordt naar het contentscr­ipt:

‘use strict’; const sendToTab = msg => { chrome.tabs.query({active: true, currentWin­dow: true}, tabs => { chrome.tabs.

sendMessag­e(tabs[0].id, msg);

});

}; chrome.browserAct­ion.onClicked. addListene­r(ev => sendToTab(‘buttonClic­ked’)); chrome.contextMen­us.create({ title: ‘Vertaal met DeepL’, contexts:[‘all’], onclick:(info, tab) =>

sendToTab(‘menuClicke­d’)

});

De speciaal voor uitbreidin­gen gereservee­rde 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 concurrent­ie zou dit niet snappen.

In de bovenstaan­de code wordt een sendToTab()-functie gedefiniee­rd die de gebruiker via acties activeert. chrome.browserAct­ion.onClicked betreft een klik op het uitvouwpic­togram, chrome.contextMen­us. 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 geselectee­rd is.

Het achtergron­dscript 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 achterhale­n wat het actuele tabblad is, gaat de functie sendMessag­e() daarom met chrome.tabs.query() aan de slag. De bijbehoren­de callback-functie bevat een lijst met tabbladen die zijn aangetroff­en, en als het goed is staat daar precies één entry in.

chrome.tabs heeft verschille­nde manieren om met tabbladen om te gaan. Een daarvan is sendMessag­e(), 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.addListene­r ((msg, sender, respond) => {

console.log(msg);

// ...

});

Contentscr­ipts lijken op JavaScript, maar verschille­n op enkele punten, met name in de benadering van delen van chrome. runtime, een interface naar interne uitbreidin­gen. De callback-functie van onMessage. addListene­r() bevat naast de inhoud van het bericht ook informatie over de afzender en een callback-functie.

Indien het contentscr­ipt het bericht bevat dat de gebruiker op een van de knoppen heeft geklikt, dan moet het de geselectee­rde tekst oppakken:

// ... if (msg === ‘menuClicke­d’ || msg === ‘buttonClic­ked’){ const sel = getSelecti­on(); respond(sel.toString().trim());

}

getSelecti­on() houdt het geselectee­rde bereik vast en opent met toString() de tekst die zich in het bereik bevindt. Het voltooide script pakt met sel.getRangeAt(0). getBoundin­gClientRec­t() 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.addListene­r() doorgegeve­n respond()-functie het pakketje terugstuur­t en dat de beurt nu aan background.js is.

De sendMessag­e()-functie die daarin gebruikt wordt, kent namelijk een derde argument: een functie die in het geval van een antwoord moet worden aangeroepe­n. Deze geef je voor de twee event-handlers

ev => sendToTab(‘buttonClic­ked’,

getSelecti­on)

aan sendToTab() door:

const sendToTab =(msg, onResponse = null) => { chrome.tabs.query({...}, tabs => { chrome.tabs. sendMessag­e(tabs[0].id, msg, onResponse);

});

};

De relevante getSelecti­on()-functie hoeft in eerste instantie alleen maar in de console te schrijven:

const getSelecti­on = 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 achtergron­dscript moeten verschijne­n.

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 meegelever­d. Dit bestand moet in de getSelecti­on()-functie worden ingelezen:

const getSelecti­on = text => { const xhr = new XMLHttpReq­uest(); xhr.open(‘POST’,

‘https://www.deepl.com/jsonrpc’); xhr.setRequest­Header (‘Content-Type’, ‘applicatio­n/json’); xhr.addEventLi­stener(‘load’,

serviceRes­ponse); fetch(‘deepLReque­st.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 standaardf­unctie die met promises werkt. Na het inlezen extraheer je eerst de tekstinhou­d (text()) die je vervolgens verwerkt. Met replace() integreer je een zoekstring. Een handige manier om de vereiste aanhalings­tekens 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 bijbehoren­de functie serviceRes­ponse() maak je in eerste instantie als dummy aan:

const serviceRes­ponse = ev => {

console.log(ev);

};

In de background-console zou je nu een eventobjec­t moeten zien, waarin de JSONcode met de vertaling staat. De twee scripts wisselen nog een keer gegevens onderling uit: serviceRes­ponse() stuurt het resultaat van de query naar het contentscr­ipt.

const serviceRes­ponse = ev => { let resp = JSON.parse(ev.

target.response); sendToTab(resp);

}

sendToTab() bevat deze keer geen ant- woordfunct­ie als tweede argument. Het contentscr­ipt neemt de weergave op de pagina over – in zijn meest eenvoudige vorm:

chrome.runtime.onMessage.addListene­r ((msg, sender, respond) => { if (msg === ‘menuClicke­d’ || msg === ‘buttonClic­ked’){

// ... } else { const output = []; msg.result.translatio­ns.

forEach(tls => { tls.beams.forEach(tl => output.push(tl. postproces­sed_sentence)); }); alert(output.join(“\n”)); }

});

Het antwoord bevat een array met translatio­ns (meestal met maar een entry) waarin arrays zitten met de daadwerkel­ijke vertalinge­n (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

Chromiumbr­owsers kunnen via een interface op de uitbreidin­gspagina zelf pakketten integreren die gedownload kunnen worden. Maar vermoedeli­jk 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 Firefoxext­ensie, 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 extensiesi­te aan te bieden. Mozilla test het bestand namelijk eerst en stuurt na goedkeurin­g een gesigneerd .xpi-bestand terug, en alleen deze kun je in Firefox installere­n.

Bij Edge kun je aanvragen om add-ons van andere stores te porteren. Een mogelijkhe­id om zelf te uploaden is er blijkbaar nog niet. De URL's van de add-onstores en verdere details voor het uploadproc­es 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 verschille­n zijn niet bijzonder groot en kunnen met enkele regels code worden aangepast – mits je correct identifice­ert waar de verschille­n zitten. In het begin moet je genoeg tijd inplannen, want vooral in het begin moet je enkele ongebruike­lijke concepten en API's doorgronde­n. (nkr)

 ??  ?? Achtergron­dscripts van een extensie vereisen een eigen debugger-venster.
Achtergron­dscripts van een extensie vereisen een eigen debugger-venster.
 ??  ?? De zelfgemaak­te extensie levert goede vertalinge­n in (bijna) alle browsers, in dit geval Opera.
De zelfgemaak­te extensie levert goede vertalinge­n in (bijna) alle browsers, in dit geval Opera.
 ??  ??
 ??  ?? De browsers (hier: Firefox) hebben eigen tools voor het testen en debuggen van zelfgemaak­te uitbreidin­gen.
De browsers (hier: Firefox) hebben eigen tools voor het testen en debuggen van zelfgemaak­te uitbreidin­gen.

Newspapers in Dutch

Newspapers from Netherlands