Programmeertalen: waartoe dienen ze?

Wanneer er aan de serverkant programmeertalen worden gebruikt, zoals C of PHP, dan wordt de programmeercode meestal sequentieel verwerkt. Dat betekent dat een server pas begint met het implementeren van een nieuwe instructie in de code, als de voorgaande instructie is uitgevoerd en resultaat heeft opgeleverd. Dit wordt ook wel synchrone bewerking genoemd. De uitvoering van verdere codes wordt gestopt tot de huidige handeling wordt beëindigd. Dit kan bij complexe handelingen, zoals het verkrijgen van toegang tot het bestandssysteem, databases of webservices, tot aanzienlijke vertragingen leiden.

Veel programmeertalen, runtime-omgevingen en hierop gebaseerde implementaties ondersteunen daarom de mogelijkheid om bewerkingen parallel uit te voeren in zogenaamde threads. Dit zijn afzonderlijke uitvoeringsreeksen in een proces, waarmee acties kunnen worden uitgevoerd terwijl de rest van de code blijft lopen. Het nadeel van deze methode: hoe meer threads worden gestart, des te meer CPU-tijd en RAM-geheugen nodig is. Met andere woorden: multithreading is bronintensief. Bovendien gaan extra threads gepaard met een aanzienlijk hogere programmeerinspanning. Deze problemen kun je oplossen door JavaScript aan de serverkant te implementeren, wat een asynchrone uitvoering van programmacodes mogelijk maakt. De basisprincipes daarvoor worden geleverd door de runtime-omgeving Node.js.


Wat is Node.js?

Node.js is een softwareplatform met een event-based architectuur, waarmee je de scripttaal JavaScript, die oorspronkelijk voor de client is ontwikkeld, aan de serverkant kunt gebruiken. Deze wordt dus op dezelfde manier gebruikt als PHP, Java, .NET, Ruby of Python, om codes voor de server te schrijven. Node.js wordt gebruikt bij de ontwikkeling van JavaScript-toepassingen aan de serverkant, die grote hoeveelheden gegevens in realtime moeten verwerken. De runtime-omgeving is populair voor de realisatie van lichtgewicht webservers.

Het cross-platform softwareproject werd gelanceerd in 2009 door Ryan Dahl en is gebaseerd op Google’s JavaScript-Engine V8, die ook in de webbrowser Chrome wordt gebruikt. Het project werd gestart door de firma Joyent en valt sinds 2015 onder een non-profit consortium: de OpenJS Foundation (voorheen Node.js Foundation). Er bestaan nu versies voor Microsoft Windows, Linux en macOS.

Node.js bevat een bibliotheek van diverse JavaScript-modules, die met een eenvoudige functie kunnen worden geladen en beschikbaar zijn als kant-en-klare bouwstenen voor de ontwikkeling van webtoepassingen. Een voorbeeld hiervan is de HTTP-module, waarmee je met één enkele functie een rudimentaire webserver kunt maken. Bovendien kun je met de geïntegreerde pakketmanager npm (Node Package Manager) extra modules installeren.


Node.js installeren

Op de officiële website van het softwareproject kan Node.js, voor de verschillende besturingssystemen, als installer- en/of binair pakket worden gedownload. De OpenJS-Foundation stelt daar niet alleen de broncode van de pakketten voor Windows, Linux en macOs voor x86/x64-PC's beschikbaar, maar ook voor ARM-, Power- en z-Systems-platformen. Daarnaast kun je hier versies voor AIX en SmartOS en een Docker Image downloaden.

De volgende tabel geeft een overzicht van de ondersteunde platformen en architecturen:

Platform Ondersteunde architectuur
Windows Installer (.msi) 32-bit, 64-bit
Windows Binary (.zip) 32-bit, 64-bit
macOS Installer (.pkg) 64-bit
macOS Binaries (.tar.gz) 64-bit
Linux Binaries (x64) 64-bit
Linux Binaries (ARM) ARMv7, ARMv8
SmartOS Binaries 64-bit
Docker Docker Image
Linux on Power Systems 64-bit
Linux on System z 64-bit
AIX on Power Systems 64-bit

Hieronder laten we als voorbeeld zien hoe Node.js in een lokale Windows-installatie wordt gebruikt. We tonen je in drie eenvoudige stappen hoe je Node.js op een Windows-computer installeert.


Stap 1. softwarepakket downloaden

Om Node.js op een Windows PC te installeren, hoef je alleen de 32- of 64-bit versie van Windows Installer te downloaden. Naast de huidige v15.6.0 versie is de LTS release 14.15.4 (long-term support) beschikbaar, die ook de basis vormt van deze Node.js-tutorial. De long-term support garandeert je 1,5 jaar bugfixes en updates tegen kritieke veiligheidslekken in het daaropvolgende jaar.


Stap 2. Node.js installeren

Na het dubbelklikken op het .msi-bestand verschijnt er een Windows beveiligingswaarschuwing. Klik op ‘Run’ om te bevestigen dat je het geselecteerde bestand wilt uitvoeren.

Node.js installeren
Gebruik alleen software van vertrouwde bronnen.

De installatie-setup van Node.js wordt geopend.

Node.js installatie setup
De installatie-setup van Node.js leidt je door de installatie van de runtime-omgeving.

Voordat je de installatie kunt starten, moet je eerst de licentievoorwaarden van de software accepteren.

Node.js MIT-licentie
De runtime-omgeving Node.js valt onder de MIT-licentie.

Geef aan in welke map van je bestandssysteem Node.js moet worden geïnstalleerd.

Node.js installatie in map Programma's
Wij adviseren de standaardinstallatie in de map Programma’s.

Installeer Node.js inclusief npm package manager.

Node.js standaardinstallatie
Wij adviseren om Node.js volgens de standaardinstallatie te installeren.

Start de installatie door op ‘Install’ te klikken.

Node.js admin-rechten installatie
Let op: voor de installatie van het programma zijn admin-rechten nodig

De installatie van de runtime-omgeving moet binnen enkele seconden voltooid zijn.

Node.js voortgang installatieproces
De voortgang van het installatieproces wordt in het setup-venster weergegeven.

Voltooi het installatieproces met een klik op ‘Finish’.

Node.js installatie geslaagd
De installatie van Node.js is geslaagd.

Je lokale Node.js-installatie is nu klaar voor gebruik.


Stap 3. Installatie testen

Je controleert of de installatie geslaagd is door Node.js te starten en een eerste eenvoudige JavaScript-code uit te voeren. De runtime-omgeving biedt daarvoor twee modi: je gebruikt Node.js in de interactieve modus of je voert de applicatiecode uit van een eerder aangemaakt JavaScript (.js)-bestand.

  • Interactieve modus - Om Node.js in de interactieve modus te gebruiken, open je de Windows opdrachtprompt (cmd.exe) en voer je het commando node in. De zogenaamde REPL (Read-Evaluation-Print Loop), een interactieve opdrachtregelinterpreter (Command Prompt) wordt gestart. Deze leest JavaScript-code in (Read), evalueert deze (Evaluation) en stuurt het resultaat terug (Print). Daarna begint het geheel weer opnieuw (Loop).
  • De JavaScript-code uit een bestand uitvoeren - Om de JavaScript-code uit een bestand naar de runtime-omgeving over te zetten, gebruik je het commando node in combinatie met het pad naar het betreffende bestand.

Test eerst de interactieve modus. Open hiervoor de Windows opdrachtprompt.

Node.js interactieve modus
De eenvoudigste manier om in Windows de opdrachtprompt te openen, is het indrukken van de toetsencombinatie [Windows toets] + [r] en het intypen van ‘cmd’.

Start de JavaScript runtime-omgeving via het commando node.

Node.js command prompt
De verandering van de Windows opdrachtprompt naar de Node.js-command prompt wordt bovenin het terminalvenster aangegeven door de toevoeging ‘node’.

Je bent nu in de interactieve command prompt van Node.js. Om te controleren of Node.js correct geïnstalleerd is en goed werkt, adviseren wij de functie console.log(). Deze neemt waarden op en toont ze in de terminal. Test dit bijvoorbeeld met een eenvoudig ‘Hello World!’-script.


console.log('Hello World!');

De bovenstaande functie, die in de Node.js-prompt is ingevoerd en met de Enter-toets is bevestigd, geeft Node.js opdracht om de tekenreeks (string) 'Hello World!' naar de terminal te schrijven. Het resultaat zie je in het volgende screenshot:

Node.js hello world uitvoer
De uitvoer 'Hello World!' laat zien dat de installatie van Node.js is geslaagd.

De terminaluitvoer laat zien dat Node.js correct is geïnstalleerd en in staat is om JavaScript-code interactief uit te voeren. Je kunt met console.log() bijvoorbeeld ook het resultaat van een rekensom naar de terminal schrijven:


console.log (15 + 385 * 20);

