[[ maas / 2021-03-23]]
// Node //
323
Express

Bevezetés

Az Express (amit gyakran Express.js-nek, vagy ExpressJS-nek is hívnak) egy szerver oldali (back end) applikáció fejlesztői keretrendszer Node.js-hez. Használata és terjesztése ingyenes, szabad szoftver az MIT licensze alatt. Elsősorban webes applikációkat és API-kat fejleszthetünk vele, és rendkívül sikeres életútja során viszonylag hamar az általános webes applikációs keretrendszerré vált Node.js alatti fejlesztések esetében.

A keretrendszer weboldala az expressjs.com. Jelenleg a 4.17.1-es verziónál tart, de már kipróbálható az 5.0 Alpha.

Az Expresshez egy Express Generator nevű kiegészítő is letölthető, ami egy általános használhatóságú könyvtár és fájlstruktúrát (ún. skeletont) és minden szükséges függőséget előkészít egy gyors kezdéshez. Ez nem csak fejlesztést segíti, de a tanuláshoz is nagyon jó. Az alábbiakban nem ezzel, hanem teljesen az alapoktól fogunk hozzá az Express megismeréséhez.

Fejlesztői környezet és webszerver villámgyorsan

2.1 Előkészületek

Miután meggyőződtünk róla, hogy telepítettük a Node.js-t (tehát a node -v és npm -v is kedvező kimeneteket adtak), készítsünk egy új, üres könyvtárat a projektünknek, váltsunk ebbe a könyvtárba, majd az npm init segítségével csináljunk egy alap információkkal feltöltött package.json állományt. Mindenre nyugodtan üthetünk Entert; jelen esetben csak az author részt írtuk át valami egyedire:

myapp> npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
...
author: Jakab Gipsz
...
Is this OK? (yes)

2.2 Az Express és a NodeMon telepítése

Villámgyorsan letölthetjük az aktuális könyvtár node_modules könyvtára alá a szükséges modulok aktuális stabil verzióját, valamint az npm azonnal a package.json fájlunkba is beírja őket, mint függőségek:

myapp> npm install express nodemon
 
added 168 packages, and audited 169 packages in 5s
 
11 packages are looking for funding
run `npm fund` for details
 
found 0 vulnerabilities

Ezzel még nem vagyunk készen. A package.json fájlban írjuk át a "test" részt "start": "nodemon ./index.js"-re. Ezáltal a nodemon minden változtatáskor újraindítja automatikusan a webszervert, ami nagyon hasznos.

A package.json tartalma ebben a fázisban:

{
  "name": "src",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "nodemon ./index.js"
  },
  "author": "Jakab Gipsz",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1",
    "nodemon": "^2.0.7"
  }
}

2.3 Az index.js létrehozása

A projektünk könyvtárának gyökerébe hozzunk létre egy üres index.js állományt, és mentsük bele az alábbi, kezdeti tartalmat:

const EXPRESS = require('express')
const APP = EXPRESS()
const PORT = 3000
 
APP.listen(PORT, () => {
  console.log(`Server is listening on port ${PORT}`)
})

2.4 Indítsuk el

Ezzel készen is vagyunk, a webszerver a 3000-es porton figyelni fog (remélhetőleg) egy egyszerű npm start parancs indításával:

myapp> npm start
 
