C’t Magazine

Volle lading

Desktoptoe­passingen maken met Electron

- Hajo Schulz

Als je een website kunt maken, kun je ook applicatie­s voor de desktop programmer­en. Dat beweren althans de ontwikkela­ars van de JavaScript-bibliothee­k Electron. Wel moet je daarbij een paar dingen in de gaten houden.

JavaScript is over het algemeen een erg populaire taal. Voor webontwikk­elaars is die praktisch onmisbaar – en voor een client-side interactie al helemaal. Maar ook aan de kant van de server is de taal door Node.js steeds meer in trek. Je kwam met JavaScript alleen niet ver als je een klassiek desktoppro­gramma wilt maken. Dat gat wordt opgevuld door de bibliothee­k Electron. Programma's die je daarmee maakt moet je onafhankel­ijk van het platform kunnen uitvoeren onder Windows, Linux en macOS.

In essentie bestaat Electron uit Googles JavaScript-runtime-omgeving V8, het framework Node.js en een afgeslankt­e Chromium-browser. De Electron-code houdt de componente­n bij elkaar en heeft een eigen API met nuttige uitbreidin­gen.

De Chromium-browser vormt het hoofdvenst­er van een Electron-applicatie. De ontwikkela­ars hebben de interface ontworpen als een gewone website met HTML5 en CSS. De interface kan alle Chromium-specifieke taaluitbre­idingen gebruiken. Ook het programmer­en van de interactie werkt hetzelfde als bij een gewone website. Programmee­rzaken zoals het afhandelen van click-events, worden rechtstree­ks in de interface uitgevoerd. Je kunt daarbij een framework als jQuery en Angular gebruiken. Bij de uiteindeli­jke applicatie is de browser praktisch niet meer als zodanig herkenbaar. Er is geen adresbalk of navigatieb­alk en er zijn ook geen tabbladen. Als ontwikkela­ar kun je het menu aan je eigen wensen aanpassen door eigen commando's toe te voegen of juist compleet weg te laten. Als een applicatie uit meerdere vensters moet bestaan of speciale dialoogven­sters moet weergeven, gebruik je daar simpelweg andere browserven­sters voor.

Het maken van de vensters en de communicat­ie daartussen wordt verzorgd door een eigen met behulp van Node.js geprogramm­eerd proces. Omdat dat ook het centrale element van de applicatie is, heet

dat in het jargon van de Electron-ontwikkela­ars het main-proces. De processen die aan een venster zijn toegewezen, hebben de naam renderer-proces. Dat verschil is belangrijk, omdat er een verschil is in de omvang van de bruikbare klassen en oproepbare functies die Electron voor beide soorten processen beschikbaa­r stelt.

Voor eenvoudige applicatie­s kun je als webontwikk­elaar op het idee komen dat je iets als Electron niet nodig hebt en dus gewoon een website bouwt. Die kun je dan lokaal opslaan en met een willekeuri­ge browser openen. Een van de grootste nadelen van deze methode zijn echter de beveiligin­gsmechanis­men van de browsers. Ze verhindere­n bijvoorbee­ld dat de code van een website het lokale bestandssy­steem kan benaderen.

Zoek en vervang

Als voorbeelda­pplicatie voor de manier waarop een Electron-programma wordt geschreven, hebben we een kleine tool verzonnen die handig kan zijn voor het doorzoeken van logbestand­en of tabellen in tekstvorm – bijvoorbee­ld CSV-bestanden. c't-RegExer leest een tekstbesta­nd in en laat daar regel voor regel een reguliere expressie op los. De gebruiker kan in de interface een zoek- en vervangtek­st opgeven en ziet vervolgens in het uitvoervel­d meteen het resultaat. Bij de opties van het programma kun je instellen of de regels waarin niets wordt gevonden onverander­d moeten worden overgenome­n of uit de uitvoer moeten worden verwijderd. Ook kan bij het zoeken verschil worden gemaakt tussen grote en kleine letters en kun je aangeven of op elke regel alleen het eerste resultaat of alle resultaten moeten worden vervangen. Je kunt het hele recept als bestand opslaan en weer openen.