Node.js rekensom check
Controleer met een eenvoudige rekensom of Node.js goed werkt.

Wil je gedetailleerde informatie over de geïnstalleerde Node.js-versie en haar modules laten weergeven, gebruik dan de volgende uitdrukking (expression):


process.versions

Node.js commando process.versions
Het commando process.versions geeft aan welke Node.js-versie is geïnstalleerd.

Wil je Node.js beëindigen en terugkeren naar de Windows opdrachtprompt, gebruik dan de toetsencombinatie [STRG] + [C]. Er zijn twee toetsaanslagen nodig. Dit voorkomt dat je de interactieve opdrachtprompt per ongeluk verlaat.

Let op: wordt - zoals hierboven beschreven - de installatie-setup gebruikt, dan kan er per systeem slechts één versie van de JavaScript runtime-omgeving worden geïnstalleerd. Bij de installatie van een nieuwe versie van Node.js wordt de oude versie overschreven. Wil je meer dan één versie van Node.js tegelijk laten draaien, dan heb je een versiemanager nodig, zoals de Node Version Manager.


Node.js-module

Op basis van de runtime-omgeving V8 kun je Node.js gebruiken om efficiënte webservers en andere netwerktoepassingen te schrijven in de populaire scripttaal JavaScript. Node.js heeft een bewust compact ontwerp, waarbij alleen kernfunctionaliteiten − zoals de interactie met het besturingssysteem, de netwerkcommunicatie of versleutelingsmechanismen − als basismodule in de runtime-omgeving zijn geïntegreerd. Alle overige functionaliteiten zijn als uitbreidingsmodules beschikbaar. Gebruikers kunnen terugvallen op een uitgebreid assortiment modules van derden of hun eigen modules programmeren.

Om een willekeurige module in een Node.js-toepassing te laden, volstaat de functie require(). Deze verwacht een string die als parameter aangeeft welke module moet worden geladen. Meestal is dit de naam van de module. Wordt require() gebruikt in combinatie met een modulenaam, dan doorzoekt Node.js verschillende directories naar modules die als basismodule deel uitmaken van Node.js, of die met npm projectspecifiek (lokaal) of projectoverstijgend (globaal) geïnstalleerd zijn. Deze modules worden zonder pad gevonden. De huidige werkmap wordt niet doorzocht. Wil je een zelfgeschreven module uit je huidige projectmap laden, dan is een relatief pad met ./ nodig.

De volgende voorbeelden laten beide basisschema's zien, volgens welke modules in Node.js-toepassingen worden geladen:


const modulname = require('modulname');
const modulname = require('./modulname');

In deze Node.js-tutorial tonen we je aan de hand van voorbeelden hoe je basismodules gebruikt, modules van derden integreert en je eigen Node.js-modules maakt.


Basismodules gebruiken

Moet bijvoorbeeld de basismodule os geladen worden, dan zou de code er zo uitzien:


const os = require('os');

De os-module biedt diverse functies die kunnen worden gebruikt om informatie over het besturingssysteem op te roepen. Als een gebruiker bijvoorbeeld de hoeveelheid vrij systeemgeheugen in bytes wil weten, wordt de volgende code gebruikt:


const os = require('os');
const freemem = os.freemem();
console.log(freemem);

In de eerste regel wordt de os-module in de gelijknamige constante os geladen. Het codegedeelte os.freemem() in regel 2 voert een van de functies, die door de os-module wordt ondersteund, uit en wordt gebruikt om de hoeveelheid vrij systeemgeheugen in bytes te meten. De gemeten waarde wordt opgeslagen in de constante freemem. In de derde regel wordt tot slot de al bekende functie console.log() gebruikt om de in de constante freemem opgeslagen waarde naar de terminal te schrijven.

Node.js module os
De module os geeft besturingssysteem-gerelateerde informatie uit.

Zoals de uitvoer in de terminal laat zien, is er op ons testsysteem momenteel 25,6 gigabyte systeemgeheugen beschikbaar.

Met andere functies van de os-module, zoals os.networkInterfaces() of os.hostname(), kan een lijst van netwerkinterfaces worden opgeroepen of kan de hostnaam worden bepaald.

De onderstaande tabel bevat een selectie van de belangrijkste basismodules van de JavaScript runtime-omgevingen, met beschrijving.

Node.js-basismodule (selectie)
Module Beschrijving Oproep
assert De basismodule assert biedt een set eenvoudige stellingtests, waarmee ontwikkelaars kunnen controleren of de methodeparameters en invarianten van een programma correct zijn. Assertions (stellingen) worden gebruikt om bepaalde uitspraken over de status van een programma te verifiëren en ervoor te zorgen dat ze worden opgevolgd. Als uitspraken ongeacht de uitvoering van bepaalde programmacommando's gelden en daarom in elke loopherhaling moeten worden uitgevoerd, dan worden ze invarianten genoemd. assert is hoofdzakelijk bedoeld voor het interne gebruik van Node.js, maar kan ook opgenomen worden in de eigen toepassingscode via de require()-functie. require('assert')
buffer De buffer-module biedt Node.js een mechanisme voor het verwerken van binaire datastromen. De buffermodule is een globale klasse binnen Node.js. Het is daarom niet nodig de module via require() op te roepen.
child_process De basismodule child_process breidt Node.js uit met een functie waarmee kindprocessen kunnen worden gecreëerd. child_process werkt op dezelfde manier als de Unix-functie popen (3). require('child_process')
cluster Normaliter loopt Node.js in één thread op één processor van de onderliggende computer. Ontwikkelaars die gebruik willen maken van de voordelen van een multicore-systeem moeten de cluster-module integreren. Deze biedt Node.js een mechanisme waarmee het een netwerk van afzonderlijke processen kan creëren, die een gemeenschappelijke serverpoort delen. Als deze processen naar de verschillende kernen van de onderliggende computer worden gedistribueerd, dan heeft Node.js de volledige prestaties van het systeem tot zijn beschikking. require('cluster')
console De basismodule console biedt ontwikkelaars een eenvoudige console met twee componenten:
  • een klasse console, die functies biedt voor het schrijven van data naar een Node.js-stream
  • een globale console die functies biedt voor het schrijven van data naar de standaard uitgang (stdout) of naar de standaard foutuitvoer (stderr).
De console module wordt meestal gebruikt voor debugging.
De klasse console wordt opgeroepen via require(' console'). De globale console kan zonder verdere voorbereiding worden gebruikt.
crypto De crypto-module bevat diverse wrappers voor verschillende functionaliteiten van de open source TLS toolkit OpenSSL, inclusief hashing, HMAC, encryptie, decryptie, ondertekening en verificatie. require('crypto')
debugger Met de debugger-module bevat Node.js een tool voor het diagnosticeren en vinden van fouten in JavaScript-documenten. Om de module debugger te gebruiken, start je Node.js met het argument debug, gevolgd door het te scannen JavaScript-document. Voorbeeld: node debug myscript.js
dns De basismodule dns biedt twee soorten functies:
  • Functies die gebruikmaken van het onderliggende besturingssysteem voor de naamresolutie
  • Functies die een verbinding tot stand brengen met een DNS-server voor de naamresolutie
require('dns')
events Ontwikkelaars die gebeurtenissen willen activeren of verwerken in de context van een Node.js-toepassing, gebruiken daarvoor de basismodule events. require('events')
fs De basismodule fs (file system) geeft Node.js toegang tot het bestandssysteem en bevat verschillende functies voor het lezen en schrijven van bestanden. require('fs')
http(s) Met http(s) biedt Node.js een basismodule, die alle functies bevat die ontwikkelaars nodig hebben om eenvoudige HTTP(s)-servers en -clients te creëren. require('http'); require('https')
net De basismodule net biedt ontwikkelaars functies om TCP- of IPC-servers en clients te creëren. require('net')
os os is een basismodule die verschillende functies biedt voor fundamentele systeembewerkingen. Met os.freemem() is een van die functies al in deze Node.js-tutorial aan bod gekomen. require('os')
path Ontwikkelaars die de basismodule path oproepen, krijgen toegang tot functies die verschillende handelingen mogelijk maken met betrekking tot bestands- en directorypaden. Als Node.js op een Windows-systeem wordt gebruikt, zorgt de path-module er bijvoorbeeld voor dat bestands- en directorypaden correct worden geïnterpreteerd in Windows-stijl. require('path')
process process is een globaal object dat ontwikkelaars informatie verschaft over het huidige Node.js-proces en hen in staat stelt het te besturen. Als globaal object kan process op elk moment worden gebruikt, zonder dat de basismodule via require() hoeft te worden opgeroepen.
querystring De basismodule querystring biedt functies voor het analyseren en formatteren van URL-querystrings. require('querystring')
stream In de Node.js-terminologie is een stream een abstracte interface die het mogelijk maakt om met streamingdata te werken. De basismodule stream biedt een API voor het maken van objecten die de streaminterface bevatten. require('stream')
timers De basismodule timers biedt een globale interface voor de scheduling van functies die op een later tijdstip moeten worden opgeroepen. De functies van de module timers werken globaal, dus ze hoeven niet te worden opgeroepen via require().
url De url-basismodule biedt functies voor URL-resolutie en het parseren van URL’s. require('url')
util util is een basismodule die voornamelijk de interne API's van Node.js ondersteunt. Indien gewenst kan de module ook in andere toepassingen worden geïntegreerd. require('util')
vm De basismodule vm biedt interfaces waarmee je JavaScript-code kunt compileren en op een V8 virtuele machine kunt uitvoeren. require('vm')
zlib Node.js biedt in de basisinstallatie al compressiefunctionaliteiten die gebaseerd zijn op Gzip en Deflate/Inflate. Deze worden geleverd door de basismodule zlib. require('zlib')

