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)
  1. Ruby mit dem Ruby on Rails-Framework
  2. Java mit dem Play-Framework
  3. Python mit dem Django-Framework
  4. C# mit dem ASP.Net-MVC-Framework
Schönere serverseitige Alternativen für kleine Projekte
  1. Ruby mit dem Sinatra-Framework
  2. 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.8.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
  • intro_node/module-common.js
    // Import des globalen Moduls "fs"
    const fs = require('fs');
    
    // Import der exportierten Eigenschaften der
    // lokalen Datei "my-funcs.js"
    const myFuncs = require('./my-funcs');
    
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
  • intro_node/module-common-ex.js
    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
      }
    }
    
  • intro_node/module-common-im-ex.js
    const newMath = require('./module-common-ex')
    
    console.log("Various values of pi", newMath.pi);
    console.log("What is 1 + 2?", newMath.add(1,2));
    
  • Various values of pi { egypt: 3.1604938271604937,
      greek: 3.142857142857143,
      physics: 3 }
    What is 1 + 2? 3
    

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 &gt; 4</p>
    {{/if}}
    
    {{#if (greater 4 2)}}
      <p>4 &gt; 2</p>
    {{/if}}
    
    {{#if (and (greater 4 2) (greater 2 4))}}
      <p>4 &gt; 2 AND 2 &gt; 4</p>
    {{/if}}
    
    {{#if (and (greater 4 2) (greater 6 4))}}
      <p>4 &gt; 2 AND 6 &gt; 4</p>
    {{/if}}

Beispiel: Helper zur Formatierung von Geldbeträgen

  • 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
  • framework_projects/vorlesung/helpers/error.js
    module.exports = {
      "thisWillUndefined": function(a) {
        return (a);
      },
      "thisWillNaN": function() {
        return (0/0);
      },
      "thisWillCrash": function() {
        return (a + b);
      }
    }
    
  • framework_projects/vorlesung/pages/this-will-error.hbs
    { "template" : "html" }
    ---
    <p><code>undefined</code>-Ergebnis: {{ thisWillUndefined }}</p>
    <p><code>NaN</code>-Ergebnis: {{ thisWillNaN }}</p>
    
  • /this-will-error
  • framework_projects/vorlesung/pages/this-will-crash.hbs
    { "template" : "html" }
    ---
    {{ thisWillCrash }}
  • /this-will-crash

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
  • fhw_web/routes.json
    [
      {
        "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
  • fhw_web/routes-params-hello.json
    [
      {
        "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
          }
        });
      }
    }
    
  • fhw_web/routes-calculator-v1.json
    [
      {
        "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 URLs 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”

Cookies einer Anfrage im Firefox

Cookies einer Anfrage im Chromium

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
  • fhw_web/http-server-cookie-2.txt
    HTTP/1.0 200 OK
    Content-type: text/html
    Set-Cookie: theme=light
    Set-Cookie: sessionToken=abc123; Expires=Wed, 09 Jun 2021 10:18:14 GMT
    …
    
Zweite Anfrage des Clients
Schickt einen Cookie-Header mit den geforderten Werten mit
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

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
  • intro_node/data.json
    [
      { "name": "John Smith" },
      { "name": "Donna Noble" },
      { "name": "River Song" }
    ]
    
  • intro_node/io-simple-sync.js
    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");
    
  • { name: 'John Smith' }
    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
  • intro_node/data.json
    [
      { "name": "John Smith" },
      { "name": "Donna Noble" },
      { "name": "River Song" }
    ]
    
  • intro_node/io-simple-async.js
    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");
    
  • Program finished
    { name: 'John Smith' }
    
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
          }
        });
      }
    }
    

Webserver mit http.createServer

Aufgaben eines Webservers
  • Implementierung des HTTP-Protokolls
  • Ausliefern von statischen (oder dynamischen) Inhalten
Im Beispiel: Auf alle URLs 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
  • intro_node/webserver-simple-1.js
    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}/`);
    });