> src@1.0.0 start
> nodemon ./index.js
 
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): _._
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node ./index.js`
Server is listening on port 3000

2.5 Nézzük meg böngészőben

Indítsunk egy webböngészőt és nyissuk meg a localhost:3000 címet. Egyelőre nem kell mást látnunk, mint az alábbi üzenetet:

Cannot GET /

Hurrá, működik!

2.6 Hello World

Az APP.listen blokk elé szúrjuk be az alábbi blokkot, és máris elláttuk a weboldalt tartalommal:

...
APP.get('/', (req, res) => {
res.send('Hello World');
});
...

Az index.js mentésekor a NodeMon segítségével máris újraindul a szerver, a böngészőben azonban egy F5-öt nyomnunk kell, hogy láthassunk munkánk gyümölcsét:

Hello World

A továbbiakban az appunk minden további bővítése az APP.listen blokk elé kerül; tulajdonképpen ez az utolsó blokk, ami jelen esetben is és általában az Expressben megírt alkalmazások legtöbbjében egyformán megtalálható.

Útvonalkezelés

Egy weboldal linkjében a /-el elválasztott "mappákat" az Express egy villámgyors és könnyen kezelhető útvonalkezelési megoldással ruházza fel. Ennek a közismert neve: routing. Segítségével egyáltalán nem szükséges, hogy az útvonal valódi mappákat tartalmazzon, helyette újabb és újabb funkciókkal láthatjuk el webes appunkat.

3.1 Statikus fájlok szolgáltatása

Az Express use metódusa teszi lehetővé, hogy bizonyos middleware-eket használjunk bizonyos útvonalakhoz.

Ehhez először csináljunk a projektünk főkönyvtárunkon belül egy images és egy public alkönyvtárat és másoljunk bele egy-egy képfájlt:

images / rocket.jpg;
public / tent.jpg;

3.1.1 Statikus fájlok az alapértelmezett / útvonal után

A legegyszerűbb ilyen útvonalbetöltés az alábbi, amelyet az előző APP.get blokk elé beírhatunk:

...
APP.use(EXPRESS.static('public'));
...

E rövid kód a public könyvtárat jelöli meg útvonalnak, amely alapértelmezés szerint a / lesz. Ennek köszönhetően a könyvtár alatti fájlok a böngészőben úgy fognak tűnni, mintha a / alatt lennének. Jelen esetben csak a tent.jpg képfájl helyezkedik is el ott; próbáljuk is ki.

Bizonyosodjunk meg róla, hogy az app még mindig fut (a NodeMon újratölti minden fájlváltozáskor; ha nem fut: npm start), majd hívjuk meg a http://localhost:3000/tent.jpg linket.

3.1.2 Statikus fájlok egyéni útvonalakon

Mindehhez csak az APP.use további paraméterét kell igénybe vennünk (az előzőt is meghagytuk). Jelen esetben az images könyvtárat tesszük elérhető ugyanúgy /images néven:

...
// for 'public' as '/'
APP.use(EXPRESS.static('public'));
 
// for 'images' as '/images'
APP.use('/images', EXPRESS.static('images'));
...

A fentivel már a http://localhost:3000/images/rocket.jpg linken a másik kép is elérhetővé válik.

3.2 A GET, POST, PUT és DELETE requestek

Egy weboldal betöltődésekor nem történik más, mint a kliens részéről a szerver felé kért GET request végrehajtása.

3.2.1 A GET

Az előző fejezet végén a /-t módosító "Hello World"-ös rész átirata szakmaibb nyelvre az alábbi:

...
APP.get('/', (req, res) => {
res.send(`a get request on / route on port ${PORT}`);
});
...

3.2.2 A POST, a PUT, a DELETE és a Postman fejlesztői segédeszköz

A POST request adatot küld a szerver felé, a PUT már meglévő adatot módosít, a DELETE pedig adatot töröl.

Ennek teszteléséhez már töltsük le az ingyenes Postman nevű programot a postman.com/downloads/ címről egy regisztráció, majd bejelentkezés után. A Postman webes applikációja nem fog működni a helyi requestekkel (legalábbis nekem nem ment), így mindenképpen a saját gépünkre telepített (nem kérdez semmit, csak kibontja magát és elindul) appra lesz szükségünk, és abba ismét bejelentkeznünk.

A már meglévő get blokk alapján írjuk meg a többi requestet. Az alábbi kód már egy véletlenszerű adatokat tartalmazó JSON adathalmazt is betölt, amelyet egyelőre csak a konzolra ír ki. Az adathalmaz legenerálásában (véletlenszerű nevekkel, email címekkel stb-vel) a mockaroo.com lehet nagy segítségünkre.

const EXPRESS = require('express')
const DATA = require('./data/data.json')
 
const APP = EXPRESS()
const PORT = 3000
 
APP.get('/', (req, res) => {
  res.send(`a get request on / route on port ${PORT}`)
})
 
APP.post('/newItem', (req, res) => {
  res.send(`a post request on /newItem route on port ${PORT}`)
})
 
APP.put('/item', (req, res) => {
  res.send(`a put request on /item route on port ${PORT}`)
})
 
APP.delete('/item', (req, res) => {
  res.send(`a delete request on /item route on port ${PORT}`)
})
 
APP.listen(PORT, () => {
  console.log(`Server is listening on port ${PORT}`)
  console.log(DATA)
})

Mentés után a Postman appon a requestek működését tesztelhetjük (jobb oldali kép).

A program már indulásakor feltérképezi, hogy milyen requestek állnak rendelkezésre, amit a bal oldali sávban láthatunk. A requestek küldésekor ügyeljünk az elérési utakra (localhost:3000/newItem vagy localhost:3000/item).

A program egyébként a manapság egyre népszerűbb módon, egyszerűen a c:\Users\<felhasználónév>\AppData\Local\Postman\ alá települ, amit át is mozgathatunk egy jobban kontrollálható helyre (pl. d:\bin\Postman).

3.3 Adat kiíratása a kliens oldalra

Ennél a résznél már a GET egy valós adatot igényel meg a szervertől. Elsőként az előzőleg már meghívott JSON struktúrát.

3.3.1 JSON adat kiküldése

Nem túl szép az eredmény, ám annál látványosabb. Sőt, a mai modern böngészők már szépen meg is formázzák, böngészhető módon jelenítik meg az így kapott adatot:

...
APP.get('/', (req, res) => {
res.json(DATA)
});
...

3.3.2 Adattípusok

Itt a jó alkalom, hogy megragadjuk az alkalmat, és kitérjünk két általános adattípusra.

JSON adat:

{ "hello": "A JSON egy szuper dolog" }

URL kódolású adat:

hello=Az+URL+kodolasu+adat+egy+szuper+dolog Ezekre az adattípusokra a későbbiek során még visszatérünk.

3.4 Paraméterátadás az útvonalba

Útvonalakat akár úgy is definiálhatunk, hogy az egy adathalmaz egy bizonyos elemére mutasson úgy megigényelve azt, hogy a böngésző linkjénél egy paramétert adunk át útvonalként. Ezt jól demonstrálja az alábbi programblokk:

...
APP.get('/item/:id', (req, res) => {
console.log(req.params.id);
let user = Number(req.params.id);
console.log(user);
console.log(DATA[user]);
res.send(DATA[user]);
});
...

A korábban már felhasznált data.json állomány DATA nevű konstansba töltését fejlesztettük itt tovább. A Number függvény alakítja át a JSON struktúra legelső, id elemét valódi számmá. A : teszi lehetővé, hogy paraméterként viselkedjen.

Mentsük el a kódot, és a böngészőben teszteljük pl. a http://localhost:3000/item/10 link meghívásával a 10-es sorszámú elem kiíratásával. Ahogyan a böngészőben, úgy a konzolon is ugyanúgy láthatjuk, hogy valóban a 10-es sorszámú elem részletei jelennek meg (amennyiben 0-val kezdődik a lista):

...
{
id: 10,
first_name: 'Tuesday',
last_name: 'Ravenscroftt',
email: 'travenscroftt9@netscape.com',
gender: 'Female'
}

3.5 Kiegészítő handlerek

Bármilyen ilyen útvonalakat definiáló blokk kiegészíthető handlerekkel. Fontos viszont, hogy a req, res részek csak egyszer használhatóak fel.

Nem csak az első blokk végét, de a függvény fejrészét is ki kellett egészíteni a next-el:

...
APP.get('/item/:id', (req, res, next) => {
console.log(req.params.id);
let user = Number(req.params.id);
console.log(user);
console.log(DATA[user]);
res.send(DATA[user]);
next();
}, (req, res) =>
console.log('Did you get the right data?')
);
...

Eredménye (miután ezúttal a http://localhost:3000/item/03 címet hívtuk meg):

...
{
id: 3,
first_name: 'Wyn',
last_name: 'Swynley',
email: 'wswynley2@example.com',
gender: 'Male'
}
Did you get the right data?

3.6 Általános response metódusok

Eddig a res után csak a send metódusra láthattunk példákat, de itt is van lehetőségünk további műveletekre. Lehet akár egy APP.post után res.redirect, hogy az adatot pl. egy másik szerverre továbbítsuk, de nézzünk egyre érdekesebb példákat.

3.6.1 A res.end

...
APP.get('/item', (req, res) => {
res.end();
});
...

Ez egyszerűen megállítja a programblokk további futását és a http://localhost:3000/item oldal egy üres semmi lesz.

3.6.2 A res.redirect

Ezzel egy adott útvonal meghívására átirányítás történik egy másik weboldalra:

...
APP.get('/item', (req, res) => {
res.redirect('http://maas.hu');
});
...

3.6.3 A res.dowload

Az adott útvonal meghívására letöltési ablak ugrik elő (a böngésző automatikus letöltési funkciójának engedélyezése esetén le is töltődik) még akkor is, ha ez a fájl egy képfájl:

...
APP.get('/images', (req, res) => {
res.download('images/rocket.jpg');
});
...

További response metódusokról az expressjs.com/en/guide/routing.html oldalon, a "Response methods" részben olvashatunk.

3.7 Láncolás

Ez helyett:

...
APP.put('/item', (req, res) => {
res.send(`a put request on /item route on port ${PORT}`);
});
 
APP.get('/item', (req, res) => {
res.send(`a get request on /item route on port ${PORT}`);
});
 
APP.delete('/item', (req, res) => {
res.send(`a delete request on /item route on port ${PORT}`);
});
...

...lehet így is:

...
APP.route('/item')
.put((req, res) => {
res.send(`a put request on /item route on port ${PORT}`);
})
.get((req, res) => {
res.send(`a get request on /item route on port ${PORT}`);
})
.delete((req, res) => {
res.send(`a delete request on /item route on port ${PORT}`);
})
...

Köztes alkalmazásrétegek: middleware-ek

A köztes alkalmazásrétegek (közismert nevükön middleware-ek) azok a függvények, amelyek akkor futnak le, mielőtt még az app response (res) blokkja meghívásra kerülne.

A köztes alkalmazásrétegeket reprezentáló függvények az alábbi feladatokat láthatják el:

  • futtathatnak bármilyen kódot
  • módosíthatják a request és a response objektumokat
  • véget vethetnek a request-response ciklusnak
  • meghívják a programkód következő middleware függvényét

4.1 Egyszerű middleware

A korábban már megismert, paraméterátadással és kiegészítő handlerrel rendelkező programblokkunkat kissé kiegészítve a req objektum kezelésével a következőképpen jelölhetjük be a köztes alkalmazásrétegeket:

...
APP.get('/item/:id', (req, res, next) => {
 
// middleware, ami az adatot behúzza
console.log(req.params.id);
let user = Number(req.params.id);
console.log(user);
console.log(DATA[user]);
 
// middleware, ami a req objektumot használja
console.log(`Request from: ${req.originalUrl}`);
console.log(`Request type: ${req.method}`);
 
// minden, ami idáig volt, az middleware
res.send(DATA[user]);
next();
}, (req, res) =>
console.log('Did you get the right data?')
);
...

A fenti mentése után a http://localhost:3000/item/5 címet meghívva az alábbiakat láthatjuk szerverünk konzolján:

{
id: 5,
first_name: 'Carma',
last_name: 'Verdey',
email: 'cverdey4@mediafire.com',
gender: 'Female'
}
Request from: /item/5
Request type: GET
Did you get the right data?

4.2 Beépített middleware

Az Express egy egészen alap, általános keretrendszert nyújt, így ennek megfelelően nincs agyonzsúfolva mindenféle szolgáltatással. A webes applikációk adatbeküldési, adatmódosítási és adatkiolvasási megoldásokat viszont hiánytalanul megoldhatjuk vele külső függvénytárak nélkül is. Nézzünk erre példákat.

4.2.1 JSON adat küldése

Adjuk hozzá az alábbi blokkokat applikációnkhoz:

...
APP.use(EXPRESS.json());
...
APP.post('/newItem', (req, res) => {
console.log(req.body);
res.send(req.body);
});
...

Miután elmentettük (és az app automatikusan újraindult), a Postman segítségével paraméterezzük fel, hogy legyen postolt adat, amire applikációnk reagálhat.

Ehhez indítsuk el a Postmant, a "method"-ot állítsuk POST-ra, a "request URL"-nek adjuk meg a localhost:3000/newItem-et, a "Headers" fülön a listához adjuk hozzá a "Content-Type" key-t, ahhoz a "application/json" value-t, a "Body" fülön a formátumot állítsuk "raw"-ra, majd gépeljük be az alábbi egyszerű JSON adatot:

{
  "hello": "A JSON egy szuper dolog"
}

Ezután már rányomhatunk a "Send" gombra, és rögtön láthatjuk is, hogy a postolt adat a konzolon is megjelenik:

{
  "hello": "A JSON egy szuper dolog"
}

4.2.2 URL kódolású adat küldése

Az előző kódrészletek közül csak az APP.use részt kell átírnunk, az APP.post marad ugyanaz, mint az előző, hiszen ugyanazt a feladatot végzi el, csak más adattípussal:

...
APP.use(EXPRESS.urlencoded({extended: true}));
...
APP.post('/newItem', (req, res) => {
console.log(req.body);
res.send(req.body);
});
...

A Postman-ben "Content-Type"-ként kiválasztott érték itt már "application/x-www-form-urlencoded" kell legyen, a "Body" részben a formátum pedig "x-www-form-urlencoded". Ebben a felállásban a "key" mezőt tölthetjük ki a hello szöveggel, a "value" mezőt pedig a Az URL kódolású adat egy szuper dolog értékkel. A "Post" gomb megnyomásakor azt tapasztalhatjuk, hogy az előző formátumban látható módon kapjuk meg az eredményt:

{
  "hello": "Az URL kódolású adat egy szuper dolog"
}

4.3 Hibakezelési middleware

A teszt érdekében, alkossunk egy olyan útvonalat, aminek meghívása automatikusan egy általános hibaüzenetet generál:

...
APP.get('/err', (req, res) => {
throw new Error();
});
...

A hosszú hibaüzenetet a böngészőben és a konzolon egyaránt megkapjuk. Ha ez nem lenne elég, a hiba megjelenésének pontos pozícióját (sor, oszlop) nem csak csak az index.js fájlban, hanem az összes kapcsolódó modulra vonatkozóan is megjeleníti a Node.js/Express. Ezt a teljes hibakifejtési szöveget nevezzük "error stack"-nek.

Ez tehát az alapértelmezett hibakezelési viselkedés, de ezt finomíthatjuk:

...
APP.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send(`Váratlan hiba történt! ${err.stack}`);
});
...

Ez az APP.use rész alkotja a hibakezelési middleware-t. A módszerrel akár olyan alkalmazást is készíthetünk, ami hiba esetén átirányít, letöltést indít el stb.

4.4 Külső middleware-ek

Az Express készítői számos külső middleware-t ajánlanak annak weboldalán.

4.4.1 A serve-favicon middleware

Ez az apró modul a webes appunk faviconját hivatott beállítani.

Gyorsan telepíthetjük appunk főkönyvtárában az npm install serve-favicon paranccsal. Kerítsünk is egy .ico fájlt (vagy rajzoljunk egyet GIMP-el), majd helyezzük el az app public könyvtárában favicon.ico néven.

Alkalmazásunkat az alábbiak szerint bővítsük:

const EXPRESS = require('express');
const FAVICON = require('serve-favicon');
const PATH = require('path');
...
 
APP.use(FAVICON(PATH.join(\_\_dirname, 'public', 'favicon.ico')));
...

A kód mentésével, az app újraindításával (ha nem futott volna: npm start), majd a weboldal frissítésekor máris láthatunk egy apró ikont a böngésző fülén, akármelyik útvonalon is járunk.

A legtöbb külső middleware az előbbihez hasonlóan jól dokumentált, így érdemes kipróbálni továbbiakat is. Express appunkhoz pl. erős biztonsági réteget adhatunk a Helmet segítségével.

Hibakeresés

Expressben viszonylag egyszerű a hibakeresés, azaz a debug-olás. Mindössze be kell kapcsolni ehhez a Debug üzemmódot még az applikáció elindítása előtt.

5.1 Rendszerszintű debug bekapcsolása

Linuxon vagy Mac-en:

myapp$ DEBUG=express:\* node index.js

Windows-on (az & használata nem biztos, hogy engedélyezett):

myapp> set DEBUG=express:\* & node index.js

Így persze nem az npm-el indítjuk el az applikációt, így van ennél sokkal jobb megoldás is:

5.2 Hibakeresési üzemmód NPM-ből

Ehhez egyszerűen a package.json fájlt kell kiokosítanunk kicsit, hogy ugyanúgy induljon el az app, ahogyan eddig, de debug módban. Ennek megvalósításához a már meglévő start szekció végére írjunk egy vesszőt és adjuk hozzá a debug-os sort.

Linux-on és Mac-en:

...
"scripts": {
"start": "nodemon ./index.js",
"debug": "DEBUG=express:\* nodemon ./index.js"
},
...

Windows-on (meglepő módon így elfogadottá válik a & is):

...
"scripts": {
"start": "nodemon ./index.js",
"debug": "set DEBUG=express:\* & nodemon ./index.js"
},
...

Ezután az app indítása bármilyen platformon:

myapp> npm run debug
...

Socket.IO

Legalább említésképpen mindenképpen ki kell térnünk a Socket.IO-ra, amely a folyamatos szerver ↔ kliens kapcsolat megvalósításának egy hagyományos módját biztosítja. Segítségével eseményvezérelt kapcsolatot biztosíthatunk appunk dinamikus mnűködéséhez.

Használatához természetesen a socket.io-t is telepítenünk kell az npm-el.

6.1 A backend oldal (/index.js):

const APP = require('express')();
const SERVER = require('http').Server(APP);
const IO = require('socket.io')(SERVER);
const PORT = 3000;
 
SERVER.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
 
APP.get('/', (req, res) => {
res.sendFile(\_\_dirname + '/public/index.html');
});
 
IO.on('connection', (socket) => {
console.log('user connected');
socket.emit('message', {gizi: 'Szia! Hogy vagy?'});
socket.on('another event', (data) => {
console.log(data);
});
});

6.2 A frontend oldal (/public/index.html):

Készítsünk egy public könyvtárat (ha még nem lenne), és abban az index.html tartalmazza az alábbi, igazán minimális kódsort:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title></title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>
  <body>
    <script src="/socket.io/socket.io.js"></script>
    <script>
      const SOCKET = io.connect('http://localhost:3000')
      SOCKET.on('message', (data) => {
        console.log(data)
        SOCKET.emit('another event', {
          gerzson: 'Remekül vagyok, köszi szépen!',
        })
      })
    </script>
  </body>
</html>

A szerver elindítása után, ahogy megnyitjuk a http://localhost:3000 oldalt, az böngésző konzolján máris megjelenik az alábbi:

Object { gizi: "Szia! Hogy vagy?" }

Majd pillanatokon belül a szerver konzolján is az alábbi:

...
[nodemon] starting `node ./index.js`
Server is running on port 3000
user connected
{ gerzson: 'Remekül vagyok, köszi szépen!' }

A fentiekben nem foglalkoztunk vele, de az Express dokumentációja még tartalmaz rengeteg hasznos információt pl. a proxy-król, a security-ről vagy a sütik használatáról. Érdemes áttekintenünk, ha belemélyednénk a témába.