Meer informatie over deze en andere basismodules van Node.js vind je in de officiële documentatie van het softwareproject.


Modules van derden integreren

De post-installatie van programmamodules gebeurt bij Node.js via de geïntegreerde package manager npm. Hiermee hebben Node.js-gebruikers toegang tot de npm-registry, een community-based online archief voor Node.js-modules. Dit betekent dat uitbreidingsmodules niet handmatig van externe pagina's gedownload en naar een bijbehorende directory gekopieerd hoeven te worden. Voor het installeren hoef je alleen enkele coderegels in te voeren in de besturingssysteemconsole (voor Windows-besturingssystemen de opdrachtregelinterpreter cmd.exe). Het enige wat je nodig hebt is het commando npm install en de naam van de te installeren module.


npm install modulname

LET OP! De installatie is standaard lokaal en vindt plaats in de huidige directory.
Om npm-modules globaal te installeren, heb je de optie -g (of --global) nodig.

Alle beschikbare uitbreidingsmodules voor Node.js kun je via een zoekfunctie bekijken op de officiële website van npm.

Als bijvoorbeeld de derdenmodule colors moet worden geïnstalleerd, navigeer je in de console van het besturingssysteem naar de gewenste directory en bevestig je de volgende opdracht met de Enter-toets:


npm install colors

LET OP! Er wordt geadviseerd om voor elke applicatie die met Node.js is ontwikkeld een aparte toepassingsdirectory aan te maken.
In deze directory kun je alle lokale bestanden opslaan die nodig zijn voor de programma-uitvoering.

De volgende screenshot toont de installatie van de color-module in de eerder aangemaakte directory C:\my_app:

Node.js-tutorial waarschuwing ontbreken metadata
Waarschuwing tijdens de installatie wijst op het ontbreken van metadata, maar kun je negeren.

Als onderdeel van de installatie maakt npm een subdirectory node_modules aan in je toepassingsdirectory. Deze dient als opslaglocatie voor alle andere modules die je wilt installeren. Als een module in de directory node_modules wordt aangemaakt, dan verandert de boomstructuur. Dit soort veranderingen wordt opgeslagen in een automatisch gegenereerd JSON-bestand (JavaScript Object Notation), de zogenaamde package-lock.

LET OP! In het huidige voorbeeld krijgen we verschillende waarschuwingen tijdens de installatie van de colors-module. Dit komt omdat we voor onze toepassingsdirectory my-app geen metadata bestand – de zogenaamde package.json – hebben aangemaakt.
Dit wordt echter pas relevant wanneer je je eigen applicaties als modules wilt publiceren. Hoe je een package.json maakt, zie je in het hoofdstuk ‘Eigen modules publiceren’.

Om de later geïnstalleerde module in een Node.js-toepassing te laden, gebruik je de al ingevoerde functie require(). Het volgende codevoorbeeld laat zien hoe de color-module wordt gebruikt om de uitvoer in de Node.js-console in kleur weer te geven:


const colors = require('colors');
console.log('hello'.green); // levert groene tekst op 
console.log('I like cake and pies'.bold.red); // levert vetgedrukte rode tekst op 
console.log('inverse the color'.inverse); // keert de kleuren om 
console.log('All work and no play makes Jack a dull boy!'.rainbow); // regenboog

In de eerste coderegel wordt de colors-module in de colors-constante met dezelfde naam geladen. De regels 2 tot 5 bevatten elk een functie console.log (), waarvan de strings door de color-module op verschillende manieren gemarkeerd moeten worden. Mogelijk zijn kleuraanpassingen (bijv. .red, .green), markeringen (bijv. .bold, .inverse) en sequenties (bijv. .rainbow).

Node.js-tutorial codevoorbeeld colors-module
Het codevoorbeeld toont hoe de colors-module wordt toegepast.

LET OP! Normaal gesproken bevat elke toepassing die je ontwikkelt met Node.js zijn eigen node_modules-directory, die dient als opslaglocatie voor modules van derden. Hoewel modules met de optie -g of --global in principe ook globaal kunnen worden geïnstalleerd, geven ontwikkelaars van Node.js meestal de voorkeur aan lokale installatie. Het nadeel hiervan is dat elke module voor elke toepassing opnieuw moet worden geïnstalleerd.

Als bijvoorbeeld een bepaalde module in tien verschillende toepassingen vereist is, dan moet deze module tien keer op de harde schijf worden opgeslagen. Dit neemt extra opslagruimte in beslag, maar voorkomt versieconflicten, aangezien elke toepassingsdirectory alle afhankelijkheden bevat die nodig zijn om de bijbehorende toepassing uit te voeren.


Eigen Node.js module

De applicatiecode die je kunt downloaden via npmjs.com is in principe dezelfde als de code die je zelf programmeert op basis van Node.js. Iedereen die de JavaScript runtime-omgeving gebruikt, kan applicaties programmeren en met de community delen als modules via npm. We zullen je laten zien hoe je dit kunt doen: Eerst maken we via een korte Node.js-server tutorial met slechts acht coderegels een eigen webserver en breken deze op om de applicatiecode te integreren. Vervolgens bereiden we het programma voor op de publicatie.


Eigen modules maken

Een standaartoepassing van Node.js is de ontwikkeling van lichtgewicht webservers. Een simpele ‘Hello World!’-server kan met de voorgeprogrammeerde module ‘http’ binnen enkele seconden worden geprogrammeerd.

1. Applicatiecode schrijven: het enige wat je nodig hebt om een webserver met Node.js te maken, zijn de volgende acht regels JavaScript-code. Voer deze in een teksteditor in en sla de applicatie op als een JavaScript-bestand in een willekeurige directory. In deze Node.js-server tutorial wordt het bestand webserver.js genoemd en opgeslagen in de directory my_webserver.


var http = require('http');
var port = 8080;

var server = http.createServer(function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World!');     
});
server.listen(port);
console.log('The server is available at http://127.0.0.1:' + port + '/');

In coderegel 1 wordt de webserver-module http via de functie require() geladen en opgeslagen in de constante http met dezelfde naam.


var http = require('http');

Vervolgens wordt in regel 2 het poortnummer 8080 gedefinieerd en opgeslagen in de constante poort.


var port = 8080;

De volgende regels 4 tot 10 definiëren met behulp van de functie createServer() van de http-module een callback-functie die inkomende verzoeken van de browser ontvangt (request, req) en een antwoord terugstuurt (respons, res). Welke reactie wordt teruggestuurd wordt bepaald met de functies res.writeHead() en res.write(). Terwijl res.write() met ‘Hello World!’ het eigenlijke antwoord bevat, biedt res.writeHead() een header die naast de HTTP statuscode 200 (OK) meta-informatie over het antwoord verstrekt. In het huidige voorbeeld wordt het antwoord geclassificeerd als tekst in HTML-formaat. Daarna wordt de verbinding beëindigd via res.end().


var server = http.createServer(function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World!');     
});

De listen-functie in coderegel 12 wordt gebruikt om de webserver te verbinden met het poortnummer zoals gedefinieerd in regel 2, en hiermee kun je ook een callback toevoegen. In regel 13 gebruiken we deze om een statusmelding naar de terminal te schrijven met console.log.


