035.2 Lektion 2
Zertifikat: |
Web Development Essentials |
---|---|
Version: |
1.0 |
Thema: |
035 Node.js Server-Programmierung |
Lernziel: |
035.2 Grundlagen von Node.js Express |
Lektion: |
2 von 2 |
Einführung
Webserver haben vielseitige Mechanismen, um auf Client-Anfragen zu antworten. Bei einigen Anfragen reicht es aus, wenn der Webserver eine statische, unverarbeitete Antwort liefert, da die angeforderte Ressource für jeden Client gleich ist. Wenn ein Client beispielsweise ein Bild anfordert, das allgemein zugänglich ist, genügt es, dass der Server die Bilddatei sendet.
Werden Antworten jedoch dynamisch erzeugt, müssen sie unter Umständen besser strukturiert sein als einfache Zeilen im Serverskript. In solchen Fällen ist es praktisch, wenn der Webserver ein vollständiges Dokument generiert, das der Client interpretieren und rendern kann. Bei der Entwicklung von Webanwendungen werden HTML-Dokumente üblicherweise als Vorlagen erstellt und vom Serverskript getrennt gehalten, das dynamische Daten an vorgegebenen Stellen in die entsprechende Vorlage einfügt und dann die formatierte Antwort an den Client sendet.
Webanwendungen verbrauchen oft sowohl statische als auch dynamische Ressourcen. Ein HTML-Dokument kann, auch wenn es dynamisch generiert wurde, Verweise auf statische Ressourcen wie CSS-Dateien und Bilder enthalten. Um zu demonstrieren, wie Express bei solchen Anforderungen hilft, richten wir zunächst einen Beispielserver ein, der statische Dateien liefert, und implementieren dann Routen, die strukturierte, vorlagenbasierte Antworten erzeugen.
Statische Dateien
Der erste Schritt besteht darin, die JavaScript-Datei zu erstellen, die als Server ausgeführt werden soll. Folgen wir dem Muster der vorangegangenen Lektion, um eine einfache Express-Anwendung zu erstellen: Legen Sie zunächst ein Verzeichnis namens server
an und installieren Sie dann die Basiskomponenten mit dem Befehl npm
:
$ mkdir server $ cd server/ $ npm init $ npm install express
Als Einstiegspunkt kann ein beliebiger Dateiname dienen — hier nutzen wir den Standarddateinamen index.js
. Die folgende Auflistung zeigt eine einfache Datei index.js
als Startpunkt für unseren Server:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
Sie müssen keinen expliziten Code schreiben, um eine statische Datei zu senden. Express hat dafür eine Middleware namens express.static
. Muss Ihr Server statische Dateien an den Client senden, laden Sie einfach die Middleware express.static
am Anfang des Skripts:
app.use(express.static('public'))
Der Parameter public
gibt das Verzeichnis an, in dem Dateien gespeichert sind, die der Client anfordern kann. Pfade, die von Clients angefordert werden, dürfen nicht das Verzeichnis public
enthalten, sondern nur den Dateinamen oder den Pfad zur Datei relativ zum Verzeichnis public
. Um zum Beispiel die Datei public/layout.css
anzufordern, stellt der Client eine Anfrage nach /layout.css
.
Formatierte Ausgabe
Während das Senden statischer Inhalte einfach ist, können dynamisch generierte Inhalte stark variieren. Die Erstellung dynamischer Antworten mit kurzen Nachrichten erleichtert das Testen von Anwendungen im Anfangsstadium ihrer Entwicklung. Das folgende Beispiel ist eine Testroute, die lediglich eine Nachricht, die mit der HTTP-Methode POST
gesendet wurde, an den Client zurücksendet. Die Antwort kann einfach den Inhalt der Nachricht im Klartext wiedergeben, ohne jegliche Formatierung:
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
Eine Route wie diese ist ein gutes Beispiel für das Erlernen von Express und für Diagnosezwecke, bei denen eine unformatierte Antwort, die mit res.send()
gesendet wird, ausreicht. Aber ein nützlicher Server muss in der Lage sein, komplexere Antworten zu erzeugen. Wir werden darum nun eine komplexere Route entwickeln.
Unsere neue Anwendung sendet nicht nur den Inhalt der aktuellen Anfrage zurück, sondern verwaltet eine vollständige Liste der Nachrichten, die in früheren Anfragen von jedem Client gesendet wurden, und sendet die Liste jedes Clients auf Anfrage zurück. Eine Antwort, die alle Nachrichten zusammenfasst, ist eine Option, aber andere formatierte Ausgabemodi sind besser geeignet, insbesondere wenn die Antworten umfangreicher werden.
Um die während der aktuellen Sitzung gesendeten Client-Nachrichten zu empfangen und zu speichern, müssen wir zunächst zusätzliche Module für die Behandlung von Cookies und Daten, die über die HTTP-Methode POST
gesendet werden, einfügen. Der einzige Zweck des folgenden Beispielservers besteht darin, per POST
gesendete Nachrichten zu protokollieren und zuvor gesendete Nachrichten anzuzeigen, wenn der Client eine GET
-Anfrage stellt. Es gibt also zwei Routen für den /
-Pfad. Die erste Route erfüllt Anfragen, die mit der HTTP-Methode POST
gestellt werden, die zweite erfüllt Anfragen, die mit der HTTP-Methode GET
gestellt werden:
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 }))
// Array to store messages
let messages = []
app.post('/', (req, res) => {
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Locate cookie in the request
let uuid = req.cookies.uuid
// If there is no uuid cookie, create a new one
if ( uuid === undefined )
uuid = uuidv4()
// Add message first in the messages array
messages.unshift({uuid: uuid, message: req.body.message})
// Collect all previous messages for uuid
let user_entries = []
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
// Update cookie expiration date
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// Send back JSON response
res.json(user_entries)
})
app.get('/', (req, res) => {
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Locate cookie in the request
let uuid = req.cookies.uuid
// Client's own messages
let user_entries = []
// If there is no uuid cookie, create a new one
if ( uuid === undefined ){
uuid = uuidv4()
}
else {
// Collect messages for uuid
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
}
// Update cookie expiration date
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// Send back JSON response
res.json(user_entries)
})
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
Wir haben die Konfiguration der statischen Dateien an den Anfang gestellt, weil wir gleich statische Dateien wie layout.css
benötigen. Neben der Middleware cookie-parser
, die bereits vorgestellt wurde, enthält das Beispiel auch die uuid
-Middleware, um eine eindeutige Identifikationsnummer zu erzeugen, die als Cookie an jeden Client weitergegeben wird, der eine Nachricht sendet. Falls nicht bereits im Verzeichnis des Beispielservers installiert, sollten Sie diese Module mit dem Befehl npm install cookie-parser uuid
installieren.
Das globale Array namens messages
speichert die von allen Clients gesendeten Nachrichten. Jedes Element in diesem Array besteht aus einem Objekt mit den Eigenschaften uuid
und message
.
Das wirklich Neue in diesem Skript ist die Methode res.json()
, die beide Routen am Ende nutzen, um eine Antwort im JSON-Format mit dem Array zu erzeugen, das die bereits vom Client gesendeten Nachrichten enthält:
// Send back JSON response
res.json(user_entries)
JSON ist ein einfaches Textformat, mit dem Sie eine Reihe von Daten in einer einzigen, assoziativen Struktur gruppieren, d.h. als Schlüssel-Werte-Paare. JSON ist besonders nützlich, wenn die Antworten vom Client verarbeitet werden sollen. Mit diesem Format kann ein JavaScript-Objekt oder Array auf Client-Seite leicht mit allen Eigenschaften und Indizes des ursprünglichen Objekts auf dem Server rekonstruiert werden.
Da wir jede Nachricht in JSON strukturieren, lehnen wir Anfragen ab, die in ihrem accept
-Header nicht application/json
enthalten:
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
Eine Anfrage über einen einfachen curl
-Befehl, um eine neue Nachricht einzufügen, wird nicht akzeptiert, da curl
standardmäßig nicht application/json
im accept
-Header angibt:
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt Not Found
Die Option -H "accept: application/json"
ändert den Request-Header, um das Format der Antwort anzugeben, die diesmal akzeptiert und im angegebenen Format beantwortet wird:
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt -H "accept: application/json" ["My first message"]
Das Abrufen von Nachrichten über die andere Route erfolgt auf ähnliche Weise, diesmal jedoch mit der HTTP-Methode GET
:
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -H "accept: application/json" ["Another message","My first message"]
Templates
Antworten in Formaten wie JSON sind praktisch für die Kommunikation zwischen Programmen, aber der Hauptzweck der meisten Webanwendungsserver besteht darin, HTML-Inhalte für die Benutzer zu erzeugen. Die Einbettung von HTML-Code in JavaScript-Code ist keine gute Idee, da die Vermischung von Sprachen in derselben Datei das Programm anfälliger für Fehler macht und die Wartung des Codes erschwert.
Express kann mit verschiedenen Template Engines arbeiten, die den HTML-Code für dynamische Inhalte separieren — eine vollständige Liste finden Sie auf der Seite Express Template Engines. Eine der beliebtesten Template Engines ist Embedded JavaScript (EJS), mit der Sie HTML-Dateien mit spezifischen Tags für das Einfügen dynamischer Inhalte erstellen.
Wie andere Express-Komponenten muss auch EJS in dem Verzeichnis installiert werden, in dem der Server ausgeführt wird:
$ npm install ejs
Danach definieren Sie die EJS-Engine als Standard-Renderer im Serverskript (am Anfang der Datei index.js
, vor den Routen-Definitionen):
app.set('view engine', 'ejs')
Die mit der Vorlage erzeugte Antwort wird mit der Funktion res.render()
an den Client gesendet, die als Parameter den Namen der Vorlagendatei und ein Objekt mit Werten erhält, auf die innerhalb der Vorlage zugegriffen werden kann. Die im vorherigen Beispiel verwendeten Routen können umgeschrieben werden, um sowohl HTML-Antworten als auch JSON zu erzeugen:
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: "My messages", 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: "My messages", messages: user_entries})
})
Beachten Sie, dass das Format der Antwort von dem in der Anfrage gefundenen accept
-Header abhängt:
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
Eine Antwort im JSON-Format wird nur gesendet, wenn der Client sie ausdrücklich anfordert. Andernfalls wird die Antwort aus dem Template index
generiert. Das gleiche Array user_entries
füllt sowohl die JSON-Ausgabe als auch das Template, aber das Objekt, das als Parameter für das Template verwendet wird, hat darüber hinaus die Eigenschaft title: "My messages"
, die als Titel in der Vorlage verwendet wird.
HTML-Templates
Wie statische Dateien befinden sich auch die HTML-Templates in einem eigenen Verzeichnis. Standardmäßig nimmt EJS an, dass sich die Vorlagendateien im Verzeichnis views/
befinden. Im Beispiel nutzen wir ein Template namens index
, also sucht EJS nach der Datei views/index.ejs
. Hier der Inhalt eines einfachen Templates views/index.ejs
, das mit dem Beispielcode verwendet werden kann:
<!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="Submit">
</p>
</form>
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
</div>
</body>
</html>
Der erste spezielle EJS-Tag ist das Element <title>
im Abschnitt <head>
:
<%= title %>
Während des Rendering-Prozesses wird dieses spezielle Tag durch den Wert der Eigenschaft title
des Objekts ersetzt, das als Parameter an die Funktion res.render()
übergeben wird.
Der größte Teil des Templates besteht aus konventionellem HTML-Code — das HTML-Formular zum Senden neuer Nachrichten. Der Testserver antwortet auf die HTTP-Methoden GET
und POST
für denselben Pfad /
, daher die Attribute action="/"
und method="post"
im Formular-Tag.
Andere Teile der Vorlage sind eine Mischung aus HTML-Code und EJS-Tags. EJS hat Tags für bestimmte Zwecke innerhalb des Templates:
<% … %>
-
Fügt eine Flusskontrolle ein. Mit diesem Tag wird kein Inhalt direkt eingefügt, aber er kann mit JavaScript-Strukturen verwendet werden, um Abschnitte von HTML auszuwählen, zu wiederholen oder zu unterdrücken. Beispiel für das Starten einer Schleife:
<% messages.forEach( (message) => { %>
<%# … %>
-
Definiert einen Kommentar, dessen Inhalt vom Parser ignoriert wird. Anders als in HTML geschriebene Kommentare sind diese Kommentare für den Client nicht sichtbar.
<%= … %>
-
Fügt den escapeten Inhalt der Variablen ein. Es ist wichtig, unbekannte Inhalte zu escapen, um die Ausführung von JavaScript-Code zu vermeiden, der Schlupflöcher für Angriffe per Cross Site Scripting (XSS) öffnen kann. Beispiel:
<%= title %>
<%- … %>
-
Fügt den Inhalt der Variablen ohne Escaping ein.
Die Mischung aus HTML-Code und EJS-Tags wird in dem Ausschnitt deutlich, in dem die Client-Nachrichten als HTML-Liste dargestellt werden:
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
In diesem Ausschnitt beginnt das erste <% … %>
-Tag eine forEach
-Anweisung, die alle Elemente des message
-Arrays durchläuft. Mit den Begrenzungszeichen <%
und %>
können Sie die HTML-Schnipsel kontrollieren. Ein neues HTML-Listenelement, <li><%= message %></li>
, wird für jedes Element von messages
erzeugt. Mit diesen Änderungen wird der Server die Antwort in HTML senden, wenn er eine Anfrage wie die folgende empfängt:
$ curl http://myserver:8080/ --data message="This time" -c cookies.txt -b cookies.txt <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>My messages</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="Submit"> </p> </form> <ul> <li>This time</li> <li>in HTML</li> </ul> </div> </body> </html>
Die Trennung von dem Code für die Verarbeitung der Anfragen und dem Code für die Darstellung der Antwort macht den Code sauberer und ermöglicht es einem Team, die Anwendungsentwicklung auf Personen mit unterschiedlichen Spezialgebieten aufzuteilen. Webdesigner können sich zum Beispiel auf die Vorlagendateien in views/
und die zugehörigen Stylesheets konzentrieren, die als statische Dateien im Verzeichnis public/
auf dem Beispielserver gespeichert sind.
Geführte Übungen
-
Wie konfigurieren Sie
express.static
, damit Clients Dateien im Verzeichnisassets
anfordern können? -
Wie kann der Typ der Antwort, der im Header der Anfrage angegeben ist, innerhalb einer Express-Route identifiziert werden?
-
Welche Methode des Routen-Parameters
res
(response) erzeugt eine Antwort im JSON-Format aus einem JavaScript-Array namenscontent
?
Offene Übungen
-
Standardmäßig befinden sich die Express-Templates im Verzeichnis
views
. Wie ändern Sie diese Einstellung, so dass die Vorlagendateien im Verzeichnistemplates
gespeichert werden? -
Ein Client erhält eine HTML-Antwort ohne Titel (d.h.
<title></title>
). Nach der Überprüfung der EJS-Vorlage finden Sie das Tag<title><% title %></title>
im Abschnitthead
der Datei. Was ist die wahrscheinliche Ursache für dieses Problem? -
Verwenden Sie EJS-Template-Tags, um ein HTML-Tag
<h2></h2>
mit dem Inhalt der JavaScript-Variablenh2
zu schreiben. Dieses Tag sollte nur gerendert werden, wenn die Variableh2
nicht leer ist.
Zusammenfassung
Diese Lektion behandelt die grundlegenden Methoden, die Express.js für die Erzeugung statischer und formatierter, dynamischer Antworten bereitstellt. Mit geringem Aufwand lässt sich ein HTTP-Server für statische Dateien einrichten, und das EJS-Templating-System bietet eine einfache Möglichkeit zur Erzeugung dynamischer Inhalte aus HTML-Dateien. In dieser Lektion werden die folgenden Konzepte und Verfahren behandelt:
-
Verwendung von
express.static
für Antworten mit statischen Dateien. -
Wie man eine Antwort erstellt, die dem Feld für den Inhaltstyp im Header der Anfrage entspricht.
-
JSON-strukturierte Antworten.
-
Verwendung von EJS-Tags in HTML-basierten Vorlagen.
Lösungen zu den geführten Übungen
-
Wie konfigurieren Sie
express.static
, damit Clients Dateien im Verzeichnisassets
anfordern können?Sie nehmen den Aufruf von
app.use(express.static('assets'))
in das Serverskript auf. -
Wie kann der Typ der Antwort, der im Header der Anfrage angegeben ist, innerhalb einer Express-Route identifiziert werden?
Der Client legt die zulässigen Typen im Header-Feld
accept
fest, das auf die Eigenschaftreq.headers.accept
abgebildet wird. -
Welche Methode des Routen-Parameters
res
(response) erzeugt eine Antwort im JSON-Format aus einem JavaScript-Array namenscontent
?Die
res.json()
-Methoderes.json(content)
.
Lösungen zu den offenen Übungen
-
Standardmäßig befinden sich die Express-Templates im Verzeichnis
views
. Wie ändern Sie diese Einstellung, so dass die Vorlagendateien im Verzeichnistemplates
gespeichert werden?Das Verzeichnis kann zu Beginn in den Skripteinstellungen mit
app.set('views', './templates')
definiert werden. -
Ein Client erhält eine HTML-Antwort ohne Titel (d.h.
<title></title>
). Nach der Überprüfung der EJS-Vorlage finden Sie das Tag<title><% title %></title>
im Abschnitthead
der Datei. Was ist die wahrscheinliche Ursache für dieses Problem?Das
<%= %>
-Tag sollte verwendet werden, um den Inhalt einer Variablen einzuschließen, wie in<%= title %>
. -
Verwenden Sie EJS-Template-Tags, um ein HTML-Tag
<h2></h2>
mit dem Inhalt der JavaScript-Variablenh2
zu schreiben. Dieses Tag sollte nur gerendert werden, wenn die Variableh2
nicht leer ist.<% if ( h2 != "" ) { %> <h2><%= h2 %></h2> <% } %>