[[ maas / 2021-10-06]]
// Node //
006
Node.js

Az alábbiakban rengeteg jegyzetet felhalmoztam a témában, amiket szándékomban áll jelentősen rendszerezni a közeljövőben.

Bevezetés

A Node.js egy nyíltforrású, többplatformos, szerveroldali JavaScript futtatókörnyezet (JavaScript runtime environment) a Google Chrome V8 JavaScript-motoron alapulva. Leginkább webes applikációk, dinamikus weboldalak fejlesztésére használják, de akár szerveroldali feldolgozószkripek készítésére is kiválóan alkalmas.

Segítségével megvalósítható a "JavaScript everywhere" paradigmája, azaz, hogy a fejlesztők mind a szerveroldali (back-end), mind a kliensoldali (front-end) programkódokat egyazon programozási nyelvvel alkothassák meg.

A fejlesztők többnyire a futtatókörnyezet eseményvezérelt arhitektúrájának lehetőségeit aknázzák ki, amellyel aszinkron I/O-ra képes webes alkalmazások készíthetőek. Ennek a lényege röviden összefoglalva annyi, hogy a programkód egyes blokkjai akár egyszerre is legenerálhatják a kért weboldalt, szolgáltatják a kliens oldal számára az adatokat egy-egy grafikai elem megrajzolásához, ezáltal valódi real-time alkalmazásokat fejleszthetünk vele.

A Node.js ötlete és fejlesztésének kezdeti szakaszai a kaliforniai születésű Ryan Dahl nevéhez fűződik, aki felismerte az Apache HTTP Server többszálú kódfuttatással kapcsolatos korlátoltságát, majd 2009-ben bemutatott egy működőképes szerveroldali JavaScript futtatókörnyezetet, amely ekkor még csak Linuxon és Mac OS X-en volt elérhető. Ekkor indult el a közösségi fejlesztés Dahl vezetésével. 2010-ben Isaac Z. Schlueter már előállt egy igazán használható csomagkezelővel a Node.js-re alapuló modulok kényelmes karbantartásához, amely nemes egyszerűséggel a Node Pakcage Manager (NPM) nevet kapta. Dahl már 2010-ben a következő PHP-ként vízionálta a Node.js-t, és már 2011-ben komoly cégek álltak át Node.js alapokra. 2015-ben egy ideig a Node.js-el párhuzamosan annak forkja, az io.js projekt is felbukkant meggyorsítva ezzel a fejlesztéseket, majd a két projekt újra egybeolvadt, és végeredményben a Node.js 4.0-s verziója tekinthető a modern Node.js 1.0-jának. Innentől kezdve a munka rohamos fejlődésnek indult és a futtatókörnyezet jelentős népszerűségre tett szert a webes applikációk fejlesztőinek körében. A jelenleg is Node.js technológiára épülő nagyobb cégekre néhány példa: az eBay, a PayPal és az Uber weboldala, a Netflix applikációja, a LinkedIn mobilapp mögötti webszerver.

A fejlesztési környezet kialakítása, alapfogalmak

2.1 A Visual Studio Code

A Microsoft VSCode-ja hibátlanul támogatja a JavaScript és azon túl is a Node.js programkódok szintaksziskiemelését, valamint több egészen hasznos segédeszközt is kínál. A Prettier és ESLint kiegészítők telepítésével már a kód szerkesztése során és mentéskor automatikusan javíttathatjuk a kódot, a behúzásokat, a programblokkokat. Bár mindez kellemetlen meglepetéseket is okozhat, így ezeket a kiegészítőket egyelőre nyugodt szívvel még nem ajánlanám, a VSCode-ot magát viszont annál inkább.

A program bővebb ismertetése a /learning/it/prog/vscode oldalon olvasható.

2.2 A fejlesztési környezet

A Node.js és annak teljes dokumentációja a nodejs.org oldalon érhető el.