server.listen(port); {
console.log('The server is available at http://127.0.0.1:' + port + '/');

In het huidige voorbeeld is de webserver beschikbaar onder het Localhost adres 127.0.0.1:8080 in het eigen systeem.

2. Applicatiecode uitvoeren: om de webserver te starten, voer je het JavaScript-bestand uit dat je net hebt aangemaakt via de Windows opdrachtprompt. Navigeer hiervoor naar de directory my_webserver en bevestig de volgende opdracht met de Enter-toets:


node webserver.js

Wanneer je alle informatie in het script correct hebt overgenomen en het bestandspad correct hebt opgegeven, zal de console het webadres van de server uitvoeren.

Node.js voor opzetten lichtgewicht webservers
Node.js wordt vaak gebruikt om lichtgewicht webservers op te zetten

3. Webserver testen: de webserver is nu klaar om verzoeken te ontvangen. Je kunt dit testen door in een willekeurige webbrowser het webadres 127.0.01:8080 in te voeren. Als de server correct geprogrammeerd is, verschijnt in het browservenster de tekst “Hello World!”.

Node.js programmeren webserver met

Als de browser de tekst “Hello World!” weergeeft, dan is de webserver correct geprogrammeerd

Je hebt met webserver.js je eerste eigen applicatie geschreven. In het volgende hoofdstuk laten we zien hoe je je eigen applicaties als modules kunt integreren in andere toepassingen.


Eigen modules integreren

Via de require-functie worden eigen modules, basismodules of modules van derden geïntegreerd. Om dit te illustreren, splitsen we de zojuist aangemaakte webserverapplicatie in twee afzonderlijke bestanden: een bestand met de webservercode en een bestand met de applicatiecode. Vervolgens integreren we de applicatiecode als module in de webservercode.

LET OP! Een opdeling naar functionaliteit komt overeen met het ontwerpprincipe Separation of Concerns (SoC).
Dit is een fundamenteel ontwerpprincipe van modulaire computerprogramma's waarbij de applicatiecode zo is gestructureerd dat elk onderdeel dat een nieuwe, zelfstandige functionaliteit biedt, in een apart bestand wordt opgeslagen.

Ga als volgt te werk om je webserver te splitsen in twee afzonderlijke applicaties volgens het principe Separation of Concerns (SoC):

1. Schrijf de applicatiecode voor de handling van verzoeken en antwoorden: Maak een JavaScript bestand handle.js zoals in het volgende voorbeeld en sla het op in de directory my_webserver.


const handle = function (req, res) {
    res.writeHead(200, {
      'Content-Type': 'text/html'
    });
    res.write('Hello World!'); 
    res.end();
};

module.exports = handle;

Het handle.js-bestand bevat het gedeelte van je webserverapplicatie dat verzoeken en antwoorden behandelt. Het omvat in principe de callback-functie function(req, res), die je in het vorige voorbeeld binnen de http.createServer() inline hebt gedefinieerd. Deze callback-functie wordt opgeslagen in de constante handle. Om de als handle gedefinieerde callback-functie ook buiten handle.js te kunnen gebruiken, moet je deze als module exporteren. Node.js heeft daarvoor het object module.exports, waarmee willekeurige elementen (bijv. functies, strings, objecten, etc.) kunnen worden geëxporteerd.

2. Webservercode schrijven: Maak een JavaScript-bestand webserver1.js, zoals in het volgende voorbeeld, en sla het op in je applicatie-directory my_webserver.


const http = require('http');
const port = 8080;
const handle = require('./handle');

const server = http.createServer(handle); 

server.listen(port, () => {
    console.log('Server is available under http://127.0.0.1:' + port + '/');
});

De code is in principe dezelfde als die van de webserverapplicatie uit het vorige voorbeeld. Maar in plaats van de handling van verzoeken inline te definiëren in de createServer-functie, verwijs je in regel 5 naar de eerder aangemaakte module handle (je kunt de extensie .js weglaten). Om ervoor te zorgen dat de resource handle tijdens de programmaverloop beschikbaar is, moet deze als module via require() worden geïntegreerd. Aangezien beide bestanden zich in de huidige werkmap bevinden, gebruiken we het relatieve bestandspad ./handle. Absolute bestandspaden worden meestal niet gebruikt in Node.js-projecten.

3. Webserverapplicatie uitvoeren: Open de Windows opdrachtprompt, navigeer naar de directory my_webserver en voer het JavaScript-bestand Webserver1.js uit met de volgende opdracht:


node webserver1.js

Als de module handle correct geïntegreerd is, verschijnt de statusmelding: “Server is available under http://127.0.0.1:8080”. Als je dit adres oproept, krijg je: “Hello World!”.


Eigen modules publiceren

Wil je je zelfgemaakte toepassingen of programmaonderdelen aan andere gebruikers van Node.js ter beschikking stellen, dan adviseren wij een publicatie via npm. De npm registry is voor de community een centraal archief voor modules van derden. In principe kan elke gebruiker van Node.js modules aanbieden. Voorwaarde hiervoor is een gratis registratie bij npm.

LET OP! Naast de gratis account, biedt npm ook betaalde abonnementen voor individuele ontwikkelaars en teams aan,
die een uitgebreide reeks extra functies biedt.

Om je bij npm te registreren, ga je als volgt te werk:

  1. Ga naar de npm startpagina.
  2. Vul het inschrijfformulier in en stuur het terug.
  3. Bevestig je e-mailadres.

Je kunt nu inloggen via de webinterface onder www.npmjs.com. Als alternatief stelt npm ook de volgende opdracht voor het aanmelden via de terminal ter beschikking:


npm login

Telkens wanneer je je via de terminal aanmeldt, wordt je gevraagd om je gebruikersnaam, wachtwoord en e-mailadres. Nadat je bent ingelogd via de terminal kun je een willekeurig aantal Node.js-modules publiceren.

Voorwaarde voor de publicatie van een module is dat deze een volledig package.json bevat, met alle metadata voor de module. Standaard bevat dit metadatabestand de volgende informatie:

  • name: de naam van je module (verplicht)
  • version: de versie van je module (verplicht)
  • description: een korte beschrijving van je module
  • entry point: het oorspronkelijke bestand van je applicatie (bijv. index.js)
  • test command: een commando dat kan worden gebruikt om tests te starten (indien beschikbaar)
  • git repository: URL naar een git repository, bijv. GitHub (indien beschikbaar)
  • keywords: sleutelwoorden voor de vindbaarheid via de npm-zoekfunctie
  • author: je naam
  • license: de licentie waaronder het project wordt gepubliceerd (bv. MIT)

Node.js-gebruikers die de package .json niet handmatig willen schrijven, kunnen de volgende opdrachtregelopdracht gebruiken:


npm init

Deze start een commandoregelprogramma dat basis-metadata in de interactieve modus opzoekt en vervolgens een JSON-bestand aanmaakt volgens onderstaand schema:


{
  "name": "module-name",
  "version": "1.0.0",
  "description": "An example module to illustrate the usage of a package.json",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/username/repo.git"
  },
  "keywords": [
    "example"
  ],
  "author": "John Doe",
  "license": "MIT"
}

LET OP! Het via npm init aangemaakte package.json moet normaal gesproken handmatig worden aangevuld,
bijvoorbeeld met een lijst van derdenmodules die voor het programmaverloop nodig zijn. Deze worden opgenomen onder dependencies (afhankelijkheden).

"dependencies": {
    "express": "4.2.x",
    "… "
  }

Het huidige voorbeeld toont een afhankelijkheid van de derdenmodule express in de versie 4.2. x.

Alleen de in package.json opgeslagen metadata maken het mogelijk om een module uniek te identificeren via naam en versienummer. Bovendien biedt package.json andere gebruikers van Node.js alle informatie die ze nodig hebben om de module te gebruiken en de benodigde afhankelijkheden te leveren. Want in principe wordt elke module in het register geladen zonder node_modules-map. Als een module van een derde wordt gebruikt, dan gebruikt Node.js de waarden onder dependencies in plaats daarvan om de vereiste modules automatisch te downloaden. Je moet deze metadata daarom altijd specificeren wanneer je eigen derdenmodules in je toepassing opneemt.

Wanneer je alle relevante metadata in de package.json hebt gestopt, staat niets de publicatie van je toepassing meer in de weg. De upload naar de npm-registry wordt in drie eenvoudige stappen uitgevoerd:

1. Inloggen: Log in bij npm via de terminal.


npm login

2. Applicatie kiezen: Navigeer naar de directory van de toepassing die je als module wilt publiceren.

3. Module publiceren: Voer de volgende opdracht in de terminal in en bevestig met Enter:


npm publish

Het commando npm publish pakt de geselecteerde module (applicatiecode en package.json) als een tar-archief en uploadt deze naar het npm-registry.

Let op: een gepubliceerde module kan alleen binnen de eerste 24 uur na publicatie worden verwijderd. Gebruik daarvoor het volgende commando:


npm unpublish

Modules die langer dan 24 uur online zijn, kunnen niet meer door de gebruiker worden verwijderd. Neem in dit geval contact op met de npm-support. Deze unpublish-functie is bedoeld om te voorkomen dat gebruikers van Node.js applicaties ontwikkelen die afhankelijk zijn van modules die later niet meer beschikbaar zijn.


Asynchrone programmering met Node.js: callbacks, events en streams