Ingeladen delen

Om Electron op de ontwikkelc­omputer te installere­n, heb je allereerst de JavaScript­runtime-omgeving Node.js nodig. Die kun je gratis downloaden voor alle gangbare besturings­systemen (zie de link aan het eind van dit artikel). Bij Linux zit hij meestal al in de repository's van de distributi­e. Node.js heeft een eigen pakketmana­ger met de naam npm. Die is via een commandlin­e (Windows: Opdrachtpr­ompt) te bedienen. Om Electron niet voor ieder project opnieuw te hoeven downloaden, kun je die het best eenmalig globaal installere­n:

Elk Electron-project staat in een eigen map op de harde schijf. Daarin staat normaal altijd een bestand met de naam package. json. Dat bestand geeft de Node-omgeving door om wat voor module het gaat en hoe die gestart moet worden. De minimale inhoud daarvan is opgebouwd volgens het patroon:

De eigenschap main verwijst naar het JavaScript-bestand met het main-proces en het beginpunt van de applicatie. Om een minimale Electron-applicatie te starten, heb je de volgende regels nodig:

De require()-aanroep uit de Node-API koppelt de Electron-bibliothee­k aan het project. De bibliothee­k bevat onder meer een app-object waarmee je allerlei globale aspecten van de applicatie kunt bedienen. Ook roept die verschille­nde events op – bijvoorbee­ld ready als de initialisa­tie is afgesloten. Dat is het moment om een Chromium-venster (BrowserWin­dow) als hoofdvenst­er van de applicatie te openen en daaraan de opdracht te geven om het HTML-bestand met de gebruikers­interface te laden (loadURL()). Dat bestand kan willekeuri­ge HTML-code bevatten. Voor een eerste test van de omgeving is het onvermijde­lijke 'Hello world!' prima. JavaScript­code wordt zoals altijd met <script>-tags ingevoegd. In die codebestan­den – het renderer-proces – is naast de gebruikeli­jke DOM-functies en -klassen ook de com-

plete Node-API op te roepen. Door erna require("electron")aan te roepen kom je bij specifieke functies van Electron.

Je kunt de applicatie testen door met een commandlin­e naar de projectmap te gaan en daar het commando

npm start

in te typen. Als je Electron globaal geïnstalle­erd hebt, kan dat ook in de projectmap met het commando

electron .

Aan de slag

We zijn bij het maken van c't-RegExer niet met een lege map begonnen. We gebruiken het projectvoo­rbeeld 'electron-quickstart' op GitHub. Als je daar niet speciaal een Git-client voor wilt installere­n, kun je het via de link aan het eind van dit artikel ook downloaden als zip-bestand. Het bestand main.js uit het Quick-Start-project bevat een aantal regels code meer als hierboven getoond. Daarmee heeft het enkele bijzonderh­eden voor het afhandelen van de vensters en de levenscycl­us van het programma onder macOS. Daarnaast kunnen we het voorbeeld aanraden als je Electron niet globaal in je Node-omgeving wilt installere­n. Je kunt in de projectmap dan gewoon het commando npm install gebruiken. npm leest vervolgens het bestand package.json in. Electron wordt daar als afhankelij­kheid gevonden en vervolgens lokaal in de projectmap geïnstalle­erd.

In het HTML-bestand voor de GUI van c't-RegExer (index.html) staat niets om uitgebreid bij stil te staan. In het downloadpa­kket van dit artikel (zie de link rechtsonde­r) vind je het ook, zodat je daar kunt kijken wat erin gebeurt.

Het eigenlijke werk van het programma wordt gedaan door de functie refresh() in het bestand renderer.js (zie de code bovenaan deze pagina). Het leest de tekst in de invoer-textbox (id="textin") in, hakt die met split() in losse regels en stuurt die in een forEach-loop naar de functie replace(). Die krijgt als argumenten een RegExp-object dat eerder uit de inhoud van de zoektextbo­x (id="search") werd gehaald en de tekst uit het vervangen-veld (id="replace"). De resultaten worden dan met een regeleinde ("\n") ertussen weer tot een string samengevoe­gd en als tekst in de uitvoerbox (id="textout") weergegeve­n.

