035.2 Lecke 2
Tanúsítvány: |
Web Development Essentials |
---|---|
Verzió: |
1.0 |
Témakör: |
035 NodeJS szerverprogramozás |
Fejezet: |
035.2 NodeJS Express alapok |
Lecke: |
2/2 |
Bevezetés
A webszerverek nagyon sokoldalú mechanizmusokkal rendelkeznek a kliensek kéréseire adott válaszok előállításához. Bizonyos kérések esetén elég, ha a webszerver statikus, feldolgozatlan választ ad, mivel a kért erőforrás minden kliens számára ugyanaz. Ha például egy kliens egy mindenki számára elérhető képet kér, akkor elég, ha a szerver elküldi a képet tartalmazó fájlt.
Ha a válaszok dinamikusan generálódnak, akkor lehet, hogy jobban kell strukturálni őket, mintha írnánk néhány egyszerű szerverszkriptet. Ilyen esetekben célszerű, ha a webszerver képes egy teljes dokumentumot generálni, amelyet a kliens értelmezhet és megjeleníthet. A webalkalmazás-fejlesztés során a HTML-dokumentumokat általában sablonként hozzák létre, és elkülönítve tartják a szerverszkripttől, amely dinamikus adatokat illeszt be a megfelelő sablon előre meghatározott helyére, majd a formázott választ elküldi a kliensnek.
A webes alkalmazások gyakran hasznának statikus és dinamikus erőforrásokat egyaránt. Egy HTML-dokumentum, még ha dinamikusan generálták is, tartalmazhat hivatkozásokat statikus erőforrásokra, például CSS-fájlokra és képekre. Annak bemutatására, hogy az Express hogyan segít az ilyen jellegű igények kezelésében, először egy statikus fájlokat szállító példaszervert állítunk be, majd olyan útvonalakat implementálunk, amelyek strukturált, sablonalapú válaszokat generálnak.
Statikus fájlok
Az első lépés a kiszolgálóként futó JavaScript-fájl létrehozása. Kövessük az előző leckékben tárgyalt mintát egy egyszerű Express alkalmazás létrehozásához: először hozzunk létre egy server
nevű könyvtárat, majd telepítsük az alapkomponenseket az npm
paranccsal:
$ mkdir server $ cd server/ $ npm init $ npm install express
A belépési ponthoz bármilyen fájlnevet használhatunk, de itt az alapértelmezett fájlnevet fogjuk használni: index.js
. A következő lista egy alap index.js
fájlt mutat, amelyet a szerverünk kezdőpontjaként fogunk használni:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.listen(port, host, () => {
console.log(`A szerver kész a http://${host}:${port} címen`)
})
Nem kell explicit kódot írnunk egy statikus fájl elküldéséhez. Az Expressnek van erre a célra egy middleware-je, az express.static
. Ha a szervernek statikus fájlokat kell küldenie a kliensnek, csak töltsük be az express.static
middleware-t a szkript elején:
app.use(express.static('public'))
A public
paraméter azt a mappát jelöli, amely a kliens által lekérhető fájlokat tárolja. A kliensek által kért elérési utak nem tartalmazhatják a public
mappát, csak a fájlnevet, vagy a fájl elérési útját a public
mappához képest. A public/layout.css
fájl lekéréséhez például a kliens a /layout.css
könyvtárba küld kérést.
Formázott kimenet
Míg a statikus tartalmak küldése egyszerű, a dinamikusan generált tartalmak nagyon eltérőek lehetnek. A rövid üzeneteket tartalmazó dinamikus válaszok létrehozása megkönnyíti az alkalmazások tesztelését a fejlesztés kezdeti szakaszában. A következő példa egy tesztút, amely csak visszaküld a kliensnek egy üzenetet, amelyet a HTTP POST
módszerrel küldött. A válasz csak az üzenet tartalmát ismétli egyszerű szövegként, mindenféle formázás nélkül:
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
Egy ilyen út nem csak az Express tanulásához jó példa, hanem diagnosztikai célokra is, ahol a res.send()
paranccsal küldött nyers válasz is elég. Egy hasznos szervernek azonban képesnek kell lennie összetettebb válaszok előállítására is. Most továbblépünk, hogy ilyen kifinomultabb típusú utakat fejlesszünk.
Az új alkalmazásunk ahelyett, hogy csak az aktuális kérés tartalmát küldené vissza, teljes listát vezet az egyes kliensek által a korábbi kérésekben küldött üzenetekről, és kérésre visszaküldi az egyes kliensek listáját. Lehetőség van az összes üzenetet egyesítő válaszra, de megfelelőbbek lehetnek más formázott kimeneti módok, különösen, ha a válaszok egyre bonyolultabbá válnak.
Az aktuális munkamenet során küldött kliensüzenetek fogadásához és tárolásához először is extra modulokat kell beépítenünk a sütik és a HTTP POST
módszerrel küldött adatok kezeléséhez. Az alábbi példaszerver egyetlen célja a POST
módszerrel küldött üzenetek naplózása és a korábban küldött üzenetek megjelenítése, amikor a kliens GET
kérést küld. Tehát két útvonal van a /
útvonalhoz. Az első útvonal a HTTP POST
módszerrel, a második pedig a HTTP GET
módszerrel küldött kéréseket teljesíti:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.use(express.static('public'))
const cookieParser = require('cookie-parser')
app.use(cookieParser())
const { v4: uuidv4 } = require('uuid')
app.use(express.urlencoded({ extended: true }))
// Tömb az üzenetek tárolásához
let messages = []
app.post('/', (req, res) => {
// Csak JSON-kompatibilis kérések
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Süti lokalizálása a kérésben
let uuid = req.cookies.uuid
// Ha nincs uuid süti, jöjjön létre egy új
if ( uuid === undefined )
uuid = uuidv4()
// Üzenet hozzáadása az üzenetek tömbhöz
messages.unshift({uuid: uuid, message: req.body.message})
// A korábbi üzenetek összegyűjtése az uuid számára
let user_entries = []
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
// A süti lejárati dátumának frissítése
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// JSON-válasz visszaküldése
res.json(user_entries)
})
app.get('/', (req, res) => {
// Csak JSON-kompatibilis kérések
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Süti lokalizálása a kérésben
let uuid = req.cookies.uuid
// A kliens saját üzenetei
let user_entries = []
// Ha nincs uuid süti, jöjjön létre egy új
if ( uuid === undefined ){
uuid = uuidv4()
}
else {
// Üzenetek összegyűjtése az uuid számára
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
}
// A süti lejárati dátumának frissítése
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// JSON-válasz visszaküldése
res.json(user_entries)
})
app.listen(port, host, () => {
console.log(`A szerver kész a http://${host}:${port} címen`)
})
A statikus fájlok konfigurációját felül tartottuk, mert nemsokára hasznos lesz olyan statikus fájlokat biztosítani, mint a layout.css
. Az előző fejezetben bemutatott cookie-parser
middleware mellett a példa tartalmazza a uuid
middlewaret is, amely egy egyedi azonosítószámot generál, amelyet sütiként adunk át minden egyes kliensnek, aki üzenetet küld. Ha még nincsenek telepítve a példaszerver könyvtárába, akkor ezek a modulok az npm install cookie-parser uuid
paranccsal telepíthetők.
A messages
nevű globális tömb tárolja az összes kliens által küldött üzenetet. A tömb minden egyes eleme egy objektumból áll, amelynek tulajdonságai a uuid
és az message
.
Ami igazán új ebben a szkriptben, az a res.json()
metódus, amelyet a két út végén használunk, hogy JSON-formátumú választ generáljunk a kliens által már elküldött üzeneteket tartalmazó tömbbel:
// JSON-válasz visszaküldése
res.json(user_entries)
A JSON egy egyszerű szöveges formátum, amely lehetővé teszi, hogy egy adathalmazt egyetlen asszociatív struktúrába csoportosítsunk: a tartalom kulcsok és értékek formájában jelenik meg. A JSON különösen akkor hasznos, ha a válaszokat a kliens fogja feldolgozni. Ennek a formátumnak a használatával egy JavaScript-objektum vagy tömb könnyen rekonstruálható a kliensoldalon az eredeti objektum összes tulajdonságával és indexével a szerveren.
Mivel minden egyes üzenetet JSON-ban strukturálunk, elutasítjuk azokat a kéréseket, amelyek nem tartalmazzák az accept
fejlécben az application/json
kifejezést:
// Csak JSON-kompatibilis kérések
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
A sima curl
paranccsal egy új üzenet beillesztésére irányuló kérés nem kerül elfogadásra, mivel a curl
alapértelmezés szerint nem adja meg az application/json
opciót az accept
fejlécben:
$ curl http://myserver:8080/ --data message="Az első üzenetem" -c cookies.txt -b cookies.txt Not Found
A -H "accept: application/json"
kapcsoló megváltoztatja a kérés fejlécét, hogy megadja a válasz formátumánt, ami ezúttal a megadott formátumban lesz elfogadva és megválaszolva:
$ curl http://myserver:8080/ --data message="Az első üzenetem" -c cookies.txt -b cookies.txt -H "accept: application/json" ["Az első üzenetem"]
A másik útvonalat használó üzenetek megszerzése hasonló módon történik, de ezúttal a HTTP GET
módszerrel:
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -H "accept: application/json" ["Másik üzenet","Az első üzenetem"]
Sablonok
Az olyan formátumú válaszok, mint a JSON, kényelmesek a programok közötti kommunikációhoz, de a legtöbb webes alkalmazásszerver fő célja az emberi fogyasztásra szánt HTML-tartalom előállítása. A HTML-kód beágyazása a JavaScript-kódba nem jó ötlet, mert a nyelvek keveredése ugyanabban a fájlban a programot hibaérzékenyebbé teszi, és árt a kód karbantartásának.
Az Express képes együttműködni különböző sablonmotorokkal (template engine), amelyek elkülönítik a HTML-t a dinamikus tartalomhoz; a teljes lista megtalálható a Express sablonmotorok webhelyen. Az egyik legnépszerűbb sablonmotor az Embedded JavaScript (EJS), amely lehetővé teszi, hogy HTML fájlokat hozzunk létre speciális címkékkel a dinamikus tartalom beillesztéséhez.
A többi Express komponenshez hasonlóan az EJS-t is abba a mappába kell telepíteni, ahol a szerver fut:
$ npm install ejs
Ezután az EJS motort alapértelmezett renderelőként kell beállítani a szerverszkriptben (az index.js
fájl elején, az útvonaldefiníciók előtt):
app.set('view engine', 'ejs')
A sablonnal generált választ a res.render()
függvénnyel küldjük el a kliensnek, amely paraméterként megkapja a sablonfájl nevét és egy olyan objektumot, amely a sablonon belül elérhető értékeket tartalmazza. Az előző példában használt utak átírhatók úgy, hogy a JSON mellett HTML válaszokat is generáljanak:
app.post('/', (req, res) => {
let uuid = req.cookies.uuid
if ( uuid === undefined )
uuid = uuidv4()
messages.unshift({uuid: uuid, message: req.body.message})
let user_entries = []
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "Üzeneteim", messages: user_entries})
})
app.get('/', (req, res) => {
let uuid = req.cookies.uuid
let user_entries = []
if ( uuid === undefined ){
uuid = uuidv4()
}
else {
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
}
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "Üzeneteim", messages: user_entries})
})
Vegyük figyelembe, hogy a válasz formátuma a kérelemben található accept
fejléctől függ:
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "Üzeneteim", messages: user_entries})
A JSON formátumú válasz csak akkor kerül elküldésre, ha a kliens ezt explicit módon kéri. Ellenkező esetben a válasz az index
sablonból generálódik. Ugyanaz a user_entries
tömb táplálja mind a JSON kimenetet, mind a sablont. Az utóbbi paramétereként használt objektumnak van a title: "Az üzeneteim"
tulajdonsága, amely a sablonon belül címként lesz használva.
HTML sablonok
A statikus fájlokhoz hasonlóan a HTML-sablonokat tartalmazó fájlok is a saját mappájukban találhatók meg. Alapértelmezés szerint az EJS feltételezi, hogy a sablonfájlok a views/
könyvtárban vannak. A példában egy index
nevű sablont használtunk, így az EJS a views/index.ejs
fájlt keresi. A következő felsorolás egy egyszerű views/index.ejs
sablon tartalma, amely a példakóddal együtt használható:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><%= title %></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/layout.css">
</head>
<body>
<div id="interface">
<form action="/" method="post">
<p>
<input type="text" name="message">
<input type="submit" value="Elküld">
</p>
</form>
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
</div>
</body>
</html>
Az első speciális EJS tag a <title>
elem a <head>
szekcióban:
<%= title %>
A feldolgozás során ezen a speciális címke helyébe a res.render()
függvény paramétereként átadott objektum title
tulajdonságának értéke lép.
A sablon nagy része hagyományos HTML-kódból áll, így a sablon tartalmazza az új üzenetek küldésére szolgáló HTML-űrlapot. A tesztszerver a HTTP GET
és POST
módszerekre válaszol, ugyanazon a /
elérési útvonalon, ezért vannak az űrlap tagben az action="/"
és a method="post"
attribútumok.
A sablon egyéb részei HTML-kód és EJS-tagek keverékei. Az EJS a sablonon belül speciális célokra szolgáló tagekkel rendelkezik:
<% … %>
-
Folyamvezérlés beillesztése. Ez a tag közvetlenül nem illeszt be tartalmat, de JavaScript struktúrákkal együtt használható a HTML-szakaszok kiválasztására, megismétlésére vagy elhagyására. Példa egy ciklus indítására:
<% messages.forEach( (message) ⇒ { %>
<%# … %>
-
Meghatároz egy kommentet, amelynek tartalmát a feldolgozás során figyelmen kívül hagyjuk. A HTML-ben írt kommentekkel ellentétben ezek a kommentek nem láthatók a kliens számára.
<%= … %>
-
Beilleszti a változó escapelt (feloldott) tartalmát. Az ismeretlen tartalmak feloldása fontos, mivel ezzel elkerülhető a JavaScript-kód futtatása, amely kiskapukat nyithat a Cross-Site Scripting (XSS) támadások számára. Példa:
<%= title %>
<%- … %>
-
A változó tartalmának beillesztése escaping nélkül.
A HTML-kód és az EJS-tagek keveredése jól látható abban a kódrészletben, ahol a kliensüzenetek HTML-listaként jelennek meg:
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
Ebben a kódrészletben az első <% … %>
tag egy forEach
utasítással kezdődik, ami bejárja a message
tömb összes elemét. A <%
és a %>
delmiterek segítségével kontrollálhatjuk a HTML részleteit. Egy új HTML listaelem, a <li><%= message %></li>
, jön létre a messages
minden eleméhez. Ezekkel a változtatásokkal a szerver HTML-ben küldi el a választ, amikor egy olyan kérés érkezik, mint az alábbi:
$ curl http://myserver:8080/ --data message="Most" -c cookies.txt -b cookies.txt <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Üzeneteim</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/layout.css"> </head> <body> <div id="interface"> <form action="/" method="post"> <p> <input type="text" name="message"> <input type="submit" value="Elküld"> </p> </form> <ul> <li>Most</li> <li>HTML-ben</li> </ul> </div> </body> </html>
A kérések feldolgozására és a válasz megjelenítésére szolgáló kódok szétválasztása tisztábbá teszi az egész kódot, és lehetővé teszi, hogy a csapat az alkalmazásfejlesztést különböző szaktudással rendelkező emberek között ossza fel. Egy webdesigner például a views/
könyvtárban található sablonfájlokra és a kapcsolódó stíluslapokra összpontosíthat, amelyek a példaszerver public/
könyvtárában tárolt statikus fájlokként vannak megadva.
Gyakorló feladatok
-
Hogyan kell beállítani az
express.static
állományt, hogy a kliensek azassets
könyvtárban lévő fájlokat kérhessék le? -
Hogyan azonosítható annak a válasznak a típusa egy Express úton belül, amelyet a kérés fejlécében adunk meg?
-
A
res
(válasz) út paraméter melyik módszere generál JSON formátumú választ acontent
nevű JavaScript tömbből?
Gondolkodtató feladatok
-
Alapértelmezés szerint az Express sablonfájlok a
views
könyvtárban vannak. Hogyan lehet ezt a beállítást úgy módosítani, hogy a sablonfájlok atemplates
könyvtárban legyenek tárolva? -
Tegyük fel, hogy a kliens cím nélküli HTML-választ kap (azaz
<title></title>
). Az EJS-sablon ellenőrzése után a fejlesztő megtalálja a<title><% title %></title>
taget a fájlhead
szekciójában. Mi lehet a probléma legvalószínűbb oka? -
Az EJS sabloncímkék segítségével írjunk egy
<h2></h2>
HTML-címkét ah2
JavaScript-változó tartalmával! Ez a tag csak akkor jelenjen meg, ha ah2
változó nem üres!
Összefoglalás
Ez a lecke az Express.js alapvető módszereit mutatja be a statikus és formázott, de mégis dinamikus válaszok generálásához. A HTTP-szerver beállításához statikus fájlok esetén kevés erőfeszítés szükséges, az EJS sablonozó rendszere pedig egyszerű módot biztosít a dinamikus tartalom HTML-fájlokból történő generálására. Ez a lecke a következő fogalmakat és eljárásokat veszi át:
-
Az
express.static
használata statikus fájl-válaszokhoz. -
Hogyan hozzunk létre olyan választ, amely megfelel a kérés fejlécében lévő tartalomtípus mezőnek.
-
JSON-struktúrájú válaszok.
-
EJS-tagek használata HTML-alapú sablonokban.
Válaszok a gyakorló feladatokra
-
Hogyan kell beállítani az
express.static
állományt, hogy a kliensek azassets
könyvtárban lévő fájlokat kérhessék le?Az
app.use(express.static('assets'))
hívást hozzá kell adni a szerverszkripthez. -
Hogyan azonosítható annak a válasznak a típusa egy Express úton belül, amelyet a kérés fejlécében adunk meg?
A kliens az elfogadható típusokat az
accept
fejléc mezőben határozza meg, amely areq.headers.accept
tulajdonsághoz van rendelve. -
A
res
(válasz) út paraméter melyik módszere generál JSON formátumú választ acontent
nevű JavaScript tömbből?A
res.json()
metódus:res.json(content)
.
Válaszok a gondolkodtató feladatokra
-
Alapértelmezés szerint az Express sablonfájlok a
views
könyvtárban vannak. Hogyan lehet ezt a beállítást úgy módosítani, hogy a sablonfájlok atemplates
könyvtárban legyenek tárolva?A mappát a kezdeti szkriptbeállításokban az
app.set('views', './templates')
paranccsal lehet meghatározni. -
Tegyük fel, hogy a kliens cím nélküli HTML-választ kap (azaz
<title></title>
). Az EJS-sablon ellenőrzése után a fejlesztő megtalálja a<title><% title %></title>
taget a fájlhead
szekciójában. Mi lehet a probléma legvalószínűbb oka?A
<%= %>
címkét egy változó tartalmának bezárására kell használni, mint például a<%= title %>
esetben. -
Az EJS sabloncímkék segítségével írjunk egy
<h2></h2>
HTML-címkét ah2
JavaScript-változó tartalmával! Ez a tag csak akkor jelenjen meg, ha ah2
változó nem üres!<% if ( h2 != "" ) { %> <h2><%= h2 %></h2> <% } %>