Az épp aktuális (Current - Latest Features) Node.js letöltéséhez látogassuk meg a nodejs.org/en/download/current oldalt. Itt akár telepítőként is letölthető a csomag Windows és macOS rendszerekhez, valamint Linuxra a disztribúciónak megfelelő csomagkezelővel (zypper, yum, dnf, apt-get, urpmi) szintén vesződségtől mentesen telepíthetünk. Én azonban kifejezetten örvendetesnek tartom, hogy telepítést sem igénylő csomagok formájában is beszerezhető a Node.js, így a továbbiakban ezt a telepítési formát részletezném. Ehhez Windows-hoz a "Windows Binary (.zip) | 64-bit", Linuxhoz a "Linux Binaries (x64) | 64-bit", AIX-hoz az "AIX on Power Systems | 64-bit" linkeken található fájlokat kell letöltenünk, esetlegesen a kívánt gépre felmásolnunk.

2.2.1 Telepítés Windows-on

Miután letöltöttük a legfrissebb x86_64-es verziót egyetlen .zip fájlban, azt egyszerűen kibonthatunk (pl. D:\bin\Node.js\ alá). Ezek után már csak be kell állítanunk az alábbi %PATH% és %NODE_MODULES% környezeti változókat (maradva a D:\bin\Node.js\ útvonalnál):

> setx path "C:\Program Files;C:\Windows;C:\Windows\System32;D:\bin\Node.js"
> setx NODE_PATH d:\bin\Node.js\node_modules

Ha a környezeti változókkal megvagyunk, akár a Command Promptban (cmd), akár közvetlenül a Visual Studio Code "Terminal" ablakában kipróbálhatjuk, hogy működik-e a Node:

> node -v
v15.2.0
> npm -v
7.0.8

2.2.2 Telepítés Linuxon, AIX-en

Unix-szerű rendszereken az /opt/Node.js/ könyvtár egy potenciálisan jó és ajánlott telepítési hely. Ha a csomagot kibontottuk ide, a jogosultságokról is gondoskodtunk, már csak a $PATH és $NODE_PATH környezeti változókról kell gondoskodnunk.

Linuxon így néz ki a két fontos változó beállítása felhasználónk .bash_profile fájljában:

export PATH=$PATH:/opt/Node.js/bin
export NODE_PATH=/opt/Node.js/lib/node_modules

Egy gyors terminál újraindítás vagy ki-/belépés után ugyanúgy a node -v és npm -v parancsokkal ellenőrizhetjük a program működőképességét.

2.3 Alapfogalmak

Mindenek előtt meg kell ismerkednünk annak a módjával, ahogy a tanulás során a programjainkat működésre bírjuk. Ez lehetőleg kényelmes, gyors és áttekinthető kell legyen, minimális kattintgatással.

2.3.1 Variációk a "Hello világ!" programra

2.3.1.3 A Node.js inline parancsértelmezővel

A Visual Studio Code "Terminal" mezőjébe gépeljük be a node parancsot, amivel egy parancsértelmezőbe kerülünk. Itt gépeljük be a > prompt után a már ismert parancssort, majd üssünk [Enter]-t.

> node
Welcome to Node.js v13.0.1.
Type ".help" for more information.
> console.log("Hello világ!");
> Hello világ!
undefined
> .exit

2.3.1.4 A Node.js-ből meghívott .js állománnyal

Készítsünk el a fenti, egyetlen sorból álló test.js nevű állományt, majd hívjuk meg azt a node parancs paramétereként (feltételezve, hogy nem léptünk át másik könyvtárba):

> node test
Hello világ!

Az állomány teljes nevére is hivatkozhatunk, de a Node.js számos esetben megengedi a .js kiterjesztés elhagyását.

Node.js alapok

Az aktuális könyvtár és az aktuális fájl, annak teljes elérési útjának megkaparintása:

console.log(**dirname);
console.log(**filename);

A fájl nevének feldolgozhatóságához már igénybe kell vennünk a path modult:

const PATH = require("path");
 
console.log(`The file name is ${PATH.basename(__filename)}`);
const PATH = require("path");
const \_DIR = "sajt";
const MOREPATH = PATH.join(\_DIR, "user", "files");
 
console.log(MOREPATH);