Een groot voordeel van Node.js is de event-gestuurde architectuur, waarmee je programmacode asynchroon kunt uitvoeren. Node.js vertrouwt daarbij op single threading en een uitbesteed input/outputsysteem (I/O) waarmee meerdere schrijf- en leesbewerkingen parallel kunnen worden verwerkt.

  • Asynchrone I/O: Tot de klassieke taken van een server behoren het beantwoorden van verzoeken, het opslaan van gegevens in een database, het lezen van bestanden vanaf de harde schijf en het tot stand brengen van verbindingen met andere netwerkcomponenten. Deze activiteiten worden samengevat onder de afkorting ‘I/O’ (Input/Output). I/O-bewerkingen worden synchroon uitgevoerd in programmeertalen zoals C of Java. Dit betekent dat de ene taak na de andere wordt verwerkt. Het I/O-systeem wordt geblokkeerd totdat de huidige taak is voltooid. Node.js daarentegen maakt gebruik van asynchrone I/O, waarbij schrijf- en leesbewerkingen direct worden overgedragen aan het besturingssysteem of een database. Dit maakt het mogelijk om een groot aantal I/O-taken parallel uit te voeren zonder blocking (een blokkade), wat toepassingen op basis van Node.js en JavaScript in veel scenario's een enorm snelheidsvoordeel geeft.
  • Single threading: Om wachttijden bij synchrone I/O te compenseren, gebruiken serverapplicaties op basis van klassieke programmeertalen aan de server-kant extra threads – met de nadelen van een multithreading-benadering zoals hierboven beschreven. Apache HTTP Server start bijvoorbeeld een nieuwe thread voor elke inkomende aanvraag. Het mogelijke aantal threads wordt beperkt door het beschikbare geheugen – en daarmee ook door het aantal aanvragen dat in een synchroon multithreading-systeem parallel kan worden beantwoord. Node.js daarentegen vereist slechts één thread door het uitbestede I/O-systeem, wat de complexiteit en het gebruik van resources aanzienlijk vermindert.

De asynchrone uitvoering van I/O-taken met Node.js wordt gerealiseerd via concepten zoals callbacks, events en streams.

LET OP! De kernapplicatie Node.js start meerdere threads, alleen de zogenaamde ‘userland code’ –
de programmacode van de desbetreffende ontwikkelaar – loopt op basis van single threading.


Callbacks

Een callback is een terugroepfunctie, die door Node.js wordt gebruikt wanneer een taak moet worden uitbesteed en JavaScript-code asynchroon moet worden uitgevoerd (bijv. in het geval van een I/O-handeling). Callback-functies hebben als taak zich te melden zodra een gedelegeerde handeling is voltooid en het resultaat terug te sturen. Doorgaans worden callbacks zo geïmplementeerd dat een foutmelding plaatsvindt als de gedelegeerde taak niet naar wens kon worden uitgevoerd.

Let er hierbij op dat callback-functies niet noodzakelijkerwijs asynchroon zijn. Ook al worden callbacks altijd gebruikt wanneer de programmacode asynchroon moet worden uitgevoerd. Omgekeerd is het niet zo dat callbacks altijd betrekking hebben op asynchrone code.

Het verschil tussen een synchrone en asynchrone uitvoering van programmacode wordt duidelijk aan de hand van voorbeelden. Ga als volgt te werk om een bestand synchroon te importeren:

1. Tekstbestand maken: maak een tekstbestand (.txt) input_sync aan met een willekeurige inhoud – bijvoorbeeld:


This code example runs synchronously.

Sla dit op in een willekeurige directory op je systeem.

2. Applicatie schrijven: schrijf een eenvoudige JavaScript-toepassing, zoals in het volgende voorbeeld, en plaats deze onder app_sync.js in dezelfde directory.


var fs = require('fs');
var data = fs.readFileSync('input_sync.txt');
console.log(data.toString());
console.log('Program Ended');

3. Applicatie uitvoeren: open de Windows opdrachtprompt, navigeer naar de directory waarin je de toepassing en het tekstbestand hebt opgeslagen en voer app_sync.js uit met de volgende opdracht:


node app_sync.js

In de console krijg je deze uitvoer:


This code example runs synchronously.
Program Ended

De programmacode van app_sync.js wordt sequentieel, d.w.z. van boven naar beneden, uitgevoerd. Eerst worden de gegevens van input_sync.txt via fs.readFileSync() in het programma geladen en vervolgens naar de standaarduitvoer (meestal de terminal) geschreven met behulp van de console.log()-functie. Pas als deze I/O-taak is voltooid, gaat het programma verder met de tweede console.log-functie, die de string ‘Program Ended’ naar de terminal schrijft.

Ga als volgt te werk om een callback-functie asynchroon uit te voeren:

1. Tekstbestand maken: maak een tweede tekstbestand input_async aan met een willekeurige inhoud – bijvoorbeeld:


This code example runs asynchronously.

Sla dit tekstbestand op in dezelfde directory als de andere bestanden.

2. Applicatie schrijven: schrijf een tweede JavaScript-toepassing zoals in het volgende voorbeeld en plaats deze onder app_sync.js in dezelfde directory.


var fs = require('fs');
fs.readFile('input_async.txt', function (err, data) {
    if (err) return console.error(err);
    console.log(data.toString());
});
console.log('Program Ended');

3. Applicatie uitvoeren: voer app_async.js uit met de volgende opdracht:


node app_async.js

In de console krijg je deze uitvoer:


Program Ended
This code example runs asynchronously.

Ook de programmacode app_async.js wordt sequentieel van boven naar beneden uitgevoerd. Anders dan bij app_sync.js heeft het programma echter niet gewacht tot de I/O-taak in regel 2 is voltooid, maar gaat, nadat de taak aan het bestandssysteem is gedelegeerd, zonder vertraging door met de uitvoering van de verdere programmacode. Omdat de I/O-taak langer duurt dan de uitvoering van de console.log-functie in regel 6, krijgen we eerst de string ‘Program Ended’ en daarna het resultaat van de I/O-taak.


Events

Terwijl een klassieke callback slechts één keer wordt opgeroepen tijdens het programmaverloop en dus slechts één resultaat oplevert, biedt Node.js met de geïntegreerde events-module de mogelijkheid om trigger en reactie door events te ontkoppelen. Een en dezelfde event-handler kan dus gemakkelijk op verschillende event-triggers reageren. Dit concept wordt altijd gebruikt wanneer bij een bepaalde gebeurtenis automatisch een daarmee verbonden taak moet worden uitgevoerd.

De event-gestuurde architectuur van Node.js is in principe gebaseerd op één enkele thread in een oneindig draaiende eventloop. Deze eventloop heeft de taak om op gebeurtenissen te wachten en deze te beheren. Gebeurtenissen kunnen taken of resultaten zijn. Als de eventlus een taak registreert, bijvoorbeeld een database query, dan wordt deze via een callback-functie aan een proces op de achtergrond uitbesteed. De taak wordt daarom niet verwerkt in dezelfde thread als die waarin de eventloop loopt, zodat deze direct door kan gaan naar het volgende event. Als een uitbestede taak is uitgevoerd, dan worden de resultaten van het uitbestede proces met behulp van de callback-functie als nieuwe gebeurtenis in de eventloop teruggezet. Hierdoor kan het resultaat worden doorgegeven.

Om objecten te genereren, die events kunnen activeren en verwerken, biedt Node.js de klasse EventEmitter in de basismodule events. Hiermee kunnen programmeurs hun eigen klassen afleiden voor event-triggerende objecten en objecten creëren die automatisch de event-triggerende functie emit() erven.

In het volgende voorbeeld nemen we de events-module op via de require()-functie en slaan deze op als EventEmitter-klasse in een constante. Vervolgens gebruiken we extends om een eigen event-triggerende klasse myEmitter af te leiden uit de EventEmitter-klasse. Het eigenlijke event-object wordt aangemaakt met de operator new.


const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('myEvent', function(a, b) {
  console.log(a + b + a);
});
myEmitter.emit('myEvent', 1, 'and');

Om objecten de mogelijkheid te geven om op events te reageren, biedt de EventEmitter-klasse de functie on(). Deze accepteert twee parameters: de eerste parameter bevat de naam van het event waarop moet worden gelet en registreert het object voor het betreffende event. De tweede parameter geeft aan hoe op de gebeurtenis moet worden gereageerd. Hiervoor wordt er aan objecten met de tweede parameter een zogenaamde event-handler ‘gehangen’, een callback-functie die bij elk gewenst event wordt opgeroepen.

Als het event-object events moet activeren, dan wordt de functie emit() gebruikt. De emit-functie verwacht een parameter met de naam van het event. Extra optionele parameters kunnen gegevens over het event bevatten die aan de event-handler-functie worden doorgegeven.


Streams

Een stream is een abstractie van een datareeks die Node.js in staat stelt om grote hoeveelheden data efficiënt te verwerken. Conceptueel kunnen streams worden vergeleken met arrays. Maar waar arrays alle te verwerken data tegelijkertijd opslaan als waarden in het werkgeheugen, worden ze bij streams stuk voor stuk verwerkt, zodat steeds slechts een deel van de data in het werkgeheugen geladen hoeft te worden.

