Volle lading
Desktoptoepassingen maken met Electron
Als je een website kunt maken, kun je ook applicaties voor de desktop programmeren. Dat beweren althans de ontwikkelaars van de JavaScript-bibliotheek Electron. Wel moet je daarbij een paar dingen in de gaten houden.
JavaScript is over het algemeen een erg populaire taal. Voor webontwikkelaars 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 desktopprogramma wilt maken. Dat gat wordt opgevuld door de bibliotheek Electron. Programma's die je daarmee maakt moet je onafhankelijk 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 afgeslankte Chromium-browser. De Electron-code houdt de componenten bij elkaar en heeft een eigen API met nuttige uitbreidingen.
De Chromium-browser vormt het hoofdvenster van een Electron-applicatie. De ontwikkelaars hebben de interface ontworpen als een gewone website met HTML5 en CSS. De interface kan alle Chromium-specifieke taaluitbreidingen gebruiken. Ook het programmeren van de interactie werkt hetzelfde als bij een gewone website. Programmeerzaken zoals het afhandelen van click-events, worden rechtstreeks in de interface uitgevoerd. Je kunt daarbij een framework als jQuery en Angular gebruiken. Bij de uiteindelijke applicatie is de browser praktisch niet meer als zodanig herkenbaar. Er is geen adresbalk of navigatiebalk en er zijn ook geen tabbladen. Als ontwikkelaar 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 dialoogvensters moet weergeven, gebruik je daar simpelweg andere browservensters voor.
Het maken van de vensters en de communicatie daartussen wordt verzorgd door een eigen met behulp van Node.js geprogrammeerd proces. Omdat dat ook het centrale element van de applicatie is, heet
dat in het jargon van de Electron-ontwikkelaars 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 beschikbaar stelt.
Voor eenvoudige applicaties kun je als webontwikkelaar 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 willekeurige browser openen. Een van de grootste nadelen van deze methode zijn echter de beveiligingsmechanismen van de browsers. Ze verhinderen bijvoorbeeld dat de code van een website het lokale bestandssysteem kan benaderen.
Zoek en vervang
Als voorbeeldapplicatie voor de manier waarop een Electron-programma wordt geschreven, hebben we een kleine tool verzonnen die handig kan zijn voor het doorzoeken van logbestanden of tabellen in tekstvorm – bijvoorbeeld CSV-bestanden. c't-RegExer leest een tekstbestand in en laat daar regel voor regel een reguliere expressie op los. De gebruiker kan in de interface een zoek- en vervangtekst opgeven en ziet vervolgens in het uitvoerveld meteen het resultaat. Bij de opties van het programma kun je instellen of de regels waarin niets wordt gevonden onveranderd moeten worden overgenomen 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 ontwikkelcomputer te installeren, heb je allereerst de JavaScriptruntime-omgeving Node.js nodig. Die kun je gratis downloaden voor alle gangbare besturingssystemen (zie de link aan het eind van dit artikel). Bij Linux zit hij meestal al in de repository's van de distributie. Node.js heeft een eigen pakketmanager met de naam npm. Die is via een commandline (Windows: Opdrachtprompt) te bedienen. Om Electron niet voor ieder project opnieuw te hoeven downloaden, kun je die het best eenmalig globaal installeren:
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-bibliotheek aan het project. De bibliotheek bevat onder meer een app-object waarmee je allerlei globale aspecten van de applicatie kunt bedienen. Ook roept die verschillende events op – bijvoorbeeld ready als de initialisatie is afgesloten. Dat is het moment om een Chromium-venster (BrowserWindow) als hoofdvenster van de applicatie te openen en daaraan de opdracht te geven om het HTML-bestand met de gebruikersinterface te laden (loadURL()). Dat bestand kan willekeurige HTML-code bevatten. Voor een eerste test van de omgeving is het onvermijdelijke 'Hello world!' prima. JavaScriptcode wordt zoals altijd met <script>-tags ingevoegd. In die codebestanden – het renderer-proces – is naast de gebruikelijke 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 commandline naar de projectmap te gaan en daar het commando
npm start
in te typen. Als je Electron globaal geïnstalleerd 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 projectvoorbeeld 'electron-quickstart' op GitHub. Als je daar niet speciaal een Git-client voor wilt installeren, 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 bijzonderheden voor het afhandelen van de vensters en de levenscyclus van het programma onder macOS. Daarnaast kunnen we het voorbeeld aanraden als je Electron niet globaal in je Node-omgeving wilt installeren. Je kunt in de projectmap dan gewoon het commando npm install gebruiken. npm leest vervolgens het bestand package.json in. Electron wordt daar als afhankelijkheid gevonden en vervolgens lokaal in de projectmap geïnstalleerd.
In het HTML-bestand voor de GUI van c't-RegExer (index.html) staat niets om uitgebreid bij stil te staan. In het downloadpakket van dit artikel (zie de link rechtsonder) 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 zoektextbox (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 samengevoegd en als tekst in de uitvoerbox (id="textout") weergegeven.
Een gebruiker kan refresh() laten uitvoeren door te klikken op de knop 'Bijwerken'. Als er een vinkje voor 'Automatisch' staat, wordt dat ook telkens gedaan als de tekst in de invoervelden wordt gewijzigd. Daar zorgen de volgende instructies 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 zoeksjabloon en de vervangingsstrings moeten kunnen worden geladen en opgeslagen.
Die functies moeten worden geactiveerd door de opties in het menu 'Bestand' van het hoofdvenster. 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 hoofdvenster wordt opgeroepen. De functie gebruikt enerzijds methoden van het Menu-object in Electron: buildFromTemplate() maakt van een als argument meegegeven sjabloon een menu dat je met setApplicationMenu() aan de applicatievensters toewijst. De menu-items zijn volgens het volgende patroon opgebouwd:
Bij het selecteren van een menu-item wordt de hieraan gekoppelde click()-functie geactiveerd. Ter herinnering: 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 hoofdvenster kunnen worden uitgevoerd. Het proces krijgt informatie over de selectie met de regel
webContent is een eigenschap van ieder BrowserWindow en vertegenwoordigt de bijbehorende renderer. De (asynchrone) methode send() heeft als eerste argument het zogeheten kanaal (channel) nodig waar het bericht – een willekeurige letterreeks – naar moet worden gestuurd. Er kunnen meer parameters volgen als daarbij data moet worden verstuurd.
Aan de ontvangerskant 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 berichtenmechanisme is ook de andere kant op beschikbaar: 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 bijvoorbeeld het geval als de bestandsdialoog tijdens het laden en opslaan wordt opgeroepen. Het renderer-proces kan pas echt lezen of schrijven als de gebruiker een bestandsnaam heeft geselecteerd. 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 bijvoorbeeld vanuit het rendererproces een nieuw BrowserWindow openen of het menu daarvan aanpassen. Hoewel de remote-functies zich net zo gedragen als synchrone aanroepen, vindt er op de achtergrond communicatie 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 rechtsboven op deze pagina: dialog.showOpenDialog() tovert het systeemvenster 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 dialoogvenster moet daarboven verschijnen. Met het options-object, dat als tweede parameter wordt meegegeven, kun je configureren hoe dat eruit moet zien. Als de gebruiker een geldige keus heeft gemaakt, levert showOpenDialog() een array met de paden van de geselecteerde bestanden. Bij showSaveDialog() krijg je alleen een bestandsnaam.
Het feitelijke lezen wordt gedaan door de zelfgeschreven methode readInText(). Hierbij is alleen het vermelden waard dat de methode daarvoor gebruikmaakt van de Node-functie readFile():
Tweede venster
Enkele dialoogvensters in Electron-applicaties zijn niets anders dan overige BrowserWindow-instanties met een eigen renderer-proces en een HTML-bestand met daarin de gewenste besturingselementen. Je bepaalt de werking door bij het configuratieobject, dat je bij het aanmaken van het venster meegeeft, het attribuut parent op het hoofdvenster van de applicatie te zetten en modal op true. Bij c't-RegExer wordt dat gedemonstreerd door de afhandelingsroutine voor het IPC-kanaal 'showOptions' in het renderer-proces. De routine wordt geactiveerd als je klikt in het menu 'Bewerken / Opties'.
Voor data die in verschillende vensters worden gebruikt – hier: het hoofdvenster en dat voor de opties – is er in het Mainproces een global-object. Daaraan kun je willekeurige datavelden toevoegen. Renderer-processen kunnen die benaderen via de remote- functie getGlobal():
Inpakken
Als je eigen Electron-applicatie uiteindelijk 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 installeren 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, description, 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 besturingssysteem waar je op dat moment mee werkt.
In theorie kun je de builder (bij een lokale installatie 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 besturingssysteem maken. In de praktijk werkt dat echter niet. Gelukkig is het weinig moeite om de projectmap naar een computer met een ander besturingssysteem te kopiëren en de builder daar te laten draaien. Dat werkt in ieder geval betrouwbaar. Bovendien is de door Electron beloofde platformonafhankelijkheid daarmee alsnog met weinig moeite gerealiseerd. (nkr)