Az épp futó process ID-jának, a process alatt futó Node verziójának és a process-nek átadott argumentumok tömbjének kiíratása:

console.log(process.pid)
console.log(process.version.node)
console.log(process.argv)

Paraméterek átadásának a legegyszerűbb módja:

const [, , firstName, lastName] = process.argv
 
console.log(`Your name is ${firstName} ${lastName}`)
> node process Jakab Gipsz
Your name is Jakab Gipsz

Az előbbinél jóval kifinomultabb megoldás:

const grab = (flag) => {
  let indexAfterFlag = process.argv.indexOf(flag) + 1
  return process.argv[indexAfterFlag]
}
 
const greeting = grab('--greeting')
const user = grab('--user')
 
console.log(`${greeting} ${user}`)
> node test --greeting Hello --user Ede
Hello Ede

A console objektum helyett a process használata a konzolra történő kiíratáshoz:

process.stdout.write('Hello világ!')

További Node-os dolgok:

process.stdout.write()
process.stdin.on()
process.on()
process.exit()
process.stdout.clearLine()
process.cursorTo()

Stdin egyszerű kezelése:

process.stdin.on('data', data => {
process.stdout.write(\n\n ${data.toString().trim()}')
});

Az időzítő késleltetése 4 mp-el:

const waitTime = 4000
const timerFinished = () => console.log('Finished')
setTimeout(timerFinished, waitTime)

Node modulok

A require függvény a common JS module pattern része.

A readline a felhasználói interakciót elősegítő függvénygyűjtemény. A createInterface használata:

const readline = require('readline')
const rl = readline.createInterface({
  // változók helye
})

Az alábbiakban egy valós példát láthatunk a readline egyszerű használatára:

const readline = require('readline')
 
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
})
 
rl.question('How do you like Node? ', (answer) => {
  console.log(`Your answer: ${answer}`)
})

Kérdések és válaszok a readline-al:

const readline = require('readline')
 
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
})
 
const questions = ['What is your name? ', 'Where do you live? ', 'What are you going to do with node js? ']
 
const collectAnswers = (questions, done) => {
  const answers = []
  const [firstQuestion] = questions
 
  const questionAnswered = (answer) => {
    answers.push(answer)
    if (answers.length < questions.length) {
      rl.question(questions[answers.length], questionAnswered)
    } else {
      done(answers)
    }
  }
 
  rl.question(firstQuestion, questionAnswered)
}
 
collectAnswers(questions, (answers) => {
  console.log('Thank you for your answers. ')
  console.log(answers)
  process.exit()
})

Saját modulok

// A saját modulba, itt a myModule.js fájlba:
module.exports = 'Jakab'
// A .js fájlba, ami a saját modult felhasználja:
const name = require('./myModule')

Egy jóval összetettebb példa:

let count = 0
 
const inc = () => ++count
const dec = () => --count
 
const getCount = () => count
 
module.exports = {
  inc,
  dec,
  getCount,
}

Felhasználása (szájbarágósan):

const counter = require('./myModule')
 
counter.inc()
counter.inc()
counter.inc()
counter.dec()
 
console.log(counter.getCount())

...egyszerűsítve:

const { inc, dec, getCount } = require('./myModule')
 
inc()
inc()
inc()
dec()
 
console.log(getCount())

A példaprogram 3x növeli, majd egyszer csökkenti count értékét:

> node test
2

EventEmitter

A Node.js mát alapban tartalmazza ezt a hasznos ezközt, amivel a Node.js megvalósítja a "pub/sub design pattern"-t, ami a saját event-ek készítését teszi lehetővé, valamint listener-eket és handler-eket ad ezekhez.

const events = require('events')
 
const emitter = new events.EventEmitter()
 
emitter.on('customEvent', (message, user) => {
  console.log(`${user}: ${message}`)
})
 
emitter.emit('customEvent', 'Hello World', 'Computer')
emitter.emit('customEvent', "That's pretty cool huh?", 'Alex')
> node test
Computer: Hello World
Alex: That's pretty cool huh?

Felhasználói begépelés figyelése az EventEmitter-el:

const events = require('events')
 
