Dieser Leitfaden dient einerseits der Vorbereitung und Durchführung der Trainings und zusätzlich kann der Leitfaden durch inhaltliche Ergänzungen so erweitert werden, dass er im Anschluss an die Teilnehmer verteilt werden kann.
Ziel des hier beschriebenen Kurses ist es, einen ersten Einblick in die Arbeit mit dem Framework Node.js zu bekommen, indem eine vollständige 3-Schichten-Applikation um gesetzt wird (Datenbank, Backend, Frontend).
WICHTIG: Dieses Dokument listet nur die allgemeinen Inhalte mit etwas Erklärung auf. Es dient daher nur der Begleitung des Kurses. Es ist nicht darauf ausgelegt, ALLE relevanten Inhalte vollständig zu erläutern.
Der Kurs wird in insgesamt 5 Teile aufgeteilt:
Wiederholung von Frontend-Technologien - 2 Tage: An diesen beiden Tagen wird jeweils eine kleine Minianwendung mit den Technologien React & Angular umgesetzt. Hierbei wird noch kein Backend angesprochen, um die Inhalte simpel zu halten.
Node.js & SQLite - 2 Tage: Wiederholung des Toolings rund um Node.js (node, npm, package.json etc.) und erste Arbeit mit der Datenbank
HTTP & Express - 2 Tage: Wiederholung der HTTP-Grundlagen und Umsetzung erster Endpunkte mit Express
Anbindung an Frontend und weitere Features - 2 Tage: Die umgesetztn Endpunkte sollen jetzt an eines der entwickelten Frontends angeschlossen werden. Zusätzliche Funktionen, wie Sessions oder Login können zusätzlich umgesetzt werden.
Projektarbeit: Das gelernte wird an einem neuen Use Case umgesetzt
Im Theorieteil des Kurses werden wir uns mit einer neuen Applikation beschäftigen, und als Zusatzinhalt wieder auf unsere bewährte Todo-Applikation zurückgreifen.