NBArrays zijn gegevenstypen waarin - in tegenstelling tot eenvoudige objecten - meerdere waarden afzonderlijk en in een vaste volgorde kunnen worden opgeslagen.
Technisch gezien worden arrays gerealiseerd als aaneengesloten, lineair adresseerbare celbereiken in het werkgeheugen.

In plaats van in ruimtelijke volgorde zoals bij arrays, worden gegevens bij streams in chronologische volgorde opgeslagen. Met dit type dataverwerking kan Node.js datavolumes verwerken die aanzienlijk groter zijns dan het beschikbare geheugen. Een ander voordeel is de reductie van de latentietijd. Om toegang te krijgen tot data van een array moet een programma wachten tot het volledig geladen is. Streams daarentegen stellen continu data beschikbaar zodra de eerste bytes zijn aangekomen en laden de rest op de achtergrond.

Het concept van de stream is gebaseerd op de interprocescommunicatie in de UNIX-shell. Onder UNIX worden streams via de pipe (l) in de shell geïmplementeerd. Hierdoor kun je data van het ene proces naar het andere overbrengen. Ook Node.js biedt met de basismodule stream een API voor gegevensverwerking via stream. Gebruikers beschikken over twee basistypen streams: readable streams en writable streams. Op basis hiervan kunnen daarnaast zogenaamde duplex en transform streams worden gerealiseerd, die een combinatie van beide basisstreams vormen.

Basisstreams Gecombineerde streams
Readable streams Writable streams Duplex streams Transform streams
Leesbare streams, die worden toegepast om data uit een databron te lezen Beschrijfbare streams, waarmee gegevens naar een bepaalde databron kunnen worden geschreven. Combinatie van een readable stream en een writable stream
Beide streams zijn onafhankelijk van elkaar
Combinatie van een readable stream en een writable stream
Beide streams zijn afhankelijk van elkaar
Transform streams maken wijziging en transformatie van data mogelijk

Voor alle streamtypes biedt Node.js een basisklasse via de stream module, die eigenschappen erft van de EventEmitter klasse. Elke stream is daarom ook een EventEmitter. In de code worden streams gerealiseerd als objecten die events kunnen activeren. Tot de belangrijkste events voor het omgaan met streams behoren data, end, error en finish events.

Event Beschrijving
data data events worden altijd geactiveerd wanneer een readable stream een gegevensblok levert. Doorgaans wordt de gegevensstroom via de functie .pipe() geactiveerd.
end end events worden geactiveerd via readable streams wanneer het laatste gegevensblok is aangekomen en er geen gegevens meer over zijn die door de stream kunnen worden verstrekt.
error error-events worden geactiveerd via readable streams en writable streams als er een fout is opgetreden tijdens het lezen of schrijven van de gegevens.
finish finish-events worden geactiveerd via writable streams zodra alle gegevens door het onderliggende systeem zijn verwerkt.

Hierna wordt de verwerking van streams geïllustreerd aan de hand van beide basisstreamtypes.


Readable streams

In een programma worden readable streams gebruikt om data uit een bron uit te lezen. Mogelijke databronnen voor dit streamtype zijn het bestandssysteem, netwerkeindpunten of in-memory datastructuren.

Zoals alle streams zijn ook readable streams in de JavaScript code als objecten weergegeven die als eerste moeten worden aangemaakt. Een functie om readable streams te maken, wordt bijvoorbeeld geboden door de fs module, die Node.js toestaat om met het bestandssysteem te interageren. In het voorbeeld laten we in drie stappen zien, hoe je readable streams gebruikt om data uit een tekstbestand te lezen en in de vorm van een string naar de terminal te schrijven.

1. Invoerbestand aanmaken: maak een tekstbestand (.txt) input met een willekeurige inhoud en plaats dit in een map naar keuze.

2. Toepassingscode schrijven: maak een JavaScript-toepassing op basis van het volgende voorbeeld en sla deze op onder read-stream.jx in dezelfde map als input.


const fs = require('fs');
const MyReadableStream = fs.createReadStream('input.txt', {encoding: 'utf8' });
MyReadableStream.on('data' , data => {
   console.log(data);
});
MyReadableStream.on('error', err => {
   console.log(err.stack);
});
MyReadableStream.on('end', () => {
   console.log('--- The End ---');
});

Gebruik eerst de functie require() om de fs-module te laden. Deze biedt de functie fs.createReadStream(), om alleen-lezen streams te maken voor het bestandssysteem. De functie fs.createReadStream() verwacht dat de eerste parameter het pad is naar het bestand dat je wilt lezen. Een tweede parameter met opties is optioneel. Meestal wordt een encoding gebruikt.

Voor het huidige voorbeeld maken we een stream met de naam MyReadableStream voor het bestand input.txt en gebruiken we een utf8- codering. De functie fs.createReadStream() heeft twee taken: enerzijds zorgt het ervoor dat de gegevens worden ingelezen in blokken van het bestand in de stream als onderdeel van het programmaverloop. Anderzijds specificeert het de codering waarin de leesgegevens in de stream moeten worden opgeslagen. Als de codering weggelaten wordt, dan laadt fs.createReadStream() uitsluitend binaire data in de stream.

De gegevens worden blok voor blok gestreamd. Telkens wanneer de uitleesgegevens een vooraf door Node.js gedefinieerde blokgrootte bereiken, activeert de stream MyReadableStream een data event. Je reageert op deze event door een callback toe te voegen met de on-functie. In het huidige voorbeeld gebruiken we de functie console.log(). De uitgelezen gegevensblokken worden op de terminal uitgevoerd.

Naast data events kunnen readable streams error-en end-events activeren waarop je ook moet reageren. Het error-event wordt bij readable streams geactiveerd wanneer er leesfouten optreden. Het is aan te bevelen om een console.log functie toe te voegen via on(), die de foutmelding op de terminal weergeeft. Een end-event treedt op zodra de databron volledig uitgelezen is en de readable stream geen verdere gegevensblokken kan leveren. Reageer op het end-event ook met een console.log()-functie, om een statusmelding op de terminal te krijgen.

3. Toepassingscode uitvoeren: open de Windows invoerbevestiging, navigeer naar de geselecteerde directory en start read-stream.js met het volgende commando:


node read-stream.js

Het programma read-stream.js moet nu de inhoud van het door jou aangemaakte bestand in put.txtin tekstvorm op de terminal uitvoeren - gevolgd door de statusmelding dat de stream is beëindigd (--- The End ---).

Node.js toepassing read-stream.js
De toepassing read-stream.js creëert een readable stream op het tekstbestand input.txt.

Writable streams


Writable streams ontvangen data bloksgewijs en bieden je de mogelijkheid deze naar een willekeurige bestemming te schrijven – bijvoorbeeld een bestand of een andere stream. Net als readable streams zijn ook writable streams afgeleid van de EventEmitter klasse en daarom in staat om events te activeren. Deze spelen echter een ondergeschikte rol bij de omgang met writable streams.

Centrale elementen van dit type stream zijn in plaats daarvan de functies write() en end(), die in elk geval geïmplementeerd moeten zijn. De functionaliteit van de writable stream wordt geïllustreerd door een voorbeeld van een eenvoudige bestandsbewerking waarin we tekstelementen uit de programmacode van een JavaScript-toepassing naar een tekstbestand schrijven met behulp van een stream.

1. Toepassingscode schrijven: maak een JavaScript-toepassing op basis van het volgende voorbeeld en sla deze op onder write-stream.jx in een willekeurige directory.


const fs = require('fs');
const MyWritableStream = fs.createWriteStream('output.txt', { defaultEncoding: 'utf8' });
MyWritableStream.write('Hello');
MyWritableStream.write('World!\n');

MyWritableStream.end(); 

MyWritableStream.on('finish', function() {
    console.log("Write completed.");
});
MyWritableStream.on('error', function(err) {
   console.log(err.stack);
});

Net als readable streams kunnen writable streams ook met de module fs worden aangemaakt, die je in je toepassing laadt via de functie require(). Gebruik daarvoor de functie fs.createWriteStream(). Deze functie verwacht als eerste parameter ook een pad- of bestandsnaam die aangeeft waar de streamingdata moeten worden opgeslagen.

Als het bestand output.txt nog niet bestaat, dan wordt het tijdens de uitvoering van het programma automatisch aangemaakt. Anders overschrijft de writable stream een bestaand bestand met dezelfde naam. Stel ook de optionele coderingsparameter voor writable streams in. Anders dan bij readable stream wordt daarvoor het keyword defaultEncoding gebruikt.