const emitter = new events.EventEmitter()
 
emitter.on('customEvent', (message, user) => {
  console.log(`${user}: ${message}`)
})
 
process.stdin.on('data', (data) => {
  const INPUT = data.toString().trim()
  if (INPUT === 'exit') {
    emitter.emit('customeEvent', 'Goodbye!', 'process')
    process.exit()
  }
  emitter.emit('customEvent', INPUT, 'terminal')
})
> node test
hello world
terminal: hello world
node js is neato
terminal: node js is neato
exit

Az fs modul

Az fs modul fájlok és könyvtárak készítésére, módosítására, fájlok stream-elésére és jogosultságok kezelésére való.

  • Könyvtár fájljainak/könyvtárainak listázása:
const FS = require('fs')
const FILES = FS.readdirSync('..')
 
console.log(FILES)

Sync módban fut ez, azaz minden más addig blokkolva van.

Ugyanennek az async változata egy callback függvény készítését kívánja meg:

const FS = require('fs')
 
console.log('Fájlok olvasása elindult.')
FS.readdir('..', (ERR, FILES) => {
  if (ERR) {
    throw ERR
  }
  console.log('Kész.')
  console.log(FILES)
})
  • Fájlok tartalmának olvasása:
const FS = require('fs')
 
const TXT = FS.readFileSync('README.md', 'UTF-8')
 
console.log(TXT)

Ugyanennek async változata callback függvénnyel, hibakezeléssel:

const FS = require('fs')
 
FS.readFile('README.md', 'UTF-8', (ERR, TXT) => {
  if (ERR) {
    console.log(`Hiba: ${ERR.message}`)
    process.exit()
  }
  console.log('Kész.')
  console.log(TXT)
})

Bináris állomány olvasása:

const FS = require('fs')
 
FS.readFile('vscode_markup.png', (ERR, IMG) => {
  if (ERR) {
    console.log(`Hiba: ${ERR.message}`)
    process.exit()
  }
  console.log('Kész.')
  console.log(IMG)
})
  • Fájlok írása és bővítése:
const FS = require('fs')
 
const MD = `
 
# Ez egy új fájl
 
Most az FS.writeFile-al fogunk fájlba írni
`
 
FS.writeFile('./teszt.md', MD.trim(), (ERR) => {
  if (ERR) {
    throw ERR
  }
  console.log('A fájl elmentésre került.')
})
  • Könyvtár készítése:
const FS = require('fs')
 
FS.mkdir('storage-files', (ERR) => {
  if (ERR) {
    throw ERR
  }
  console.log('A könyvtár létrehozásra került.')
})

Amennyiben a könyvtár már létezik, hibára futunk. Ezt könnyen lekezelhetjük pl. így is, az .existsSync metódussal:

const FS = require('fs')
 
const DIR = 'storage-files'
 
if (FS.existsSync(DIR)) {
  console.log('A könyvtár már létezik.')
} else {
  FS.mkdir(DIR, (ERR) => {
    if (ERR) {
      throw ERR
    }
    console.log('A könyvtár létrehozásra került.')
  })
}
> node test
A könyvtár létrehozásra került.
> node test
A könyvtár már létezik.
  • Fájlokhoz való hozzáfűzés
const FS = require('fs')
 
FS.appendFile('./ujfajl', 'Valami adat', (ERR) => {
  if (ERR) {
    throw ERR
  }
  console.log('done')
})

Ezt egy JSON-ból való kiolvasással és a benne lévő objektum körbejárásával is jól szemléltethetjük:

// a programban hivatkozott colors.json tartalma
{
"sample": true,
"name": "color list",
"description": "A list of colors",
"colorList": [
{
"color": "red",
"hex": "#FF0000"
},
{
"color": "green",
"hex": "#00FF00"
},
{
"color": "blue",
"hex": "#0000FF"
}
]
}
const FS = require('fs');
const COLORDATA = require('./assets/colors.json');
 