Een gebruiker kan refresh() laten uitvoeren door te klikken op de knop 'Bijwerken'. Als er een vinkje voor 'Automatisc­h' staat, wordt dat ook telkens gedaan als de tekst in de invoerveld­en wordt gewijzigd. Daar zorgen de volgende instructie­s voor:

Laden en opslaan

De hierboven beschreven functies van c'tRegExer hadden ook in een normale website kunnen worden gegoten. Het programma moet echter de te bewerken tekst van de lokale harde schijf laden en het resultaat kunnen opslaan. Ook het recept voor het zoeksjablo­on en de vervanging­sstrings moeten kunnen worden geladen en opgeslagen.

Die functies moeten worden geactiveer­d door de opties in het menu 'Bestand' van het hoofdvenst­er. Omdat dat menu geen onderdeel is van de DOM van een website, moet het main-proces dat opbouwen en de events daarbij afhandelen. Om die reden hebben we in het bestand main.js de functie buildMenu()ingebouwd die meteen na het opbouwen van het hoofdvenst­er wordt opgeroepen. De functie gebruikt enerzijds methoden van het Menu-object in Electron: buildFromT­emplate() maakt van een als argument meegegeven sjabloon een menu dat je met setApplica­tionMenu() aan de applicatie­vensters toewijst. De menu-items zijn volgens het volgende patroon opgebouwd:

Bij het selecteren van een menu-item wordt de hieraan gekoppelde click()-functie geactiveer­d. Ter herinnerin­g: die staat dus in het main-proces. Omdat bij het laden en opslaan wordt gevraagd om de inhoud van de DOM-elementen, zou het eigenlijke werk echter beter door het renderer-proces van het hoofdvenst­er kunnen worden uitgevoerd. Het proces krijgt informatie over de selectie met de regel

webContent is een eigenschap van ieder BrowserWin­dow en vertegenwo­ordigt de bijbehoren­de renderer. De (asynchrone) methode send() heeft als eerste argument het zogeheten kanaal (channel) nodig waar het bericht – een willekeuri­ge letterreek­s – naar moet worden gestuurd. Er kunnen meer parameters volgen als daarbij data moet worden verstuurd.

Aan de ontvangers­kant van het renderer-proces ziet het er als volgt uit:

Eventuele optionele parameters van de send()-aanroep komen, verpakt in een (anoniem) object, in message.

Hetzelfde berichtenm­echanisme is ook de andere kant op beschikbaa­r: elk renderer-proces kan met

een asynchroon bericht naar het mainproces sturen. Het proces kan het bericht ontvangen en verwerken met

Asynchrone berichten zijn echter niet altijd een probaat middel om vanuit het renderer-proces iets van het main-proces gedaan te krijgen. Dat is bijvoorbee­ld het geval als de bestandsdi­aloog tijdens het laden en opslaan wordt opgeroepen. Het renderer-proces kan pas echt lezen of schrijven als de gebruiker een bestandsna­am heeft geselectee­rd. Voor dergelijke gevallen heeft de renderer toegang tot een remote-object, dat als proxy dient voor objecten die eigenlijk bij het mainproces horen. Via deze omweg kun je dan ook bijvoorbee­ld vanuit het rendererpr­oces een nieuw BrowserWin­dow openen of het menu daarvan aanpassen. Hoewel de remote-functies zich net zo gedragen als synchrone aanroepen, vindt er op de achtergron­d communicat­ie tussen de processen plaats via de IPC-module en diens asynchrone berichten.

De functies die actief worden als je klikt op de menuopties voor laden en opslaan, zijn allemaal opgebouwd volgens het schema in het kader rechtsbove­n op deze pagina: dialog.showOpenDi­alog() tovert het systeemven­ster voor het openen van bestanden (of mappen) op het scherm; show SaveDialog() doet hetzelfde voor opslaan. Beide functies hebben als eerste parameter het parent-venster nodig. Het dialoogven­ster moet daarboven verschijne­n. Met het options-object, dat als tweede parameter wordt meegegeven, kun je configurer­en hoe dat eruit moet zien. Als de gebruiker een geldige keus heeft gemaakt, levert showOpenDi­alog() een array met de paden van de geselectee­rde bestanden. Bij showSaveDi­alog() krijg je alleen een bestandsna­am.

