Historische Einordnung
Auf dem Weg zum “Web 2.0” …
… aber zunächst mit serverseitiger Dynamik
Verwendung von JavaScript
als Serverseitige Sprache
Nicht unbedingt eine gute Idee …
… aber aus Zeitgründen geboten
Schönere serverseitige Alternativen für große Projekte (hochgradig subjektiv)
Ruby mit dem Ruby on Rails-Framework
Java mit dem Play-Framework
Python mit dem Django-Framework
C# mit dem ASP.Net-MVC-Framework
Schönere serverseitige Alternativen für kleine Projekte
Ruby mit dem Sinatra-Framework
Python mit dem Flask-Framework
JavaScript ohne Browser
Keine wirklich neue Idee, verschiedene Implementierungen verfügbar
Verfügbare Standardbibliothek weichen von der “gewohnten” Browserumgebung ab
Vor allem typischerweise keine DOM
-Interaktion, keine Maus, keine Webcams, …
Wenn Sie im Internet nach weiteren Quellen suchen: Achten sie penibel darauf, welche Laufzeitumgebung dort verwendet wird. Die absolute Mehrzahl der Online-Tutorials setzt eine Browser-Umgebung voraus.
node.js
Serverseitige Umgebung für JavaScript Code
Nutzt Google V8 als JavaScript-VM
Stellt Operationen bereit, die auf dem Server relevant sind
Zugriff auf das Dateisystem
Öffnen von Ports für Serverdienste
Aufruf über die Kommandozeile möglich: node script.js
Führt das übergebene JavaScript-Programm aus
Kommt zusammen mit einem Paketmanager zur Verwaltung von Code-Abhängigkeiten
“Node Package Manager”, npm
Projekte definieren mit package.json
Grundidee: Meta-Beschreibung zusammenhängender Quelltexte als “Projekt”
Wie NetBeans-, Eclipse, Visual Studio-Projekte
Einfache JSON
-Datei die sich mit jedem Texteditor bearbeiten lässt
Vollständige Beschreibung des Formats auf docs.npmjs.com
package.json
als öffentliche Beschreibung des Projekts
Angaben wie name
, description
, homepage
und keywords
dienen der Auffindbarkeit auf npmjs.com
Versionsangabe (version
) zur Kommunikation von Kompatabilitäten
Lizenzangabe zu spezifizieren, unter welchen Bedingungen sie den Quelltext weitergeben
package.json
als Einstiegspunkt für Entwickler
Spezifikation von Abhängigkeiten, also anderen Programmen auf npmjs.com
Bei größeren Projekten: Angabe von vordefinierten Programmaufrufen
framework_projects/vorlesung/package.json
{
"name": "fhw-web-lecture-examples",
"version": "2.0.0",
"description": "Samples",
"homepage": "https://webanwendungen.fh-wedel.de/",
"author": "Marcus Riemer",
"license": "ISC",
"dependencies": {
"fhw-web": "^0.11.0"
}
}
Abhängigkeiten definieren mit package.json
Grundsätzlich: Fast jedes Projekt nutzt auch fremden Code
Nutzung von frei verfügbaren, quelloffenen Bibliotheken wünschenswert
Häufig: Software-Qualität bei OpenSource Projekten hoch
Gefährlich: Einige Pakete von Einzelpersonen und jahrelang ohne Pflege
Im dependencies
-Objekt: Angabe von benötigten Paketen (Schlüssel) nebst der gewünschten Version (Wert)
Konkret installierte Versionen werden in der package-lock.json
hinterlegt
Versionsangabe folgt dem “Semantic Versioning” (SemVer) Schema
Grundlegendes Format: Drei Zahlen mit besonderer Bedeutung (Major.Minor.Patch
)
Major
wird erhöht wenn inkompatible Änderungen eingeführt werden
Minor
wird erhöht wenn neue Funktionalität eingeführt wird, es aber keine “Breaking Changes” gibt
Patch
wird erhöht wenn Bugs behoben werden
Abhängigkeiten können auch Mindestversionen anfordern (Details in der SemVer Dokumentation )
Der Tilde-Operator (z.B. ~0.4.2
) erlaubt:
Neue Patch
-Versionen wenn eine Minor
-Version angegeben wurde
Neue Minor
-Versionen, wenn nur eine Major
-Version angegeben wurde
Der Caret-Operator (z.B. ^0.4.2
) verbietet Änderungen an der am “linkesten” stehenden Zahl, die nicht 0 ist.
^1.0.0
erlaubt beliebige neue Patch
und Minor
Versionen
^0.1.0
erlaubt beliebige neue Patch
Versionen
^0.0.1
erlaubt keine Änderungen
Wichtige npm
-Befehle
npm install
zur Installation aller Abhängigkeiten in den node_modules
Ordner
Berücksichtigt automatisch nur kompatible Versionen
npm run
zum Starten des eigentlichen Programms “hinter” dem Projekt
Startet standardmäßig die server.js
npm audit
(neu mit npm Version 6) zum Prüfen auf bekannte Sicherheitslücken in Abhängigkeiten
Keine Prüfung des eigenen Quelltextes
Das Modulsystem
Auf dem Server: Zugriff auf das Dateisystem möglich
Programme können daher den node_modules
Ordner auslesen
Grundprinzip: Angabe von Pfaden, die zu einem Modul führen
Öffentlich (exportierte) Eigenschaften diese Moduls können dann genutzt werden
Pfade die nicht mit einem .
beginnen werden ausgehend vom node_modules
-Ordner aufgelöst
Einstiegsdatei des jeweiligen Ordners wird in der package.json
im main
-Attribut angegeben
Pfade die mit einem Punkt beginnen, werden ausgehend von der aktuellen Datei aufgelöst
Erlaubt Einbindung von anderen Modulen im gleichen Projekt
Exaktes Präfix ist ./
, weil ja eine Datei aus einem Ordner geladen werden soll
Import von Common-JS-Modulen mit require
Technisch gesehen ein normaler Funktionsaufruf mit Zuweisung an eine Variable
Im Hintergrund wird dann Code nachgeladen
Diese Implementierung mit require
ist eine Besonderheit von node.js und steht daher im Browser nicht zur Verfügung *.
* Es sei denn man benutzt dort eine Bibliothek dafür, aber das ist eine andere Geschichte ... .
Export mit CommonJS
Technisch gesehen eine Zuweisung an ein global verfügbares Objekt: module.exports
Eben jenes Objekt wird beim Import mit require
bereitgestellt
Zur Erinnerung: Funktionen können wie Daten behandelt werden
Speichern von Funktionen in Java-Script Objekten ist also problemlos möglich
Das ist nicht das gleiche wie objektorientierte Programmmierung
const myAdd = (lhs, rhs) => lhs + rhs;
module.exports = {
"add": myAdd,
"sub": function(lhs, rhs) {
return (lhs - rhs)
},
"pi": {
"egypt": 256 / 81,
"greek": 22 / 7,
"physics": 3
}
}
const newMath = require('./module-common-ex')
console.log("Various values of pi", newMath.pi);
console.log("What is 1 + 2?", newMath.add(1,2));
Alternativ: Modulsystem mit ECMAScript 6
Die Zukunft beginnt heute: Auf dem Server könnten wir ein echtes Modulsystem nutzen 🙂
Praktisch aber leider nur mit …
Webserver mit dem fhw-Web Framework
Vorteil: Warum ein eigenes Framework?
Das node.js Programmiermodell steckt voller technischer Details, die sich in anderen Web-Frameworks nicht finden
Wesentliche Konzepte gehen hinter diesen Details verloren
Web-Frameworks sind stark schwankenden Trends unterworfen
Nachteil: Warum ein eigenes Framework?
Sie werden mit diesem Framework außerhalb der FH nie wieder zu tun haben
Dokumentation beschränkt sich auf offizielle Quellen
Eigene Handlebars-Funktionen
Bisher Fakt: Templates erlauben “keine komplexe Logik” 😞
Keine Vergleichausdrücke im if
, keine switch
-Anweisung, …
Aber: Handlebars lässt sich durch eigene JavaScript-Funktionen erweitern 🎉
Im Sinne der Handlebars-Entwickler: Komplexen Code auch als Code schreiben
Funktionsergebnis wird direkt in den HTML
-Quelltext eingesetzt
Sollte daher möglichst vom Typ string
sein.
Spezieller Ordner helpers
mit CommonJS-Modulen
Alle exportierten Funktionen dieses Ordners stehen als Helper bereit
Aufruf einfach durch Nennung des Funktionsnamens im Handlebars-Quelltext
Ohne Namensraum wie page
oder global
framework_projects/vorlesung/pages/datetime-1.hbs
{ "template": "html" }
---
<p>Heute ist der {{ currentDate }}. Es ist {{ currentTime }} Uhr.</p>
framework_projects/vorlesung/helpers/dateTime.js
module.exports = {
"currentDate": function() {
return (new Date().toLocaleDateString("de-DE"));
},
"currentTime": function() {
return (new Date().toLocaleTimeString("de-DE"));
}
};
Der Name des Helpers ergibt sich ausschließlich aus dem Namen im module.exports
-Objekt und muss dementsprechend über alle Module hinweg einzigartig sein.
Parameter für Handlebars-Helper
Aufruf: Leerzeichen-separierte Liste von Werten
Trennende Kommata sind ein Syntaxfehler
Aufruf: Übergabe von Daten aus dem Frontmatter, globalen Daten, Schleifen, … möglich
Pfad zum Daten so angeben wie in einem Interpolations-Ausdruck
JavaScript: Aufruf-Parameter werden direkt bereitgestellt
Generelles Verhalten wie bei jedem anderen Funktionsaufruf auch
framework_projects/vorlesung/helpers/helperParams.js
module.exports = {
"helperParams" : function(p1, p2, p3) {
return (JSON.stringify(arguments));
}
}
framework_projects/vorlesung/pages/helper-params.hbs
{
"template": "html",
"person" : {
"name" : "Jack Harkness",
"gender": "male",
"job": "Face of Bo"
}
}
---
<ul>
<li>{{ helperParams "Missy" "female" "Master"}}</li>
<li>{{ helperParams "Vastra" "female" "The Veiled Detective"}}</li>
<li>{{ helperParams "Jenny" "female"}}</li>
<li>{{ helperParams "Strax"}}</li>
<li>{{ helperParams page.person.name page.person.gender page.person.job }}</li>
</ul>
Sub-Expressions
Handlebars-Eigenart: Alle verschachtelten Ausdrücke müssen geklammert werden
Betrifft insbesondere eigene Helper (jeder Helper-Aufruf ist ein Ausdruck)
Vorsicht: Klammerung auch bei #if
- und #each
-Blöcken nötig
Technisch gesehen verarbeiten diese Blöck genau einen Ausdruck
framework_projects/vorlesung/helpers/compare.js
module.exports = {
"greater": function(lhs, rhs) {
return (lhs > rhs);
},
"and": function(lhs, rhs) {
return (!!(lhs && rhs));
}
}
framework_projects/vorlesung/pages/helper-compare.hbs
{ "template": "html" }
---
{{#if (greater 2 4)}}
<p>2 > 4</p>
{{/if}}
{{#if (greater 4 2)}}
<p>4 > 2</p>
{{/if}}
{{#if (and (greater 4 2) (greater 2 4))}}
<p>4 > 2 AND 2 > 4</p>
{{/if}}
{{#if (and (greater 4 2) (greater 6 4))}}
<p>4 > 2 AND 6 > 4</p>
{{/if}}
framework_projects/vorlesung/helpers/money.js
const currencySign = function(currency) {
switch (currency) {
case "EUR": return ("€");
case "USD": return ("$");
case "BTC":
case "THB":
return ("฿");
}
}
module.exports = {
"currencyConvert": function(amount, from, to) {
if (from === to) {
return (amount);
} else if (from === "EUR" && to === "USD") {
return (amount * 1.17);
} else if (from === "USD" && to === "EUR") {
return (amount * 0.84);
} else if (from === "BTC" || to === "BTC") {
return (Math.random() * amount);
}
else {
return (`Error: Can't convert from "${from}" to "${to}"`);
}
},
"currencyDisplay": function(amount, currency) {
amount = Math.floor(amount); // We don't consider sub-cents
const minor = amount % 100;
const major = Math.floor(amount / 100);
return (`${major}.${minor}${currencySign(currency)}`);
},
"currencySign": currencySign
};
framework_projects/vorlesung/pages/money.hbs
{
"template" : "html",
"price": {
"amount": 1234,
"currency": "EUR"
}
}
---
<h1>Money & Sub-Expressions</h1>
<ul>
<li>Raw: {{ page.price.amount }} {{ page.price.currency }} (Cent!)</li>
<li>currencySign "USD": {{ currencySign "USD" }}</li>
<li>currencySign page.price.currency: {{ currencySign page.price.currency }}</li>
<li>
currencyDisplay:
{{ currencyDisplay page.price.amount page.price.currency }}
</li>
<li>
currencyConvert:
{{ currencyConvert page.price.amount page.price.currency "USD" }}
</li>
<li>
currencyDisplay(currencyConvert):
{{ currencyDisplay (currencyConvert page.price.amount page.price.currency "USD") "USD" }}
</li>
</ul>
Fehler in Helpern
Häufigste Art von Fehlern: Plötzlich undefined
or NaN
Werte
Nur NaN
ist im Quelltext zu sehen
Im Falle von auftretenden Ausnahmen: Anzeige eines Stacktrace
Typischerweise zeigt der oberste Eintrag den Fehler
Model-View-Controller
MVC nach der englischen Wikipedia
Grundlegendes Prinzip der meisten Web-Frameworks: Model-View-Controller (MVC)
Vorsicht: Kein absolut einheitliches Vorgehensmodell
Model: Beschreibt die zugrundeliegenden Daten und regelt den Zugriff darauf
Benutzt die Fachsprache der Domäne
Entspricht häufig einer Zeile in der Datenbank …
… abstrahiert im Normalfall aber das Speichermedium (Datenbank, Datei, …)
View: Beschreibt auf welche Arten und Weisen Daten dargestellt werden
Häufig in Form einer Templatingsprache
Controller: Reagieren auf Benutzereingaben und nehmen Aktualisierungen an den Modellen vor
Jede Veränderung der Daten muss durch einen Controller vorgenommen werden
Model-View-Controller im fhw-Web Framework
Model: Alle Daten werden in JSON
-Dateien gespeichert
Zugriff über spezielle Funktionen zum Laden und Speichern von Dokumenten
Keine weiteren Vorgaben, insbesondere keine Objektorientierung
View: Wie gehabt über .hbs
-Templates
Daten werden allerdings nicht mehr vollständig im Frontmatter notiert
Controller: Spezielle Javascript-Callback-Funktionen, die für bestimmte Routen hinterlegt werden
Verarbeiten Eingabeparameter, laden Daten und wählen Templates aus
Abweichung zum “typischen” MVC: Nehmen auch unmittelbar Veränderungen an den Daten vor
Routendefinitionen
Bisher: Anfragen werden “magisch” auf entsprechend benannte Dateien im pages
-Ordner gemappt
Keine Angabe einer Controller-Funktion nötig
Neu: Angabe von speziellen Routen in der routes.json
Obiges Standardverhalten lässt sich damit wieder herstellen
Grundlegende Syntax: Array von speziellen JSON
-Objekten
Für jede Anfrage wird das Array durchlaufen und die Anfrage-URL
mit der “Zuständigkeit” verglichen
Zuständigkeit ergibt sich aus dem url
-Wert des Objekts
Erste zutreffendes Objekt wird zur Verarbeitung genutzt
URL
-Angaben in Routen
Konkrete Zeichenketten: /impressum
, /user/create
, …
Müssen 1:1 dem Pfad der aufgerufenen URL
entsprechen
Sternchen (*
): Beliebige Zeichen auf einer Ordner-Ebene
/assets/*
betrifft also /assets/william-hartnell.jpg
…
… aber nicht /assets/companion/k9.jpg
(Unterordner)
/*
betrifft einfach alle Anfragen der obersten Ebene
Besonderheit: *
kann als Wert für eine page
- oder static
-Angabe verwendet werden
Benannte Pfad-Segmente (:name
): Alle Zeichen bis zum nächsten Segment (/
) oder dem Ende
/doctor/:name
betrifft also /doctor/troughton
aber nicht /doctor/smith/john
/doctor/:name/:first
betrifft also nicht /doctor/pertwee
aber schon /doctor/smith/john
Grundsätzlich möglich: HTTP
-Verben einschränken
Vor allem später für Controller relevant
Ziele von Routen
page: <string>
: Angabe einer zu rendernden Seite
string
-Angabe bezeichnet eine Datei im pages
-Ordner
static: <string>
: Angabe einer auszuliefernden Datei
string
-Angabe bezeichnet eine Datei in einem beliebigen Ordner
Sollte unbedingt auf Unterordner eingeschränkt werden, andernfalls können z.B. die package.json
oder andere Unterordner ausgelesen werden
controller: { file: <string>, function: <string> }
: Verweis auf eine JavaScript-Callback-Funktion die aufgerufen werden soll
object.file
ist der Name der Datei (ohne .js
-Endung) im controllers
-Ordner
object.function
ist der Name der exportierten Callback-Funktion in obiger Datei
Die “magischen” Routen
Folgende Definition wurde bisher implizit vom Framework verwendet
Immer wenn keine routes.json
existiert
Zeile 2-5
Alle Pfade die mit /assets/
beginnen aus dem /assets/
-Ordner bedienen
Zeile 6-9
Alle anderen Pfade müssen auf eine Seite verweisen
[
{
"url": "/assets/*",
"static": "assets/*"
},
{
"url": "/*",
"method": ["get"],
"page": "*"
}
]
Zugriff auf Routenparameter mit dem request
-Objekt
Zugriff auf URL
-Parameter für page
- und controller
-Ziele
get
-Parameter sind alle Name-Wert-Paare nach dem ?
in der URL
path
-Parameter sind benannte Teile des Pfades
Beim rendern von Seiten: Zugriff primär nützlich zu Debugzwecken
Nur sehr eingeschränkt Verarbeitung der Parameter möglich
[
{
"url": "/hello/:name",
"page": "params-hello",
"params": {
"get": ["number"]
}
}
]
framework_projects/vorlesung/pages/params-hello.hbs
{ "template" : "html" }
---
<h1>Hallo {{ request.path.name }} </h1>
{{#if request.get.number }}
<p>Deine Zahl ist {{ request.get.number }}</p>
{{/if }}
Controller-Funktionen
Jede Controller-Callback-Funktion erhält beim Aufruf genau einen Parameter
Daten-Objekt (data
) mit den folgenden Eigenschaften:
data.request.get
: Alle GET
-Parameter der Anfrage
data.request.post
: Alle POST
-Parameter der Anfrage
data.request.path
: Alle Pfad-Parameter der Anfrage
data.session
: Anfrageübergreifende Sitzung
data.global
: Daten aus global.json
Jede Controller-Funktion muss als Ergebnis ein spezielles Objekt zurückliefern
page
rendert eine Seite, optionalerweise mit in der Funktion berechneten Daten
redirect
weist den Browser dazu an eine andere URL
zu laden
Rückgabeobjekt für Seiten
page
(Pflicht): Dateiname der zu rendernden Seite
Ohne Endung .hbs
data
(Optional): Daten-Objekt für das Template
Dieses Objekt steht beim rendern im page
-Namensraum zur Verfügung
status
(Optional): HTTP
-Statuscode
Wenn nicht angegeben: 200
Beispiel: Taschenrechner (I)
Zweck: Addieren oder subtrahieren, jeweils auf einer eigenen Route
Trotzdem Verwendung von nur einem Template
Problem: Parameter sind grundsätzlich Strings
Umwandlung erforderlich
framework_projects/vorlesung/pages/calculator.hbs
{ "template": "html" }
---
<code>
{{ page.lhs}} {{ page.op }} {{ page.rhs }} = {{ page.result }}
</code>
framework_projects/vorlesung/controller/calculator-v1.js
module.exports = {
"broken": function(data) {
return ({
"page": "calculator",
"data": {
"lhs": data.request.get.lhs,
"op": "+",
"rhs": data.request.get.rhs,
"result": data.request.get.lhs + data.request.get.rhs
}
});
},
"add": function(data) {
return ({
"page": "calculator",
"data": {
"lhs": data.request.get.lhs,
"op": "+",
"rhs": data.request.get.rhs,
"result": +data.request.get.lhs + +data.request.get.rhs
}
});
}
}
[
{
"url": "/calculator/v1/add",
"method": ["get"],
"controller": {
"file": "calculator-v1",
"function": "add"
},
"params": {
"get": ["lhs", "rhs"]
}
},
{
"url": "/calculator/v1/broken",
"method": ["get"],
"controller": {
"file": "calculator-v1",
"function": "broken"
},
"params": {
"get": ["lhs", "rhs"]
}
}
]
Beispiel: Taschenrechner (II)
Änderung: Mathematische Operationen mit nur einer Route und einem Template
Controller wählt passende Operation automatisch aus
framework_projects/vorlesung/pages/calculator.hbs
{ "template": "html" }
---
<code>
{{ page.lhs}} {{ page.op }} {{ page.rhs }} = {{ page.result }}
</code>
framework_projects/vorlesung/controller/calculator-v2.js
module.exports = {
"calc": function(data) {
const lhs = data.request.get.lhs;
const rhs = data.request.get.rhs;
const opName = data.request.path.op;
const operations = {
"add": (op1, op2) => op1 + op2,
"sub": (op1, op2) => op1 - op2,
"mul": (op1, op2) => op1 * op2,
"div": (op1, op2) => op1 / op2,
"mod": (op1, op2) => op1 % op2
};
const op = operations[opName];
return ({
"page": "calculator",
"data": {
"lhs": lhs,
"op": opName,
"rhs": rhs,
"result": op(+lhs, +rhs)
}
});
}
}
Beispiel: Taschenrechner (III)
Änderung: Fehlerbehandlung bei falschen Operationen oder Zahlen
Prüfung der Zahlen mit !isNaN
(“Ist das nicht eine Nicht-Zahl?”)
Prüfung der Operation auf einen “truthy”-Wert
framework_projects/vorlesung/pages/calculator.hbs
{ "template": "html" }
---
<code>
{{ page.lhs}} {{ page.op }} {{ page.rhs }} = {{ page.result }}
</code>
framework_projects/vorlesung/controller/calculator-v3.js
module.exports = {
"calc": function(data) {
const operations = {
"add": (op1, op2) => op1 + op2,
"sub": (op1, op2) => op1 - op2,
"mul": (op1, op2) => op1 * op2,
"div": (op1, op2) => op1 / op2,
"mod": (op1, op2) => op1 % op2
};
const opName = data.request.path.op;
const op = operations[opName];
const lhs = data.request.get.lhs;
const rhs = data.request.get.rhs;
if (op && !isNaN(lhs) && !isNaN(rhs)) {
return ({
"page": "calculator",
"data": {
"lhs": lhs,
"op": opName,
"rhs": rhs,
"result": op(+lhs, +rhs)
}
});
} else {
return ({
"page": "calculator-error",
"status": 400
});
}
}
}
Beispiel: Taschenrechner (IV)
Änderung: Anzeige eines passenden Operators über einen Helper
Statt “add”, “mod”, …
framework_projects/vorlesung/pages/calculator-operator.hbs
{ "template": "html" }
---
<code>
{{ page.lhs}} {{ opDisplay page.op }} {{ page.rhs }} = {{ page.result }}
</code>
framework_projects/vorlesung/helpers/calculator.js
module.exports = {
"opDisplay": function(code) {
switch(code) {
case "add": return ("+");
case "sub": return ("-");
case "mul": return ("×");
case "div": return ("÷");
case "mod": return ("%");
}
}
}
framework_projects/vorlesung/controller/calculator-v4.js
module.exports = {
"calc": function(data) {
const operations = {
"add": (op1, op2) => op1 + op2,
"sub": (op1, op2) => op1 - op2,
"mul": (op1, op2) => op1 * op2,
"div": (op1, op2) => op1 / op2,
"mod": (op1, op2) => op1 % op2
};
const opName = data.request.path.op;
const op = operations[opName];
const lhs = data.request.get.lhs;
const rhs = data.request.get.rhs;
if (op && !isNaN(lhs) && !isNaN(rhs)) {
return ({
"page": "calculator-operator",
"data": {
"lhs": lhs,
"op": opName,
"rhs": rhs,
"result": op(+lhs, +rhs)
}
});
} else {
return ({
"page": "calculator-error",
"status": 400
});
}
}
}
Umleitungen
Zur Erinnerung: HTTP
-Status Codes 3xx
stehen für eine Umleitung
Browser folgt Umleitungen normalerweise automatisch
Im Framework: Umleitung durch einen Controller durch Angabe des redirect
-Wertes auf dem Rückgabeobjekt
Gibt die URL
an, zu der die Umleitung erfolgen soll
Typischer Einsatzzweck: Transparente Änderungen an der Seitenstruktur
Alte URL
s sollten so lange wie möglich ihre Gültigkeit beibehalten
Typischer Einsatzzweck: Routen mit Seiteneffekten
Manche Aufrufe sollten beim Neuladen nicht wiederholt werden
Login auf einer Webseite
Getätigte Bezahlvorgänge
…
Umleitungen werden insbesondere im Kontext von HTTP
-POST
-Anfragen im kommenden Kapitel "Eingabeverarbeitung" noch vertieft.
Routen-Struktur
Wie in der ersten Vorlesung besprochen: Routen sollten sich niemals ändern …
… und sollten daher wohl überlegt sein.
Häufig: CRUD
-Zyklus für unterschiedlichste Arten von Resourcen
Gerade bei größeren Projekten sind daher Standard-Routen sinnvoll
Vorschlag: Routing-Konvention des Ruby on Rails-Frameworks
Verwendet eine andere Programmiersprache, aber ähnliche Routing-Konventionen wie unser Framework
HTTP Verb
Path
Controller#Action
Used for
GET
/photos
photos#index
display a list of all photos
GET
/photos/new
photos#new
return an HTML form for creating a new photo
POST
/photos
photos#create
create a new photo
GET
/photos/:id
photos#show
display a specific photo
GET
/photos/:id/edit
photos#edit
return an HTML form for editing a photo
PATCH/PUT
/photos/:id
photos#update
update a specific photo
DELETE
/photos/:id
photos#destroy
delete a specific photo
Dynamische Daten mit den “Datenbankfunktionen”
Typischer Anwendungsfall: Strukturierte Daten auslesen und anzeigen
Aus einer SQL
-Datenbank, JSON
-Dokumenten, …
Im Framework: Sehr einfacher “Document Store”
Verschiedene JSON
-Dokumente die als Ganzes geladen oder gespeichert werden können
Beispiel: Fast dynamische Daten
Zweck: Anzeige unterschiedlicher Seiten, je nach Name der Person (GET
-Parameter)
Verschiedene Seiten für verschiedene Personenkreise
Verschiedene Daten für verschiedene Personen
Problem: Daten sind Teil der Controller-Funktion
Umständlich zu schreiben und nicht aktualisierbar
framework_projects/vorlesung/pages/person-not-found.hbs
{ "template": "html" }
---
<h1>"{{ request.path.name }}" not found</h1>
framework_projects/vorlesung/pages/person-dr.hbs
{ "template": "html" }
---
<h1>Dr "{{ page.name }}"</h1>
framework_projects/vorlesung/pages/person-companion.hbs
{ "template": "html" }
---
<h1>Companion "{{ page.companion.name }}"</h1>
<ul>
{{#each page.other}}
<li>{{ name }}</li>
{{/each}}
</ul>
framework_projects/vorlesung/controller/greet.js
module.exports = {
"index": function(data) {
const people = [
{ "name": "capaldi", "type": "dr", "number": 12 },
{ "name": "smith", "type": "dr", "number": 11 },
{ "name": "tennant", "type": "dr", "number": 10 },
{ "name": "rose", "type": "companion", "numbers": [10] },
{ "name": "clara", "type": "companion", "numbers": [11, 12] }
];
const person = people.find(p => p.name === data.request.path.name);
if (person) {
if (person.type === "dr") {
return ({
"page": "person-dr",
"data": person
});
} else {
return ({
"page": "person-companion",
"data": {
"companion": person,
"other": people.filter(p => person.numbers.find(n => p.number === n))
}
});
}
} else {
return ({
"page": "person-not-found",
"status": 404
});
}
}
}
Laden und Filtern von Daten
Notwendig: Import des fhw-Web
Frameworks mit den folgenden Funktionen
Funktion loadJson(documentName)
:
documentName
: Name eines Dokuments im data
-Ordner (ohne Endung)
Ergebnis ist das geladene JSON
-Objekt
Prozedur saveJson(documentName, document)
documentName
: Name eines Dokuments im data
-Ordner (ohne Endung)
document
: Das zu speichernde JSON
-Objekt
Zweck: Daten nicht im Controller speichern, sondern aus einer Datenbank laden
Erlaubt lesende und schreibende Veränderungen
Daten anzeigen und Filtern
framework_projects/vorlesung/pages/people-data-v1.hbs
{ "template": "html" }
---
<h1>People Database</h1>
<ul>
{{#each page.people }}
<li>{{ name }}</li>
{{/each}}
</ul>
framework_projects/vorlesung/controller/people-data-v1.js
const fhwWeb = require('fhw-web');
module.exports = {
"index": function() {
const people = fhwWeb.loadJson("people");
return ({
"page": "people-data-v1.hbs",
"data": {
"people": people
}
});
},
"searchName": function(data) {
const searchedName = data.request.path.name;
const people = fhwWeb.loadJson("people")
.filter(p => p.name.indexOf(searchedName) >= 0);
return ({
"page": "people-data-v1.hbs",
"data": {
"people": people
}
});
}
}
framework_projects/vorlesung/data/people.json
[
{ "name": "capaldi", "type": "dr", "number": 12 },
{ "name": "smith", "type": "dr", "number": 11 },
{ "name": "tennant", "type": "dr", "number": 10 },
{ "name": "rose", "type": "companion", "numbers": [10] },
{ "name": "clara", "type": "companion", "numbers": [11, 12] }
]
Sessions und Cookies
Ursprüngliche Annahme im Web: Jede HTTP
-Anfrage steht für sich selbst
Alle für die Anfrage benötigten Daten sollen sich in der URL
widerspiegeln
Realität: Das Web ist voller Zustände, die nicht durch die URL
abgebildet werden
Warenkörbe, Login-Systeme, Spiele , …
Mögliche Ursache: Cookies, die bei jedem Aufruf mitgeschickt werden
Einige relevante Daten stehen also im Cookie und nicht in der URL
Entwickler-Tools geben Aufschluß über Cookies einer Anfrage
Typischerweise in einem Reiter “Netzwerk”
Andere Speicher als Cookies
Cookies als ursprüngliche Client-seitige Speichertechnik
Textuelle Daten die bei jeder Anfrage mitgeschickt werden
Mittlerweile zusätzlich: Web Storage
Speichert beliebige Daten für die Dauer der aktuellen Sitzung oder auch länger
Erlaubt Speicherung von Daten im Unfang von Megabytes (mit Einwilligung des Nutzers auch mehr)
Mittlerweile zusätzlich: Indexed DB
Speichert Daten in einer transaktional sicheren Schlüssel-Wert Datenbank (keine SQL-Datenbank, aber eine Datenkank)
Schnittstelle ist relativ kompliziert
Künftig zusätzlich: Cache-API und Service Worker
Service Worker erlauben es Anfragen an den Server zu unterbinden …
… um dann z.B. Daten aus dem Cache zu präsentieren
In dieser Veranstaltung: Eine oberflächliche Einführung in serverseitig gesetzte Cookies und serverseitige Speicherung von Daten in Sessions.
Anlegen von Cookies
Technisch gesehen: Kleine Textdateien, die beim Browser gespeichert werden
Browser kann die Speicherung auch ablehnen
Sowohl Server als auch Client können Daten in Cookies ablegen
Serverseitige Aufforderung zum Anlegen zweier Cookies
Erste Anfrage des Clients
Erfragt mit GET
die URL
www.example.org/index.html
Erste Antwort des Servers
Seite konnte geladen werden 200 OK
Bittet um das Anlegen zweier Cookies
theme=light
sessionToken=abc123
, dieses Cookie soll außerdem im Jahr 2021 ablaufen
Zweite Anfrage des Clients
Schickt einen Cookie
-Header mit den geforderten Werten mit
Im fhw-web
-Framework: Cookie zum Speichern einer Session-ID
Grundsätzliche Idee: Jeder Client schickt bei jedem Aufruf eine eindeutige Idee mit
Server speichert für jede eindeutige ID so genannte “Session”-Daten
Implementiertes Verfahren stellt automatisch ein session
-Objekt bereit
Prüfung bei jedem Aufruf: Hat der Client schon eine session-id
?
Ja : Laden der entsprechenden Daten aus dem session
-Ordner
Nein : Anlegen einer neuen Sitzung durch Vergabe einer neuen, zufälligen session-id
Änderungen an dem session
-Objekt durch den Controller werden automatisch gespeichert
Weder explizites Laden noch explizites Speichern einer Sitzung erforderlich
Sehr kurze Cookie-Lebensdauer: Nur für die Dauer einer Sitzung
Schließen des Browsers löscht dieses Cookie
Sitzungen zum Zählen
Inkrementieren eines Zählers für jeden Aufruf
Wenn kein Zähler vorhanden: mit 0 initialisieren
framework_projects/vorlesung/pages/session-count.hbs
{ "template": "html" }
---
<p>
Du warst {{ session.count }} mal hier,
deine ID ist <code>{{ session.session-id }}</code>.
</p>
framework_projects/vorlesung/controller/session-count.js
module.exports = {
"count": function(data) {
if (data.session.count === undefined) {
data.session.count = 1;
} else {
data.session.count++;
}
return ({
"page": "session-count"
});
}
}
Exkurs: Programmierung von node.js
Wesentliches Merkmal von node.js: Asynchrone Verarbeitung
Immer wenn eine Operation länger dauern könnte, sollte man Ihr einen Callback übergeben
Am Beispiel der Standardbibliothek zum Zugriff auf das Dateisystems (Modul: fs
)
Nach typischen UNIX
-Syscalls benannte Sammlung von Dateioperationen
Nutzen Sie diese Funktionen nicht in der Übung! Das fhw-web-Framework stellt Ihnen alle benötigten Funktionen zum Laden und speichern bereit.
Ausführungsmodell: “Single-Threaded, non blocking I/O”
Standardmäßig: Steuerung findet auf nur einem Thread statt
Ansatz profitiert nicht inhärent von modernen Mehrkernsystemen
Standardmäßig: Kooperatives Multitasking über Callbacks
Wenn absehbar auf externe Ereignisse gewartet werden muss, wird die Kontrolle abgegeben
Anfragen an die Datenbank
Zugriff auf das Dateisystem
…
node.js Ereignisverarbeitung
In Fachbegriffen: “Concurrent”, aber nicht “Parallel”
Es gibt immer nur einen Worker, der muss aber viele Baustellen gleichzeitig beackern
Nicht empfohlen: Synchroner Zugriff mit fs.readFileSync
Kein Code für serverseitige Umgebungen!
Lesen von großen Dateien blockiert andere Anfragen
[
{ "name": "John Smith" },
{ "name": "Donna Noble" },
{ "name": "River Song" }
]
const fs = require('fs');
try {
const data_string = fs.readFileSync('./data.json');
const data_json = JSON.parse(data_string);
console.log(data_json[0]);
} catch (err) {
console.log(err);
}
console.log("Program finished");
Asynchroner Zugriff mit fs.readFile
Irritierend: Programm beendet sich, bevor das Ergebnis ausgegeben wurde
Ursache: Node.js arbeitet noch ausstehende Ereignisse ab, wenn das Hauptprogramm sich beendet hat
[
{ "name": "John Smith" },
{ "name": "Donna Noble" },
{ "name": "River Song" }
]
const fs = require('fs');
fs.readFile('./data.json', (err, file_content) => {
if (!err) {
const data_json = JSON.parse(file_content);
console.log(data_json[0]);
} else {
console.log(err);
}
});
console.log("Program finished");
Warum kann die Reihenfolge nicht “richtig” sein?
Erst wenn das Hauptprogramm die Kontrolle abgegeben hat, können Callbacks ausgeführt werden
Keine Fehlerbehandlung mit try
und catch
möglich
Asynchroner Kontrollfluss verhindert das
Grenzen des node.js-Modells: Aufwändige Berechnungen
Auch wenn häufig Ein- und Ausgaben der Flaschenhals sind: Manchmal arbeitet die CPU doch
Und dann müssen alle anderen Anfragen warten
Beispiel verzichtet während der Berechnung komplett auf die Nutzung von Callback-Funktionen …
… und blockiert daher garantiert alle anderen Anfragen
Nutzung von Iterationsfunktionen wäre nicht ausreichend um “faires” Verhalten zu bekommen
Nur speziell vordefinierte Funktionen geben die Kontrolle an die Ereignisbehandlung zurück
framework_projects/vorlesung/pages/primes.hbs
{ "template": "html" }
---
{{#each page.primes}}
{{ this }},
{{/each}}
framework_projects/vorlesung/controller/prime.js
const isPrime = num => {
for (let i = 2; i < num - 1; ++i) {
if (num % i === 0) {
return (false);
}
}
return (num > 1);
}
module.exports = {
"prime": function(data) {
const from = +data.request.get.from;
const to = +data.request.get.to;
const primes = [];
for (let i = from; i < to; ++i) {
if (isPrime(i)) {
primes.push(i);
}
}
return ({
"page": "primes",
"data": {
"primes": primes
}
});
}
}
Aufgaben eines Webservers
Implementierung des HTTP
-Protokolls
Ausliefern von statischen (oder dynamischen) Inhalten
Im Beispiel: Auf alle URL
s mit dem gleichen statischen Inhalt reagieren
Zurückgegeben wird nicht HTML
, sondern einfach nur Text
Zeile 1
Laden des http
-Moduls
Zeile 3 & 4
Angabe von Port und Host von denen der Server Verbindungen annehmen wird
Zeile 6
Erstellung einer Server-Instanz für GET
-Anfragen unter Angabe eines Callbacks für eingehende Anfragen
Zeile 7 - 11
Jede Anfrage wird registriert und erfolgreich mit der Zeichenkette 'Hello World'
beantwortet
Zeile 12-14
Server tatsächlich starten und eine kurze Diagnosemitteilung machen
const http = require('http');
const hostname = '127.0.0.1';
const port = 3001;
const server = http.createServer((req, res) => {
console.log(`Request for ${req.url}`);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World\n');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
Dieser Webserver kann (sinngemäß) nichts, außer auf HTTP
-Anfragen zu antworten. Er kennt insbesondere keine Routen, Controller, Handlebars-Views, ...