Zodra de writable stream is aangemaakt op het output.txt bestand, kun je de functie write() gebruiken om gegevens bloksgewijs naar de stream te schrijven. Zolang de writable stream is geopend, kan de write functie een willekeurig aantal keren worden opgeroepen. Hiermee kun je gegevens in delen naar de stream schrijven. Als je geen verdere gegevens naar de stream wilt schrijven, dan sluit je deze af met de end functie.

Writable streams veroorzaken ook gebeurtenissen. In de voorbeeldtoepassing reageren we op het finish-en het error-event. Het finish-event wordt altijd geactiveerd wanneer alle streaming gegevens met succes naar het opslagdoel zijn geschreven. Wil je in dit geval een statusmelding krijgen (bijv. write completed), voeg dan een bijbehorende console.log() toe aan het finish-event via on().

Net als readable streams, genereren writable streams ook error-events als er een fout optreedt tijdens het schrijfproces. Voeg een console.log-functie toe aan dit event om de foutmelding als terminaluitvoer te ontvangen.

2. Toepassingscode uitvoeren: open de Windows opdrachtprompt, navigeer naar de geselecteerde directory en start write-stream.js met het volgende commando:


node write-stream.js

Als het programma foutloos is uitgevoerd, dan krijg je de uitvoer ‘write completed’. In je bestandssysteem is het tekstbestand output.txt gecreëerd met de inhoud ‘Hello World!’.

Node.js toepassing write-stream.js
De toepassing write-stream.js genereert een writable stream waarmee je data via write() naar het bestand output.txt kunt schrijven.

ConclusieDe event-driven, non-blocking architectuur van Node.js is gebaseerd op concepten als callbacks, events en streams. Deze bieden het voordeel dat toepassingen nooit inactief op resultaat hoeven te wachten. Zo is het bijvoorbeeld mogelijk om verschillende databasequery’s tegelijkertijd uit te voeren zonder het programmaverloop te stoppen.
Ook de opbouw van een website, waarvoor verschillende externe query's nodig zijn, kan met Node.js veel sneller worden gemaakt dan met een synchroon bewerkingsproces.


Databasebewerkingen met MySQL en MongoDB

Een centrale functionaliteit van server-kant toepassingen is de interactie met databases. Er zijn modules van derden beschikbaar voor Node.js waarmee je databases kunt openen en databasetransacties kunt uitvoeren.

Welke databasemanagementsystemen worden gebruikt is een kwestie van smaak. De meeste Node.js-gebruikers vertrouwen echter op MongoDB - en niet zonder reden. MongoDB is een documentgerichte, niet-relatieve NoSQL-database. De cross-platform software is geschreven in C++ en slaat data schematisch op als BSON-objecten, die in de terminologie van MongoDB documenten worden genoemd. Het BSON- (Binary JSON) formaat is gebaseerd op het JSON-formaat en voorziet MongoDB van een native JavaScript compatibiliteit. Voor de gebruikers betekent dit dat de interactie met de database net als de programmering door Node.js toepassingen in JavaScript kan plaatsvinden. Een extra databasetaal zoals SQL (Structured Query Language) is niet nodig.

NB JavaScript wordt vaak een full-stack taal genoemd. De reden hiervoor is dat de scripttaal op alle webontwikkelingsniveaus kan worden gebruikt:
frontend (browser-side), backend (op de webserver) en in samenhang met JavaScript-compatibele infrastructuurdiensten (bijv. databases).

MongoDB behoort tot de groep NoSQL-databases. De afkorting ‘NoSQL’ staat – anders dan je zou verwachten – voor ‘Not only SQL’ en identificeert databases die een niet-relationele benadering volgen. In tegenstelling tot relationele databases zoals MySQL of MariaDB gebruiken deze geen vast tabelschema. Dat betekent: Voor tabellen in niet-relationele databases hoef je niet te specificeren welke kolommen er bestaan of welke gegevenstypen in deze kolommen kunnen worden opgeslagen. In plaats daarvan worden de gegevens opgeslagen in tabellen zonder structurele toewijzing. Een dergelijke databasearchitectuur is bijzonder geschikt voor toepassingen waarbij grote hoeveelheden data in korte tijd moeten worden verwerkt.

Het gebruik van MongoDB biedt vele voordelen. Bij Node.js ben je echter niet beperkt tot dit databasebeheersysteem. Als je webproject een SQL-gebaseerde relationele database nodig heeft in plaats van een NoSQL database, dan kun je deze eenvoudig integreren als module.

In het volgende gedeelte van onze Node.js-tutorial wordt de omgang met databases geïllustreerd aan de hand van het voorbeeld van MongoDB en de meest gebruikte relationele database MySQL.


Databasetaken met MongoDB

Om toegang te krijgen tot MongoDB met een Node.js-toepassing, heb je een database driver nodig. De officiële MongoDB module voor Node.js is beschikbaar via npm en kan geïnstalleerd worden zoals hieronder wordt beschreven:


npm install mongodb

Hieronder laten we je zien hoe je toegang krijgt tot MongoDB via een Node.js-toepassing en hoe je databasebewerkingen kunt uitvoeren binnen het programmaverloop.

NB Om het codevoorbeeld uit te proberen, moet je een database aanleveren:
In onze MongoDB-tutorial kun je te weten komen hoe je dit kunt doen.


MongoDB aanspreken


Om je toepassing in staat te stellen om met de database te kunnen communiceren, moet je eerst een verbinding tot stand brengen. Het volgende codevoorbeeld laat zien hoe je dit doet:


const mongodb = require ('mongodb');

const MongoClient = mongodb.MongoClient;
const connectionString = 'mongodb://localhost/myDatabase ';
MongoClient.connect(connectionString, {autoReconnect: true}, (err, database) => {
  if (err) {
    console.log('Failed to connect.', err.message);
    process.exit(1);
  }
  console.log('Connected!')
});

Eerst roep je de officiële mongodb-module voor Node.js op via de functie require() en slaat deze op in de constante mongodb.


const mongodb = require ('mongodb');

Voor de interactie met de database biedt de mongodb-module de klasse MongoClient. Je bewaart deze ook in een gelijknamige constante. Deze fungeert later als referentie en maakt interactie mogelijk met de MongoClient.


const MongoClient = mongodb.MongoClient;

Definieer dan een zogenaamde connectionString. Deze bevat alle informatie die nodig is om toegang te krijgen tot de database: gebruikersnaam en wachtwoord (optioneel), het IP-adres van de computer waarop de database zich bevindt (localhost in het huidige voorbeeld), via welke poort je de database wilt openen (standaard 27017 kan worden weggelaten) en een vrij te kiezen databasenaam.

In de volgende stap geef je de connectieString van de MongoClient. Daarvoor maak je hiermee contact via de eerder gedefinieerde constante en roep je de functie connect() op.


MongoClient.connect(connectionString, {autoReconnect: true}, (err, database) => {
  if (err) {
    console.log('Failed to connect.', err.message);
    process.exit(1);
  }
  console.log('Connected!')
});

In het huidige voorbeeld worden aan de MongoClient naast de connectionString nog twee andere parameters doorgegeven: het object autoReconnect: true, dat automatisch een nieuwe verbinding start wanneer een verbinding wordt verbroken, en een callback. Als er een fout optreedt, stuurt deze een melding naar de terminal en sluit het programma af. Als er geen fout optreedt, roept de callback de afsluitende console.log functie op, waardoor de verbindingsstatus ‘Connected!’ op de terminal verschijnt.

NB In de praktijk worden alle volgende codevoorbeelden
ingevoegd in de bovengenoemde callback.


Collections aanspreken


In de terminologie van MongoDB worden schematische tabellen in een database collection genoemd. De toegang tot een collectie wordt verleend door de gelijknamige functie collection() die door de mongodb-module wordt geboden.

In het volgende voorbeeld krijgen we toegang tot de collection users.


const users = database.collection('users')

LET OP! Wanneer je een collection aanspreekt die nog niet bestaat,
genereert MongoDB deze automatisch.


Documenten invoegen


Datasets worden in de terminologie van MongoDB-documenten genoemd en als BSON-objecten opgeslagen. Ga als volgt te werk om een document in een collection op te slaan.

Definieer eerst een document - bijv. het document user:


const user = {
   firstName: 'John',
   lastName: 'Doe'
};

Sla het document met behulp van de functie insertOne() op in de zojuist aangemaakte tabel.


users.insertOne(user, err => {
   if (err) {
    console.log('Failed to insert user.');
    process.exit(1);
  }

  console.log('Successfully inserted user!')
  database.close();
});

In het huidige voorbeeld slaan we het document user op in de eerder aangemaakte tabel users en voegen we een callback-functie toe. In geval van een fout geeft deze een overeenkomstige melding af en sluit het programma af. Als het document met succes in de collectie is opgenomen, krijgen we de statusmelding ‘Successfully inserted user!’ via een console.log-functie. De databaseverbinding wordt dan afgesloten met de close-functie.