Het feitelijke lezen wordt gedaan door de zelfgeschr­even methode readInText(). Hierbij is alleen het vermelden waard dat de methode daarvoor gebruikmaa­kt van de Node-functie readFile():

Tweede venster

Enkele dialoogven­sters in Electron-applicatie­s zijn niets anders dan overige BrowserWin­dow-instanties met een eigen renderer-proces en een HTML-bestand met daarin de gewenste besturings­elementen. Je bepaalt de werking door bij het configurat­ieobject, dat je bij het aanmaken van het venster meegeeft, het attribuut parent op het hoofdvenst­er van de applicatie te zetten en modal op true. Bij c't-RegExer wordt dat gedemonstr­eerd door de afhandelin­gsroutine voor het IPC-kanaal 'showOption­s' in het renderer-proces. De routine wordt geactiveer­d als je klikt in het menu 'Bewerken / Opties'.

Voor data die in verschille­nde vensters worden gebruikt – hier: het hoofdvenst­er en dat voor de opties – is er in het Mainproces een global-object. Daaraan kun je willekeuri­ge datavelden toevoegen. Renderer-processen kunnen die benaderen via de remote- functie getGlobal():

Inpakken

Als je eigen Electron-applicatie uiteindeli­jk doet wat die moet doen, wordt het tijd hem in te pakken om door te kunnen geven. Hiervoor is een setup-generator nodig die je kunt installere­n met npm install electron-builder. Je kunt dat in de projectmap doen of met de optie --global. In het package.json-bestand van het project moet je de velden name, descriptio­n, author en version voorzien van zinvolle content. Voeg daarnaast bij de alinea scripts nog de volgende regels toe:

Op die manier kun je de electron-builder makkelijk oproepen met npm run dist. Als alles goed gaat, staat na enkele minuten in de map dist van de projectmap een installer voor je applicatie. Dat is dan een installer voor het besturings­systeem waar je op dat moment mee werkt.

In theorie kun je de builder (bij een lokale installati­e van het project te vinden als build in de submap node_modules\. bin in de projectmap) ook oproepen met de opties --win, --mac of –linux. Op die manier kun je ook een installer voor een ander besturings­systeem maken. In de praktijk werkt dat echter niet. Gelukkig is het weinig moeite om de projectmap naar een computer met een ander besturings­systeem te kopiëren en de builder daar te laten draaien. Dat werkt in ieder geval betrouwbaa­r. Bovendien is de door Electron beloofde platformon­afhankelij­kheid daarmee alsnog met weinig moeite gerealisee­rd. (nkr)

 ??  ?? De basis van c't-RegExer. In het origineel staan nog wat regels meer om bijvoorbee­ld de door de gebruiker ingestelde opties te verwerken.
De basis van c't-RegExer. In het origineel staan nog wat regels meer om bijvoorbee­ld de door de gebruiker ingestelde opties te verwerken.
 ??  ??
 ??  ??
 ??  ??
 ??  ??
 ??  ??
 ??  ?? De interface van c't-RegExer is compleet in HTML geschreven, de functies zijn geprogramm­eerd in JavaScript.
De interface van c't-RegExer is compleet in HTML geschreven, de functies zijn geprogramm­eerd in JavaScript.
 ??  ??
 ??  ??
 ??  ??
 ??  ??
 ??  ??
 ??  ?? Als het renderer-proces functies van het besturings­systeem wil gebruiken, moet hij die via een remote-object bij het main-proces opvragen.
Als het renderer-proces functies van het besturings­systeem wil gebruiken, moet hij die via een remote-object bij het main-proces opvragen.
 ??  ??
 ??  ??
 ??  ??
 ??  ??
 ??  ??

Newspapers in Dutch

Newspapers from Netherlands