COLORDATA.colorList.forEach(c => {
FS.appendFile('colors.md', `${c.color}: ${c.hex} \n`, ERR => {
if (ERR) {
throw ERR;
}
});
});
  • Fájlok átnevezése és törlése: A következő példaprogramban az átnevezés sync, majd async változatára, továbbá 4 mp-nyi várakozással egybekötve egy fájl törlésére láthatunk egy-egy megoldást:
const FS = require('fs')
 
FS.renameSync('./assets/colors.json', './assets/colorData.json')
 
FS.rename('./assets/notes.md', 'notes.md', (ERR) => {
  if (ERR) {
    throw ERR
  }
})
 
setTimeout(() => {
  FS.unlinkSync('./assets/alex.jpg')
}, 4000)
  • Könyvtárak átnevezése és törlése Egyszerű átnevezés sync módban:
const FS = require('fs')
 
FS.renameSync('./assets', './hozzavalok')

Nem üres könyvtár esetében az alábbi egyszerű program "ENOTEMPTY" hibára fut:

const FS = require('fs')
 
FS.rmdir('./hozzavalok', (ERR) => {
  if (ERR) {
    throw ERR
  }
  console.log('A könyvtár törlésre került.')
})

A nem üres könyvtár "problémájának" megoldása a korábban már megismert fájlok törlésére alkalmazható .unlinkSync metódussal:

const FS = require('fs')
 
FS.readdirSync('./hozzavalok').forEach((fileName) => {
  FS.unlinkSync(`./hozzavalok/${fileName}`)
})
 
FS.rmdir('./hozzavalok', (ERR) => {
  if (ERR) {
    throw ERR
  }
  console.log('A könyvtár törlésre került.')
})

Írható és olvasható adatfolyamok

(Readable and writable file streams)

console.log('Írj valamit!')
process.stdin.on('data', (data) => {
  console.log(`Most ${data.length - 1} karakternyi szöveget olvastam be.`)
})
> node test
Írj valamit!
Akármi
Most 8 karakternyi szöveget olvastam be.

Nagyobb kiterjedésű fájlok esetében az egész egyben történő beolvasása helyett gyakran sokkal érdemesebb a .createRedStream metódus felhasználásával "bit-by-bit and chunk-by-chunk" módjára beolvasni a tartalmat.

const FS = require('fs')
 
const READSTREAM = FS.createReadStream('../README.md', 'UTF-8')
 
READSTREAM.on('data', (data) => {
  console.log(`Most ${data.length - 1} karakternyi szöveget olvastam be.`)
})
> node test
Most 569 karakternyi szöveget olvastam be.

A data event megjelenésére a .once metódussal be is olvastathatjuk a fájl tartalmát egyben. Itt az end event-et is felhasználtuk a fájlbeolvasás befejezésének kiíratásához és még egy változót is segítségül hívtunk, hogy megtudjuk a fájl végleges hosszát:

const FS = require('fs')
 
const READSTREAM = FS.createReadStream('../README.md', 'UTF-8')
 
let fileTxt = ''
 
READSTREAM.on('data', (data) => {
  console.log(`Most ${data.length - 1} karakternyi szöveget olvastam be.`)
  fileTxt += data
})
 
READSTREAM.once('data', (data) => {
  console.log(data)
})
 
READSTREAM.on('end', () => {
  console.log('A fájl olvasása befejeződött.')
  console.log(`A fájl teljes hossza ${fileTxt.length - 1} karakter.`)
})

Node.js fejlesztőként gyakran van szükség filestream-ek használatára.

A filestream-ek írásakor tulajdonképpen ennek az egyszerű programnak:

process.stdout.write('Hello')
process.stdout.write('Világ!')

...az ilyenre történő továbbfejlesztéséről van szó:

const FS = require('fs')
const WRITESTREAM = FS.createWriteStream('./myFile.txt', 'UTF-8')
WRITESTREAM.write('Hello')
WRITESTREAM.write('Világ!')

Az olvasható stream-ek úgy lettek kitalálva, hogy írható stream-ekkel tudjanak együtt működni, így a process.stdin.on event kezeléssel a terminálról is olvashatunk, aminek következtében az eredmény ugyanúgy a myFile.txt-ben fog megjelenni a [Ctrl+C] megnyomása után:

const FS = require('fs')
const WRITESTREAM = FS.createWriteStream('./myFile.txt', 'UTF-8')
 
process.stdin.on('data', (data) => {
  WRITESTREAM.write(data)
})
> node test
Helló, szia, szevasz!
^C

A process.stdin.pipe(WRITESTREAM);-el is beleírhatjuk az adatot ugyanígy egy fájlba.

Egyik fájl másikba történő átmásolása pedig:

const FS = require('fs')
 
const WRITESTREAM = FS.createWriteStream('./myFile.txt', 'UTF-8')
const READSTREAM = FS.createReadStream('./lorem.txt', 'UTF-8')
 
READSTREAM.pipe(WRITESTREAM)

Külső programok meghívása az exec-el és a spawn-al

Az exec a szinkron processzek futtatására lett kitalálva, amik futnak, bezáródnak, aztán valami adattal térnek vissza.

Windows-on az "Intéző" meghívása egyből egy könyvtár megnyitásával:

const CP = require('child_process')
 
CP.exec('explorer file://d:/Downloads')

Hiba (standard error) kezelése:

const CP = require('child_process')
 
CP.exec('exploror', (err, data, stderr) => {
  console.log(stderr)
})

Akár egy másik Node.js app is elindítható az exec-el, vagy a spawn-al.

A spawn esetében aszinkron programok futását hajtjuk végre, így ezzel nem oldható meg, hogy a stdin-re várjunk. Helyette már előre definiálhatjuk a válaszokat. Így tesziünk a következő programmal is, ahol a korábban ismertetett 3 kérdést bekérdező programot hívjuk meg, aminek máris átadjuk a bemeneteket:

const CP = require('child_process')
const QUESTIONAPP = CP.spawn('node', ['readLine.js'])
 
QUESTIONAPP.stdin.write('Jakab \n')
QUESTIONAPP.stdin.write('Világvégén \n')
QUESTIONAPP.stdin.write('Világuralomra török \n')
 
QUESTIONAPP.stdout.on('data', (data) => {
  console.log(`a kérdező app-ból: ${data}`)
})
 
QUESTIONAPP.on('close', () => {
  console.log(`a kérdező app futása véget ért`)
})

NPM, Yarn

A Node.js-el együtt kapjuk a Node Package Manager-t (NPM-et), aminek a neve a Linux-ból jól ismert RPM-ből eredeztethető. Ez felelős azért, hogy a node_modules könyvtárban ott legyenek a gyári Node.js telepítésbe bele nem tartozó, közösségi fejlesztésű modulok, függőségek.

Néhány parancs, részletek majd később:

npm -v
npm list
npm list -g
npm install <modulnév>
npm install <modulnév> -g
npm install
npm install -g

Az NPM közismert hiányosságai:

  • lassú telepítés
  • nehezen behatárolható (nondeterministic) build-ek
  • biztonsági aggodalmak
  • a package.json függőségeiből adódó kavarás

Az NPM helyett ma már sokan a Yarn-t részesítik előnyben, aminek a fejlesztésében a Facebook és a Google is fontos szerepet vállalt.

A Yarn előnyei:

  • a yarn.lock fájlban átláthatóbbak a függőségek
  • párhuzamos build-ek (akár 50%-al gyorsabb)
  • hálózati megbízhatóság
  • deterministic
  • offline cache

CLI app építéseről jegyzetek

A Chalk, a Clear, a Figlet és a Commander modulokkal kezdődik a Musette CLI tool készítése (02-01-től 02-03-ig).

Az "inquirer" és a "minimist" volt használva a user-el való interakció, ami tök jó megoldásokat nyújt:

const inquirer = require('inquirer');
const minimist = require('minimist');
 
const files = require('./files');
 
module.exports = {
askGitHubCredentials: () => {
const questions = [
{
name: 'username',
type: 'input',
message: 'Enter your Github username or e-mail address:',
validate: function(value) {
if (value.length) {
return true;
} else {
return 'Please enter your GitHub username or e-mail address.';
...