Documenten opvragen


Database query's draaien in MongoDB via de find-functie. Hiermee kun je individuele documenten of alle documenten in een collection opvragen.

Met het volgende codevoorbeeld worden alle documenten van de collection users opgehaald en als array op de terminal uitgevoerd:


users.find().toArray((err, documents) => {
    if (err) {
      console.log('Failed to find users.');
      process.exit(1);
    }

    console.log(documents);
    database.close();
}); 

Wanneer je een bepaald document uit een collection wilt ophalen, gebruik je de functie find() in combinatie met een zoekparameter.


user.find({ 
    firstName: 'John' 
}).toArray((err, documents) => {
  if (err) {
      console.log('Failed to find user.');
      process.exit(1);
    }

    console.log(documents);
    database.close();
});

In het huidige voorbeeld worden alleen de documenten van de collectie users weergegeven die een veldnaam firstName met de inhoud John bevatten.

Als je daarentegen alleen het eerste document wilt opvragen, waarop het zoekcriterium van toepassing is, dan gebruik je find() in plaats van de functie findOne().


users.findOne({firstName: 'John'}, (err, document) => {…})


Objecten verwijderen


Om documenten te kunnen verwijderen, biedt de module mongodb de functies deleteMany() en deleteOne(). DeleteOne() verwijdert alleen het eerste document dat aan het zoekcriterium voldoet, terwijl deleteMany() alle documenten verwijdert die aan het zoekcriterium voldoen.

De functies deleteMany() en deleteOne() verwachten de parameter filter.


const filter = {
   firstName: 'John'
   lastName: 'Doe'
};

Gebruik de deleteOne-functie zoals in het volgende voorbeeld, om het eerste document te verwijderen dat aan het zoekcriterium voldoet:


users.deleteOne(filter, err => {
  if (err) {
    console.log('Failed to insert user.');
    process.exit(1);
  }

  console.log('Successfully deleted one document!');
  database.close();
});

Wanneer je alle documenten die voldoen aan de in de filter constante gedefinieerde criteria wilt verwijderen, dan vervang je de deleteOne-functie door deleteMany():


collection.deleteMany(filter, err => {
  if (err) {
    console.log('Failed to insert user.');
    process.exit(1);
  }

  console.log('Successfully deleted documents!');
  database.close();
});


Documenten bijwerken


Wil je documenten bijwerken, dan gebruik je die functies updateOne() en updateMany(). Deze verwachten twee parameters: een filter-parameter om te definiëren welke documenten je wilt bijwerken en een update-parameter om te definiëren waarmee je de geselecteerde documenten wilt overschrijven. UpdateOne() overschrijft alleen het eerste document dat aan het zoekcriterium voldoet, terwijl updateMany() alle documenten overschrijft die aan het zoekcriterium voldoen.

Als je een document wilt bijwerken dan definieer je eerst de parameters filter en update.


const filter = {
   firstName: 'John',
   lastName: 'Doe'
};
const update = {
   firstName: 'John Thomas',
   lastName: 'Doe'
};
Vervolgens roep je de functie updateOne() aan. 
users.updateOne(filter, update, err => {
  if (err) {
    console.log('Failed to update user.');
    process.exit(1);
  }

  console.log('Successfully updated!');
  database.close();
});

NB De parameters filter en update kunnen indien nodig ook inline
en dus binnen de functie updateOne() worden gedefinieerd.

In het huidige voorbeeld vervangen we het eerste document met de velden firstName: 'John' en lastName: 'Doe' door een document met de velden firstName: 'John Thomas' en lastName: 'Doe'. Let op dat documenten altijd volledig worden overgeschreven. De parameter update geeft aan hoe het geselecteerde document eruit moet zien na het bijwerken. Wil je alleen afzonderlijke gegevensvelden van een document vervangen, voer dan een gedeeltelijke update uit. Gebruik daarvoor de update operator $set.


const filter = {
   firstName: 'John',
   lastName: 'Doe'
};
const update = {
  $set: { firstName: 'John Thomas' }
};

users.updateOne(filter, update, err => {
  if (err) {
    console.log('Failed to update user.');
    process.exit(1);
  }

  console.log('Successfully updated!');
  database.close();
});

In het huidige voorbeeld geeft de operator $set aan dat in het eerste document waarop het filtercriterium van toepassing is, alleen de waarde in het gegevensveld firstname door John Thomas moet worden vervangen.

Wil je meerdere datasets bijwerken met updateMany() dan moet je altijd een update operator gebruiken. Voor meer informatie over de update en query operators van MongoDB kun je de documentatie van het Database Management Systeem bekijken.


Database-operaties met MySQL

Om een Node.js-toepassing te koppelen met een MySQL-database, heb je net als bij MongoDB een database driver nodig. Deze stellen verschillende modules van derden beschikbaar, die gratis verkrijgbaar zijn via npm. Er is geen officiële MySQL-module voor Node.js. Wij raden de module mysql van Douglas Wilson aan.

De installatie vindt plaats volgens dit schema:


npm install mysql –save


MySQL aanspreken


Het volgende codevoorbeeld laat zien hoe je de module mysql gebruikt om een databaseverbinding met een Node.js-toepassing tot stand te brengen.


const mysql = require('mysql');

const connection = mysql.createConnection({
  host: "localhost",
  user: "yourusername",
  password: "yourpassword"
});

connection.connect(function(err) {
  if (err) {
    console.log('Failed to connect.', err.message);
    process.exit(1);
  }
  console.log('Connected!')
});

Laad eerst de module mysql in je toepassing en sla deze op in de constante mysql. Let erop dat deze vooraf al via npm op je systeem moet worden geïnstalleerd.


const mysql = require('mysql');

Gebruik vervolgens de functie createConnection() in de mysql-module om de verbindingsgegevens naar de database te definiëren. De volgende informatie is vereist: de naam van de server (of het IP-adres) waarop de database zich bevindt, je gebruikersnaam, je wachtwoord en de naam van de database tot welke je toegang wilt krijgen.


const connection = mysql.createConnection({
  host: "localhost",
  user: "yourusername",
  password: "yourpassword",
  database: "yourdb"
});

Om later toegang te kunnen krijgen tot de functie createConnection() en de als parameters opgeslagen verbindingsgegevens, sla je deze op in de constante connection.

De eigenlijke verbindingsopbouw wordt in regel 9 uitgevoerd. Daarvoor spreek je de constante connection aan en roep je de door de mysql-module voorziene functie connect() aan. Je voegt een callback functie toe die een statusmelding naar de terminal stuurt.


connection.connect(function(err) {
  if (err) {
    console.log('Failed to connect.', err.message);
    process.exit(1);
  }
  console.log('Connected!')
});

In geval van een fout krijg je een foutmelding als terminaluitvoer. Vervolgens wordt het programma afgesloten. Als de verbinding tot stand is gebracht, dan geeft de toepassing de status ‘Connected!’ weer.


Database-operaties uitvoeren


In de context van database-operaties spreek je ook de constante connection aan, die je in het vorige voorbeeld hebt gedefinieerd, om toegang te krijgen tot createConnection() en de parameters voor de totstandbrenging van de verbinding. Alle database-taken worden uitgevoerd met de functie query(). Deze verwacht als eerste parameter een SQL-statement, die aangeeft welke taak moet worden uitgevoerd en op welke data deze betrekking heeft. De feitelijke interactie met de database vindt plaats in de databasetaal SQL. De SQL-statement kan inline gedefinieerd worden of in een variabele/constante worden opgeslagen.

NB
Hoewel MongoDB asynchroon loopt – wat kenmerkend is voor Node.js – werkt MySQL synchroon.
Vergelijk:

connection.connect();

connection.query(…);


en

connection.connect(xy => {xy.query()})


Dat is niet te wijten aan de database - het is een beslissing van de moduleontwikkelaars.

Inline:


connection.query('SELECT column FROM table', function(error, results, fields) {
    if (err) {
    console.log('Failed to select column Form table.', err.message);
    process.exit(1);
  }

  console.log('The solution is: ', results[0].column;
});

Uitbestede SQL-statement:


const sql = "SELECT-kolom FROM-tabel";
connection.query(sql, function(err, results, fields) {
  if (err) {
    console.log('Failed to connect.', err.message);
    process.exit(1);
  }
  console.log('The solution is: ', results[0].column;
});

De tweede parameter van de query-functie is een callback met drie argumenten:

  • err slaat fouten op die optreden tijdens de uitvoering van de SQL-statement.
  • results bevat de resultaten van de query.
  • fields bevat gedetailleerde informatie over de betreffende gegevensvelden.

Voor gedetailleerde informatie over het opzetten van een MySQL-database en het formuleren van SQL-statements om databasebewerkingen uit te voeren, zie onze MySQL tutorial voor beginners.