Wieso benutzen wir Frameworks?
In diesem Teil sollen die absoluten Basics von Angular & React beschrieben. Das sind beides Frameworks, mit denen Single Page Applications - SPA umgesetzt werden können. Also Applikationen, die im Browser laufen und sich eher wie Desktopanwendungen oder Apps verhalten, als wie Webseiten. Diese basieren darauf, dass JavaScript im Browser läuft, um auf Nutzereingaben zu reagieren.
Beide Frameworks sind Komponentenbasiert, das heißt, wir zerschneiden unsere Oberfläche in kleinere Blöcke, die wir wiederverwenden können.
🎓 Wissen
Angular bringt eine eigene CLI (Command Line Interface) mit, um Angular-Projekte aufzusetzen. Damit sind unter anderem die Tools TypeScript, Webpack & SCSS vorkonfiguriert, sodass wir diese nutzen können, ohne weitere Konfiguration vornehmen zu müssen.
Diese können wir über das npm-Tool npx benutzen:
npx --package @angular/cli@13.1.2 -- ng new angular-app
npx installiert dabei das Paket @angular/cli in der Version 13.1.2 (aktuellste
Version mit Stand 27.12.2021) und führt mit diesem Paket das ng-Kommando aus
und generiert damit ein neues Projekt mit dem Namen "angular-app". Die CLI führt
uns im Weiteren durch eine einfach Konfiguration (Achtung, die gestellten Fragen
hängen von der Version der Angular-CLI ab.)
Achtung: In diesem Kurs arbeiten wir mit modernem JavaScript/TypeScript. Wir arbeiten also nicht mit globalen Script-Dateien sondern nur mit Modulen aus denen wir bestimmte Werte exportieren, um sie in anderen Modulen zu importieren. Zur Wiederholung von Modulen kann die TypeScript-Dokumentation gelesen werden: https://www.typescriptlang.org/docs/handbook/2/modules.html
Aufgabe
🎯 Ziel: Ein neues Angular Projekt ist angelegt, kann gestartet werden und erste Interfaces sind definiert.
npm run start aus und navigiere im Browser
zu http://localhost:4200/src/app/api.types.ts
Author:
numberstringQuote:
numberstringAuthorIn Angular sind alle UI-Komponenten als Klassen definiert.
Klasse
Ähnlich wie in Java oder anderen Objekt-Orientierten Programmiersprachen können in TypeScript Klassen definiert werden. (siehe TypeScript-Dokumentation)
Klassen haben dabei Felder und Methoden.
Die Klasse beschreibt den Logik-Part einer Komponente. Also zum Beispiel, welche Daten in der UI dargestellt werden, was passiert, wenn ein Nutzer mit der Oberfläche interagiert, etc. Neben der Klasse gibt es das Template, und die Style-Dateien.
Template Im Template können wir einerseits normales HTML schreiben, um die
Oberfläche der Komponente zu beschreiben. Zusätzlich können direkt Berechnungen
im Template einbetten, indem wir die geschweiften Klammern nutzen: {{ 1 + 1 }}. In diesen Ausdrücken können wir auf alle Felder der dazugehörigen Klasse
zugreifen. Achtung: Die Ausdrücke im Template sollten so simpel wie möglich
sein, damit der Code wartbar und skalierbar bleibt.
Modul
Zusätzlich zu den TypeScript-Modulen bringt Angular seine eigenen Module mit, damit wir unsere Applikationen besser auftrennen können. In zukünftigen Angular-Versionen werden diese Module vermutlich nicht mehr nötig sein. Aktuell müssen aber alle Komponenten, die wir benutzen wollen, in einem Module aufgehängt sein.
Aufgabe
🎯 Ziel: Erste richtige Informationen werden in der Oberfläche dargestellt.
randomQuote mit dem Typ: Quoteapp.component.html-Datei.🎓 Wissen
Services Um reine Datenverarbeitung und Logik abzubilden, benutzen wir in Angular Services: Klassen, die Logik enthalten und die in Komponenten eingehängt werden können. Diese werden in Angular automatisch per Dependency Injection mit unseren Komponenten verknüpft. Das heißt, unsere Komponenten importieren nicht direkt die Funktionen, die benutzt werden sollen, sondern nur die Typdefinition. Angular kümmert sich dann automatisch darum, dass eine Instanz der Klasse erzeugt wird und an unsere Komponente übergeben wird.
Sie können mit Hilfe der Angular-CLI automatisch erzeugt werden:
npx ng generate service quotes
Services müssen nicht im Angular-Module registriert werden.
Mehr dazu in der Angular-Dokumentation.
Observables
In Angular werden asynchrone Daten (Daten, die erst nach einer bestimmten Zeit
verfügbar sind, wie zum Beispiel Resultate von Netzwerk-Abfragen) mit Hilfe von
Observables umgesetzt. Diese können im Laufe der Zeit 0, einen oder mehrere
Werte produzieren. Sie orientieren sich am
Observer-Pattern.
import { Observable, of } from "rxjs";
import { Quote } from "src/app/api.types";
function loadDummyData(): Observable<Quote> {
const dummyQuote: Quote = {
id: 1,
text: "Quote",
author: {
id: 1,
name: "Author",
},
};
// Wir benutzen die of-Funktion von Rx.js um
// ein Observable mit einem definierten Wert zu
// erzeugen.
// So können wir später den richtigen Netzwerk-Request
// einbauen, ohne unsere Komponente anzufassen.
return of(dummyQuote);
}
loadDummyData().subscribe((result) => {
console.log(result);
displayResult(result);
});
Rx.js bietet zusätzlich eine Reihe von Operatoren, mit denen Observables transformiert werden können.
import { delay, Observable, of } from "rxjs";
import { Quote } from "src/app/api.types";
function loadDummyData(): Observable<Quote> {
const dummyQuote: Quote = {
id: 1,
text: "Quote",
author: {
id: 1,
name: "Author",
},
};
// Wir nutzen den delay-Operator
// um alle Werte in unserem Observable um 1000 Millisekunden
// zu verzögern.
return of(dummyQuote).pipe(delay(1000));
}
loadDummyData().subscribe((result) => {
console.log(result);
displayResult(result);
});
Mehr dazu in der Angular-Dokumentation
Aufgabe
🎯 Ziel: Dummy Daten können geladen werden, um sie erstmal auf die Konsole zu schreiben.
quotes.service.ts (du kannst dafür auch die
Angular-CLI benutzen)
QuoteServicegetRandomQuote
Observable<Quote> haben.of-Funktion und den delay-Operator um Dummy-Daten nach 1000
Millisekunden zurückzugeben.app.component.ts an:
QuoteService injecten.getRandomQuote-Funktion auf und subscribe dich auf
das Observable, um das zufällige Zitat auf die Konsole zu schreiben.🎓 Wissen
Häufig haben wir die Situation, dass wir Teile der Oberfläche nur unter
bestimmten Bedingungen anzeigen wollen: Ein Menü soll nur sichtbar sein, wenn
der Nutzer auf einen Button geklickt hat, ein Button soll nur sichtbar sein,
wenn der Nutzer eingeloggt ist, oder eine Liste soll nur sichtbar sein, wenn
Daten fertig geladen sind. Dafür können wir in Angular ngIf benutzen:
<button *ngIf="4 > 3">Klick mich, wenn 4 größer als 3 ist</button>
<button *ngIf="isAuthenticated()">
Klick mich, wenn du authentifiziert bist.
</button>
Aufgabe
🎯 Ziel: Die Dummydaten werden richtig in der Oberfläche dargestellt und wir haben vorarbeit geleistet für reale Netzwerkkommunikation, indem wir einen Ladezustand mit abbilden.
app.component.ts an:
randomQuote auf Quote | null und initialisiere das
Feld mit nullrandomQuote nicht null liegt.randomQuote null liegt, soll in der Oberfläche Warte auf Daten... dargestellt werden.randomQuote.🎓 Wissen Bisher ist unsere Anwendung noch sehr langweilig, weil der Nutzer nicht damit interagieren kann. Wir möchten jetzt auf Events reagieren, das geht in Angular auch schön einfach mit Hilfe von Outputs:
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
})
export class AppComponent {
count = 1;
handleClick() {
console.log("click");
count++;
}
}
<!-- Achtung! Funktion wird erst bei Klick aufgerufen. -->
<button (click)="handleClick()">Klick mich {{ count }}</button>
Aufgabe
🎯 Ziel: Neue Zitate können geladen werden.
getRandomQuote Funktion so an, dass sie jedes mal ein anderes
Zitat zurück gibt (z.B. indem du mit Math.random() noch eine zufällige Zahl
zum Zitat hinzufügst.)🎓 Wissen Bisher haben wir nur eine Komponente. Das wird einerseits auf Dauer unübersichtlich und der große Vorteil von Komponentenbasierten Frameworks ist ja, dass wir Komponenten wiederverwenden können. Dafür müssen wir erstmal Komponenten umsetzen. Diese Komponenten können dabei über sog. Inputs konfiguriert werden.
@Component({
selector: "child-component",
template: `
<p>Hello {{userName}}</h3>
`,
})
export class HeroChildComponent {
@Input() userName!: string;
}
<child-component [userName]="'Peter ' + 'Pan'"></child-component>
Wenn wir auf Änderungen von Inputs reagieren wollen, funktioniert das am einfachsten über JavaScript setter:
@Component({
selector: "child-component",
template: `
<p>Hello {{userName}}</h3>
`,
})
export class HeroChildComponent {
_userName!: string;
@Input() set userName(newValue: string) {
console.log(`New Value: ${newValue}`);
this._userName = newValue;
}
}
Aufgabe
🎯 Ziel: Unsere erste Komponente mit Interface ist umgesetzt
quotes-of-author.component.ts (das kannst du
auch wieder über die Angular-CLI machen: npx ng g component QuotesOfAuthor)
QuotesOfAuthorInput ein Author-Objekt bekommenquotes-of-author.component.ts dar.app.component.ts dar. Achte darauf, dass die Komponente nur dargestellt
wird, wenn das Zitat nicht null ist.🎓 Wissen
Wir wollen jetzt alle Zitate des Authors darstellen. Dafür müssen wir über eine
Liste mit Zitat-Objekten iterieren, und für jedes ein Stück HTMl bauen. Das geht
in Angular mit *ngFor:
<ul>
<!-- Anstatt hier eine neue Liste zu erzeugen, können wir
natürlich auch auf ein Feld auf der Komponente zugreifen. -->
<li *ngFor="let i of [1, 2, 3]">{{ i }}</li>
</ul>
Aufgabe
🎯 Ziel: Eine Liste der Zitate des Autors wird geladen und ist sichtbar.
quotes.service.ts an:
getQuotesOfAuthor, die als Arugment ein
Author-Objekt bekommt und ein Observable<Quote[]> zurückgibt.of und delay und defiere Dummy-Daten. Achte dabei
darauf, dass der Autor der Dummy-Zitate gleich ist mit dem Übergebenen
Autor.quotes-of-author.component.ts an:
getQuotesOfAuthor genutzt
werden, um alle Zitate des Autors zu laden.ngFor, um alle Zitate des Autors in der Oberfläche darzustellen,
sobald sie geladen sind. Wenn sie noch nicht geladen sind, schreibe Warte auf Daten... in die Oberfläche🎓 Wissen
Ein großer Teil von Webanwendungen sind immer die Formulare in der Applikation. Gerade weil der Anwendungsfall so wichtig ist, hat Angular dafür eine eingebaute Bibliothek, die den Zustand von Formularen verwalten kann.
Um die Formularbibliothek zu nutzen, müssen wir sie erstmal im Angular-Modul registrieren:
import { ReactiveFormsModule } from "@angular/forms";
@NgModule({
imports: [
// other imports ...
ReactiveFormsModule,
],
})
export class AppModule {}
Folgend können wir in Komponenten Formulare definieren:
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
})
export class AppComponent {
form = new FormGroup({
username: new FormControl(""),
password: new FormControl(""),
});
handleSubmit() {
console.log(this.form.value);
}
}
Die Form Controls müssen dann noch mit der UI verknüpft werden:
<!-- unsere FormGroup wird an den [formGroup] input gegeben -->
<form [formGroup]="form" (ngSubmit)="handleSubmit()">
<!-- Das for Attribut verknüpft uns Label und input -->
<label for="username">First Name: </label>
<!-- Alle Controls brauchen dann den Namen des Feldes: -->
<input id="username" type="text" formControlName="username" />
<br />
<label for="password">Last Name: </label>
<input id="password" type="password" formControlName="password" />
<!-- Submit Button triggert automatisch das ngSubmit event -->
<button type="submit">Speichern</button>
</form>
Mehr dazu in der Angular-Dokumentation
Aufgabe
🎯 Ziel: Ein Formular ist sichtbar und kann abgeschickt werden.
quotes.services.ts an:
createQuote(text: string, authorName: string) mit dem Rückgabewert: Observable<Quote>of und delay)new-quote.component.ts und stelle sie in
der App-Komponente dar.
quote und authorName.createQuote
soll aufgerufen werden. Achtung: Das angelegte Zitat wird noch nirgends
in der UI angezeigt. Das ist okay!QuoteService so an, dass die Daten im LocalStorage
gespeichert werden.
(Dokumentation)React ist, genau so wie Angular, ein Komponentenbasiertes UI-Framework. Allerdings verfolgt React die Philiosophie, weit weniger Entscheidungen schon im voraus zu treffen. Diese Entscheidungen liegen dann also bei uns. Einerseits bietet das viel Flexibilität, da man sich seine Architektur so zusammenbauen kann, wie es für das Projekt geeignet ist, andererseits setzt das auch die Kompetenz voraus, diese Entscheidungen treffen zu können.
Anders als bei Angular gibt es bei React keine offizielle CLI, die in "allen" Projekten genutzt wird. Wir werden für die folgenden Aufgaben den Vite-Bundler nutzen. Dieser ist sehr einfach zu konfigurieren, extrem performant und trotzdem flexibel genug für Enterprise-Applikationen.
Über Vite können trotzdem sehr schnell und einfach Projekte aufgesetzt werden:
npm init vite@latest
Jetzt bekommen wir eine Reihe von Fragen, die uns durch das Setup führen.
Aufgabe
🎯 Ziel: Ein neues React Projekt ist angelegt, kann gestartet werden und erste Interfaces sind definiert.
npm init vite@latest um eine neue React-App mit TypeScript-Support zu
erstellennpm installnpm run devapi.ts
Author:
numberstringQuote:
stringnumberAuthor🎓 Wissen Komponenten werden in React mit einfachen Funktionen definiert. Diese Funktionen haben einen etwas merkwürdigen Rückgabetyp:
function App() {
return <button>Click me</button>;
}
Es sieht aus, als würde unsere Funktion HTML zurückgeben. In Wirklichkeit, wird hier aber nur ein Objekt erzeugt, welches beschreibt, wie die Oberfläche auszusehen hat, React kümmert sich dann darum, dass das im Browser auch wirklich genau diese Elemente dargestellt werden. In einem TSX-File sind wir dadurch immer in 2 unterschiedlichen Kontexten: Auf TypeScript-Ebene oder auf TSX ebene, wo wir HTMl schreiben können. Um von der TypeScript-Welt in die TSX-Welt zu kommen, schreiben wir einfach einen TSX-Ausdruck:
function Component() {
const value1 = 1;
const value2 = 2;
// value1 wird als Text interpretiert!
return <button>TSX here! value1</button>;
}
Und um wieder zurück in die TypeScript-Welt zu kommen, nutzen wir geschweifte Klammern:
function Component() {
const value1 = 1;
const value2 = 2;
// value1 wird als Text interpretiert!
return <button id={`id-${value1}`}>{valur1 + value2}</button>;
}
Mehr Details gibt es in der React Dokumentation.
Aufgabe
🎯 Ziel: Erste richtige Informationen werden in der Oberfläche dargestellt.
App.tsx, den wir nicht mehr brauchen.App-Komponente eine neue Variable mit
dem Typ: Quote🎓 Wissen
Veränderliche Daten müssen in React explizit mit Hilfe der useState-Hook
deklariert werden. Diese kann nur im "Hauptbereich" einer Komponente benutzt
werden, NICHT innerhalb von Event-Handlern oder Schleifen.
function Button() {
// Use state gibt ein Tupel zurück:
// Eine Liste mit zwei Elementen
// 1. dem aktuellen Zustand
// 2. einer Funktion zum Updaten des Zustands
const [count, setCount] = useState(1);
// Updates können NUR über setCount gespeichert werden.
// Die count Variable selbst kann von uns nicht editiert werden
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
React Dokumentation zu Interaktivität in React-Apps
Aufgabe
🎯 Ziel: Das aktuelle Zitat kann per Knopfdruck ausgetauscht werden
🎓 Wissen
React braucht keine Service-Klassen sondern nutzt öfter statische Funktionen.
Diese dürfen wir aber nicht so ohne weiteres in unseren Kompontenen aufrufen!
Die Funktionen, mit denen wir Komponenten definieren, werden mit JEDER Änderung
von Zuständen erneut aufgerufen. Wenn wir also beim Aufruf eine Netzwerabfrage
starten würden, würde diese bei JEDER Zustandsänderung erneut abgeschickt
werden. Solche Art von Nebenwirkungen nennt man in der Programmierung "Side
Effects". Diese werden in React mit der useEffect-Hook abgebildet. Auch hier
gilt wieder die Regel, dass diese Hook NIEMALS in einem Event-Handler oder in
einer Schleife aufgerufen werden darf.
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count);
// Wir geben hier die Abhängigkeiten dieser Nebenwirkung an
// Diese Funktion wird also immer dann aufgerufen, wenn sich count
// ändert.
}, [count]);
useEffect(() => {
console.log(count);
// Liste der Dependencies ist leer -> Wird nur beim ersten Durchlauf ausgeführt.
}, []);
return <button onClick={() => setCount(count + 1)}>Click</button>;
}
Mehr Informationen zu Funktionen und Nebenwirkungen gibt es hier
Netzwerkabfragen mit Promises React schreibt selber nicht vor, wie wir Netzwerkabfragen umsetzen sollen, Promises haben sich hier aber als Standardmodell durchgesetzt:
function loadData() {
// Hier erzeugen wir ein neues Promise, was erst nach 1000ms "fertig" ist.
const delayedPromise = new Promise((r) => setTimeout(r, 1000));
const delayedResult = delayedPromise.then(() => {
// Wird nach 1000ms ausgeführt.
return { x: 1 };
});
// Wir geben ein Promise zurück,
// welches nach 1000ms das Objekt {x: 1} zurückgibt.
return deplayedResult;
}
loadData().then((result) => {
// Wird erst aufgerufen, wenn loadData fertig ist.
console.log(result.x);
});
Aufgabe
🎯 Ziel: Dummy Daten können mit Hilfe einer zentralen API geladen werden.
api.ts an:
getRandomQuote
Promise<Quote>
haben.new Promise(r => setTimeout(r, 1000)); um die Dummy-Daten nach
1000 Millisekunden zurückzugeben.app.component.ts an:
🎓 Wissen
Anstatt eine eingebaute Syntax zu haben, nutzt React für Control-Flow im Template einfach JavaScript Konstrukte:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Zähler: {count}</button>
{count > 10 ? (
<p>Der Zähler ist schon ganz schön hoch...</p>
) : (
<p>Sehr niedriger Zähler</p>
)}
{count % 2 === 0 ? "Eine Gerade Zahl" : null}
</div>
);
}
Aufgabe
🎯 Ziel: Die Dummydaten werden richtig in der Oberfläche dargestellt und wir haben Vorarbeit geleistet für reale Netzwerkkommunikation, indem wir einen Ladezustand mit abbilden.
App-Komponente an:
null und gib den Generic von useState
manuell an als: useState<Quote | null>(null)randomQuote nicht null liegt.randomQuote null liegt, soll in der Oberfläche Warte auf Daten... dargestellt werden.randomQuote.🎓 Wissen
Was in der Angular Welt Inputs sind, sind in der React Welt "props". Die Arugmente der Komponten:
function Child(props: { name: string }) {
return <p>Hallo {props.name}</p>;
}
function Parent() {
const hardCodedName = "Test";
return <Child name={hardCodedName} />;
}
Aufgabe
🎯 Ziel: Unsere erste Komponente mit Interface
QuotesOfAuthor.tsx
QuotesOfAuthorprops ein Feld author vom Typ Author
bekommenQuotesOfAuthor-Komponente dar.App-Komponente. Achte darauf, dass die
Komponente nur dargestellt wird, wenn das Zitat nicht null ist.🎓 Wissen
Wie beim bedingten Rendern auch, hat React für Schleifen kein eigenes Konstrukt, sondern baut auf JavaScript:
function List() {
const list = [1, 2, 3];
return (
<ul>
{list.map((item) => (
// React möchte beim Rendern von Listen immer den Key Prop
// auf dem TSX-Element
// Dadurch kann die Performance stark verbessert werden.
// Der key muss für jeden Eintrag in DIESER Liste eindeutig sein.
<li key={item}>{item}</li>
))}
</ul>
);
}
Aufgabe
🎯 Ziel: Eine Liste der Zitate des Autors wird geladen und ist sichtbar.
api.ts an:
getQuotesOfAuthor, die als Arugment ein
Author-Objekt bekommt und ein Promise<Quote[]> zurückgibt.new Promise(r => setTimeout(r, 1000)); und defiere
Dummy-Daten. Achte dabei darauf, dass der Autor der Dummy-Zitate gleich ist
mit dem Übergebenen Autor.QuotesOfAuthor.tsx an:
getQuotesOfAuthor genutzt
werden, um alle Zitate des Autors zu laden.map, um alle Zitate des Autors in der Oberfläche darzustellen,
sobald sie geladen sind. Nutze die Id des Zitats als key. Wenn sie noch
nicht geladen sind, schreibe Warte auf Daten... in die Oberfläche.🎓 Wissen
React selbst bringt keine Formular-Bibliothek mit. Es gibt einige verbreitete Community-Projekte, wie zum Beispiel Formik. Wir werden das Formular aber erstmal ohne Bibliothek bauen.
Die einfachste Möglichkeit dafür ist, einfach ein useState pro Feld zu
definieren:
import { FormEvent, useState } from "react";
function LoginForm() {
const [name, setName] = useState("");
const [password, setPassword] = useState("");
function handleSubmit(event: FormEvent<HTMLFormElement>) {
// Achtung, in React müssen wir den Browserstandard selbst deaktivieren,
// sonst lädt die Seite mit jedem Submit neu.
event.preventDefault();
console.log({ name, password });
}
return (
<form onSubmit={handleSubmit}>
{/* Achtung: for gibt es in React nicht, wir müssen htmlFor verwenden */}
<label htmlFor="username">Name</label>
<input
id="username"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<label htmlFor="password">Passwort</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Speichern</button>
</form>
);
}
Aufgabe
🎯 Ziel: Ein Formular ist sichtbar und kann abgeschickt werden.
api.ts an:
createQuote(text: string, authorName: string) mit dem Rückgabewert: Promise<Quote>new Promise...)NewQuote.tsx und stelle sie in der
App-Komponente dar.
quote und authorName.createQuote
soll aufgerufen werden.api.ts so an, dass die Daten im LocalStorage gespeichert
werden.
(Dokumentation)Wie kommt es das JavaScript auch im Backend verwendet werden kann, wenn es ursprünglich mal für den Browser entwickelt wurde? Das lässt sich durch die folgende leichte Folgekette erklären:
Wenn wir in Projekten JavaScript bzw. TypeScript über den ganzen Stack hinweg einsetzen, entsteht ein riesiger Vorteil: Wir können ein Feature auf allen Ebenen implementieren mit nur einer Programmiersprache!
Im Laufe der nächsten 6 Tage werden wir an zwei Applikationen arbeiten: Dem Backend für unsere Zitate-App und einem anderen Backend für eine Todo-Anwendung. Die Zitate-App werden wir wieder in Zusammenarbeit am Vormittag entwickeln und nachmittags habt ihr dann Zeit, das gelernte in der Todo-App zu üben.
Aufgabe
🎯 Ziel: Wir haben ein Projekt, in dem wir TypeScript-Code mit Node ausführen können.
npm init --yes aus.
package.json-Datei angelegt.npm i typescript ts-node-dev @types/node aus, um die nötigen Pakete
für die TypeScript-Entwicklung zu installieren.package.json an: tsconfig.json und
übernimm den folgenden Inhalt:{
"compilerOptions": {
"target": "ESNext",
"module": "commonjs",
"moduleResolution": "node",
"noEmit": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true
}
}
./src/index.ts
Hallo Welt! auf die Konsole.package.json hinzu: "dev": "ts-node-dev ./src/index.ts"npm run dev aus.🎓 Wissen
Um Informationen über längere Zeit hinweg abzuspeichern (zu persistieren) brauchen wir ein weiteres Tool in unserem Stack: eine Datenbank! Es gibt eine Vielzahl von Datenbankmanagementsystemen (DBMS), die unterschiedliche Methoden anbieten, wie auf Daten zugegriffen werden kann.
Auch wenn viele dieser Systeme SQL im Namen tragen, verwenden sie leicht unterschiedliche Dialekte, so gibt es viele Funktionen, die von einem DBMS verstanden werden und von anderen nicht.
Wir wählen für unser Projekt SQLite, da wir hiermit sehr schnell starten können.
Aufgabe
🎯 Ziel: Wir machen uns vertraut mit der Datenbasis
SQLite von alexcvzz: Drücke
strg+shift+p und Suche nach Open Database und drücke Enter. Wähle die
vorgeschlagene Datenbank ausstrg+shift+p und Suche nach SQL View und wähle
Explorer: Focus on SQLite Explorer View🎓 Wissen
Um mit der Datenbank zu kommunizieren, verwenden wir das npm Paket: better-sqlite3:
import Database from "better-sqlite3";
const db = Database("../quotes.db", { fileMustExist: true });
// .all() lädt alle passenden Zeilen
const authors = db.prepare(`SELECT * FROM author`).all();
// .get() extrahiert direkt den ersten Eintrag
const author = db
.prepare(`SELECT id, name FROM author WHERE name = 'Yoda'`)
.get();
// Run führt ein statement ohne Rückgabe aus (insert, delete)
db.prepare(`INSERT INTO author (name) VALUES ('Peter')`).run();
// Variablen sollten IMMER über Platzhalter an die Abfrage
// übergeben werden!
// Sonst machen wir uns angreifbar für SQL-Injections
const otherAuthor = db
.prepare(`SELECT id, name FROM author WHERE name = ?`)
.get("Yoda");
// Mit ORDER BY RANDOM()
// können wir die Ergebnisse mischen
// mit LIMIT 1 nehmen wir dann nur die erste
// Zeile
const randomQuote = db
.prepare(
`
SELECT *
FROM quote
ORDER BY RANDOM()
LIMIT 1
`
)
.get();
Aufgabe
🎯 Ziel: Du verstehst, wie better-sqlite3 bedient wird und erinnerst dich, wie
SQL für die Abfrage von Daten funktioniert.
npm i better-sqlite3 @types/better-sqlite3 ausDatabase-Klasse in einem neuen File db.ts.
Exportiere diese Instanz aus diesem File.index.ts
ausführst:
Lösung für Aufgabe
Das Grundsetup ist immer das gleiche:
import Database from "better-sqlite3";
const db = Database("./quotes.db", { fileMustExist: true });
Abfrage 1
const q1 = db
.prepare(
`
SELECT count(*) 'Anzahl Zitate'
FROM quote
`
)
.get();
console.log(q1);
Abfrage 2
const q2 = db
.prepare(
`
SELECT count(*) 'Anzahl Autoren'
FROM author
`
)
.get();
console.log(q2);
Abfrage 3
const q3 = db
.prepare(
`
SELECT count(*) as 'Zitate von Sokrates'
FROM quote as q
LEFT JOIN author as a
ON a.id = q.authorId
WHERE a.name = 'Sokrates'
`
)
.get();
console.log(q3);
Abfrage 4
const q4 = db
.prepare(
`
SELECT q.text
FROM quote as q
LEFT JOIN author as a
ON a.id = q.authorId
WHERE a.name = 'Sokrates'
`
)
.all();
console.log(q4.map((q) => q.text));
Abfrage 5
const q5 = db
.prepare(
`
SELECT q.text, a.name
FROM quote as q
LEFT JOIN author as a
ON a.id = q.authorId
ORDER BY RANDOM()
LIMIT 1
`
)
.get();
console.log(q5);
Abfrage 6
const q6 = db
.prepare(
`
SELECT a.name, count(q.id) as Zitate
FROM quote as q
LEFT JOIN author as a
ON a.id = q.authorId
GROUP BY a.id
ORDER BY count(q.id) DESC
LIMIT 1
`
)
.get();
console.log(q6);
Abfrage 7
const q7 = db
.prepare(
`
INSERT INTO author
(name)
VALUES ('Petra')
`
)
.run();
Abfrage 8
const q8 = db
.prepare(
`
INSERT INTO quote
(text, authorId)
VALUES (?, ?)
`
)
.run("Neues Zitat", q7.lastInsertRowid);
console.log(q8);
Neu angelegte Daten wieder löschen:
const deleteQuoteResult = db
.prepare(
`
DELETE FROM quote
WHERE id > 10
`
)
.run();
console.log(deleteQuoteResult);
const deleteAuthorResult = db
.prepare(
`
DELETE FROM author
WHERE id > 10
`
)
.run();
console.log(deleteAuthorResult);
🎓 Wissen
Der Rückgabetyp von SQL-Abfragen ist immer any, weil TypeScript nicht weiß, wie unsere Datenbank strukturiert ist. Hier müssen wir manuell nachhelfen:
import Database from "better-sqlite3";
const db = Database("../quotes.db", { fileMustExist: true });
type Quote = {
id: number;
text: string;
};
function loadAllQuotes(): Quote[] {
const queryResult = db.prepare(`SELECT * FROM quote`).all();
// Erinnerung: array.map transformiert alle Elemente!
const quotes = queryResult.map(
// Wir geben manuell den Rückgabetyp an
(result): Quote => ({
id: result.id,
text: result.text,
})
);
// Alternativ:
const firstResult = queryResult[0];
const firstQuote: Quote = {
id: firstResult.id,
text: firstResult.text,
};
return quotes;
}
const allQuotes = loadAllQuotes();
// Wenn wir den Typ richtig angegeben haben,
// bekommen wir hier dann einen Fehler
// weil wir das Autor Feld nicht spezifiziert haben.
allQuotes[0].author;
Wenn wir Einträge anlegen, können wir auf die id des erzeugten Eintrags zugreifen:
export function getAuthorById(id: number): Author | null {
const result = db.prepare(`SELECT * FROM author WHERE id = ?`).get(id);
if (!result) return null;
return {
id: result.id,
name: result.name,
};
}
export function addAuthor(name: string): Author {
const result = db.prepare(`INSERT INTO author (name) VALUES(?)`).run(name);
// lastInsertRowid ist als Bigint typisiert, das müssen wir in
// Number umwandeln.
const added = getAuthorById(Number(result.lastInsertRowid))!;
return added;
}
Aufgabe
🎯 Ziel: Wir können aus unserer App mit der Datenbank interagieren und haben trotzdem Typsicherheit und Bequemlichkeit.
types.ts
Author:
numberstringQuote:
stringnumberAuthordb.ts an
getRandomQuote, die keine Argumente
bekommt und als Rückgabetyp Quote hat.
Quote-Interface vorgegeben
wird.getQuotesOfAuthor, die als Argument
ein Author-Objekt bekommt und alle Zitate dieses Autors lädt. Der
Rückgabetyp soll Quote[] sein.addQuote(quote: string, authorName: string): Quote
🎓 Wissen
Das Web basiert auf einem Text-basierten Frage-Antwort-Protokoll. Clients (wie zum Beispiel unser Web-Browser) schicken Anfragen an einen Server, der die Anfrage bearbeitet und Text zurück schickt. Dieser Text könnte HTML, CSS, ein Bild oder etwas anderes enthalten.
Jede Anfrage wird dabei an einen Host (z.B. developer.mozilla.org), auf eine bestimmte Route (z.B. /en-US/docs/Web/HTTP/Overview) und mit einer bestimmten Methode (GET für Abfragen, POST für Updates, etc.) geschickt. Zusätzlich enthält die Anfrage im Kopfbereich (Header) bestimmte Metainformationen, wie zum Beispiel die Liste der unterstützten Dateiformate oder Sprachen. Der Server verarbeitet diese Anfrage dann und antwortet mit einem Statuscode (2XX für OK, 4XX für Fehler auf der Client-Seite und 5XX für Fehler auf der Serverseite.) Neben dem Status-Code schickt auch der Server Header zurück und einen Body, also den Inhalt der Antwort.
In diesem Kapitel wollen wir unseren ersten Endpunkt mit dem express Framework implementieren.
// Wir importieren das installierte Paket
import express from "express";
// Und erzeugen eine neue App-Instanz
const app = express();
// Diese App soll GET-Anfragen auf / Hello World antworten.
app.get("/", function (req, res) {
// Express-handler werden immer mit min. 2 Argumenten aufgerufen:
// Dem Request-Objekt
// Dem Response-Objekt
console.log(req);
console.log(res);
res.send("Hello World");
});
// Alle anderen Methoden und Routen antworten mit "Backend running"
app.all("*", function (req, res) {
res.send("Backend running");
});
// Unser Backend läuft auf dem Netzwerk-Port 3001
app.listen(3001);
Aufgabe
🎯 Ziel: Ein erster Dummy-Endpunkt ist implementiert und kann im Browser betrachtet werden
npm i express @types/expressindex.ts an
app.all) und
alle Pfade (*) hört. Der Endpunkt soll Hello World Antworten. Zusätzlich
sollen vom Request-Objekt die Eigenschaften, body, headers und path
auf die Konsole gelogged werden.npm run dev läuft.http://localhost:3001🎓 Wissen
Express ist ein "Middleware" basiertes Web-Framework. Eine Middleware ist ein Konstrukt, welches bei jedem Request zwischen dem Eingang der Abfrage und dem Verarbeiten der Abfrage geschaltet ist. Die Middleware kann die eingehende Abfrage modifizieren. Middlewares können zusätzlich in Schichten aufgebaut werden: Jede Middleware kann entscheiden, ob die nächste Schicht die Anfrage überhaupt bekommen soll, oder nicht:
import express from "express";
const app = express();
// Middlewares werden mit .use definiert
app.use((req, res, next) => {
const time = Date.now();
console.log(`${req.method}-Request at ${req.path}`);
// Middlewares können das Request-Objekt modifizieren.
req.headers["timestamp"] = time.toString();
// Und die Anfrage an die nächste Middleware weiterleiten.
next();
});
app.use((req, res, next) => {
const secret = req.headers.secret;
if (secret !== "Streng geheim") {
// Wenn der Header nicht gesetzt ist,
// Geben wir den Status Code für fehlende Authentizierung zurück
// Und beenden den Request
res.status(401);
res.end();
} else {
// Nur wenn der Header gesetzt ist,
// Lassen wir den Request an weitere Middlewares weiter.
next();
}
});
app.all("*", (req, res) => {
console.log(req.body);
// Der Timestamp Header ist jetzt vorhanden!
console.log(req.headers);
console.log(req.path);
res.send("Hello World");
});
app.listen(3001);
Um jetzt den Body von Anfragen verarbeiten zu können, brauchen wir eine Middleware, die den Body in bestimmten Formaten einliest: Body Parser:
import bodyParser from "body-parser";
import express from "express";
const app = express();
app.use(bodyParser.json());
app.all("*", (req, res) => {
// Durch den json body-parser kann JSON content ausgelesen werden.
console.log(req.body);
res.send("Hello World");
});
Neben dem Request-Body können Informationen auch noch über Pfad-Parameter ans Backend übergeben werden:
app.get("/users/:userId", (req, res) => {
const userId = req.params.userId;
console.log(userId);
res.send(null);
});
Aufgabe
🎯 Ziel: Alle nätigen Endpunkte für unsere Zitate-App sind implementiert
npm i body-parser @types/body-parserGET /quotes/random: Gibt ein zufälliges Zitat zurückGET /authors/:authorId/quotes: Gibt alle Zitate des Autors mit der
übergebenen authorId an.POST /quotes: Über den Request Body werden die zwei Felder: authorName
und quote übergeben. Mit diesen Daten soll ein neues Zitat angelegt
werden. Das neue Zitat soll über den Response Body zurück gegeben werden.🎓 Wissen
Standardmäßig erlaubt der Browser nicht, dass Daten an URLs geschickt werden, die nicht auf der gleichen Domain liegen wie die aktuell geöffnete Seite. Wenn du also auf www.google.de bist, verbietet der Browser dir, dass du Daten von www.facebook.de anfordest. Dieses Konzept wird Cross-Origin-Resource-Sharing genannt.
Die Backendseite kann das aber explizit erlauben, indem die richtigen Header gesetzt werden. Diese Aufgabe kann für uns das cors Paket übernehmen:
import cors from "cors";
import express from "express";
const app = express();
// Ab jetzt werden Requests von JEDER
// Domain erlaubt und es ist auch erlaubt Cookies von jeder Domain
// mit zu schicken!
// Das sollte nur für die Entwicklung genutzt werden,
// da hieraus Sicherheitsrisiken entstehen!
app.use(
cors({
origin: (origin, callback) => callback(null, origin),
credentials: true,
})
);
🎯 Ziel: Dein Frontend benutzt dein richtiges Backend
npm i cors @types/corsapp.use(cors())🎓 Wissen
Aktuell stehen wir vor einem Problem: HTTP ist Zustandslos. Wir wissen nicht wer welche Requests macht, welche Requests davor passiert sind und welche Requests in der Zukunft vielleicht noch kommen. Wir betrachten Anfragen bisher komplett isoliert.
Die Lösung: Wir weisen dem Nutzer einen eindeutigen Identifikator zu, den der Nutzer bei jeder Anfrage wieder mit schickt. Diesem Identifikator könnten wir dann beliebige Informatioenn zuordnen. Das können wir im Web mit Cookies implementieren. Express bietet dafür das express-session Paket:
import express from "express";
import session from "express-session";
const app = express();
// Zunächst müssen wir die session-middleware registrieren
app.use(
session({
// Das secret wird dazu benutzt, die Session zu signieren
// So kann nicht einfach irgendein Nutzer seine eigenen
// Session-IDs generieren.
secret: "super secret passphrase",
// Zeit in Millisekunden, wie lange der Cookie gültig ist.
cookie: { maxAge: 1000 * 60 * 60 * 24 * 14 },
})
);
// Wir müssen express sagen, welche Felder
// Auf einer session abgelegt werden sollen
declare module "express-session" {
interface SessionData {
views: number | undefined;
}
}
app.get("/page", (req, res) => {
// Jetzt können wir die Felder auf session editieren
// und können sicher sein, dass zukünftige Anfragen
// Diese Session benutzen.
if (!req.session.views) req.session.views = 0;
req.session.views++;
res.send(`${req.session.views} Views`);
});
Achtung Cookies + CORS: Cookies und eine offene CORS-Einstellung sind generell nicht zu empfehlen, sind aber während der Entwicklung manchmal nötig. Hier ist darauf zu achten, dass im Frontend beim Laden der Daten mit angegeben werden muss, dass Cookies mitgeschickt werden sollen:
// Mit fetch
export function getRandomQuote(): Promise<Quote> {
return fetch(backendUrl + "/quotes/random", {
// "Bitte Cookies mitschicken, auch wenn der
// Request an eine andere Domain geht."
credentials: "include",
}).then((r) => r.json());
}
// Mit Angulars HttpClient
function getRandomQuote() {
return this.client.get<Quote>(`${this.backendUrl}/quotes/random`, {
withCredentials: true,
});
}
Aufgabe 🎯 Ziel: Nur eingeloggte Nutzer können auf die Zitate zugreifen
express-session
express-session mit npm i express-session @types/express-sessionapp hinzu. Setze dafür in der Config
die maxAge auf 1000 * 60 * 60 * 24 * 14loggedIn: boolean | undefinedGET /check-login: Prüft die aktuelle Session auf das loggedIn Feld und
gibt den Wert über den als boolean im Response-Body zurückPOST /login: Empfängt über den Request-Body zwei Felder username &
password. Prüfe ob Nutzername und Passwort zu von dir im Code hinterlegten
Zugangsdaten passen. Wenn ja, setze auf der session loggedIn auf true
und gib den StatusCode 200 zurück. Falls die Zugangsdaten nicht passen,
setze loggedIn auf false und gibt den StatusCode 401 zurückPOST /logout: Setzt auf der session loggedIn auf falseloggedIn mit dem Typ
boolean | null und initialisiere ihn mit null./check-login und legst das
Resultat in die loggedIn VariableloggedIn true ist, soll die bisherige App angezeigt werdenloggedIn null ist, soll ein Ladeindikator dargestellt werdenloggedIn false ist, soll eine neue Komponente LoginForm angezeigt
werden.LoginForm-Komponente
username und password und einen Login-Button/login
geschickt. Ist dieser erfolgreich, soll in der App-Komponente das
loggedIn-Feld auf true gesetzt werden. War er nicht erfolgreich, soll
das Login-Formular "Nutzername oder Password falsch" darstellen./logout geschickt und
die Seite neu geladen mit location.reload()Lade die Datenbank aus folgendem Link herunter: todos.db Sichte die Daten in der bereitgestellten Datenbank und versuche zu verstehen, wie die Daten zu interpretieren sind.
Setze ein neues TypeScript Projekt auf, mit dem das Backend umgesetzt werden soll (siehe Setup Kapitel)
Schreibe zunächst geeignete Interfaces, um Nutzer und Todos abzubilden. Achtung: SQLite hat keinen Datentyp für Booleans, deswegen wird meist ein Integer mit den Werten 0 bzw. 1 verwendet.
Implementiere die folgenden Funktionen, die mit der Datenbank interagieren:
createUser(name: string): User - legt einen neuen Nutzer in der Datenbank abcreateTodo(text: string, userId: number): Todo - erzeugen von Todos für einen NutzerupdateTodo(todo: Todo): Todo - aktualisiert die Felder eines TodosdeleteTodo(todoId: number): void - löscht ein TodogetUserById(id: number) User | null - sucht einen Nutzer mit der angegebenen IDgetTodosOfUser(userId: number): Todo[] - lädt alle Todos eines NutzersImplementiere die folgenden Endpunkte:
POST /users -> User - Empfängt über den Request-Body einen Nutzernamen und speichert diesen als Nutzer in der DatenbankGET /users/:id/todos -> Todo[] - Soll alle Todos des Nutzers zurückgebenPOST /users/:id/todos -> Todo - Empfängt über den Request-Body einen Todo-Text und speichert diesen als neues Todo in der Datenbank und gibt das erzeugte Todo zurückPATCH /todos/:id -> Todo - Empfängt über den Request-Body ein Todo-Objekt und aktualisiert alle Felder des Todos mit der angegebenen ID. Gibt das aktualisierte Todo-Objekt zurück.In der Session soll die id des aktuellen Nutzers abgelegt werden.
Implementiere die folgenden Endpunkte:
POST /login - Empfängt über den Request-Body einen Nutzernamen, sucht in der Datenbank einen Nutzer mit diesem Namen und legt die ID dieses Nutzers in die Session.GET /users/me -> User - Liest aus der aktuellen Session die Nutzer-ID, lädt das dazugehörige Nutzerobjekt aus der Datenbank und gibt diesen Nutzer zurück.POST /logout - Setzt die Nutzer-ID auf undefinedSetze ein neues Frontend-Projekt (React oder Angular) auf und implementiere eine Oberfläche für die Todo-App.