dhbw phase 4: alle 8 kapitel slide-MD komplett (4176 zeilen → ~1100 zeilen, ~210 slides total)
kap1 web_eng (104 → 57 slides): kurs-intro + prüfungsleistung-übersicht, internet-timeline (4 milestones), URL→pixel-7-schritte, HTTP, onboarding (node 24 LTS via fnm, brew/winget paket-manager, VS Code/Chromium/Hoppscotch, git setup mit SSH-key, projekt-skeleton), HTML (anatomie, self-closing, grundgerüst, semantik-tabelle, native elemente details/dialog/input-types), A11y volle tiefe (BFSG+EAA 28.06.2025, WCAG-POUR, Perceivable/Operable/Understandable/Robust einzeln mit code, testen mit lighthouse/axe/manuell), CSS-vorschau (anatomie/selektoren/spezifizität/box-model/farben/einheiten/pseudos), JS-vorschau (ECMAScript-timeline/grundlagen/DOM/fetch), frameworks-übersicht (react/vue/svelte/astro counter-vergleich), build-tools/rendering-modi, selbstlernen erste-eigene-seite kap2 css_extended (13 → 28 slides): box-model + box-sizing global, flexbox basics+achsen+item-properties+typische-patterns, grid basics+areas+auto-layout, flexbox-vs-grid tabelle, mobile-first media queries, viewport-meta, fluid sizing mit clamp(), transitions (was performant), keyframe-animationen, prefers-reduced-motion pflicht, CSS-variablen + dark-mode-pattern, CSS functions (calc/clamp/gradient/oklch/filter), CSS-frameworks tabelle + tailwind pro/contra, selbstlernen flexboxfroggy+cssgridgarden kap3 nodejs_basics (~22 → 20 slides): was-ist-node + warum-node-tabelle, fnm versions-manager + node 24 LTS + .nvmrc, npm init + package.json (mit type:module), ESM modern + CJS legacy, built-in modules (node: prefix), HTTP-server eingebaut + express, REST mit express (GET/POST/PUT/DELETE-pattern), alternative frameworks tabelle (fastify/hono/koa), routing-pattern mit Router, fetch global ab node 18, error-handling res.ok, environment variables mit dotenv + niemals committen, selbstlernen mini-API kap4 nodejs_advanced (~24 → 22 slides): synchron-vs-asynchron, promise then-vs-async/await, sequenziell-vs-parallel + Promise.all, promise-methoden tabelle (all/allSettled/race/any), fs/promises lesen+schreiben, streams pipeline für große dateien, DB-übersicht SQLite/Postgres/MongoDB/Redis, better-sqlite3 prepared statements, ORM-übersicht (drizzle/prisma/knex), CRUD-pattern express+sqlite, auth-strategien tabelle, bcrypt für passwörter, JWT-pattern mit httpOnly+secure cookie, try/catch + error-klassen, globaler express-error-handler, selbstlernen REST+DB+auth kap5 testing (~23 → 23 slides): test-pyramide 70%/20%/10%, vitest setup + describe/it/expect, AAA-pattern, matchers tabelle, TDD red-green-refactor mit konkretem beispiel, supertest für API-integration, test-DB :memory: + lifecycle hooks, mocking mit vi.spyOn (sparsam), playwright E2E mit role-based-locators, E2E-vs-unit tradeoffs, coverage realistisch (80% gut), CI mit github-actions, selbstlernen tests-schreiben kap6 typescript (~30 → 19 slides): was-ist-TS + warum-tabelle, setup mit tsx + tsconfig.json + strict:true, basis-typen + any vermeiden, object-typen + interfaces + type-aliases, function-typen (Promise<T>), union + discriminated union + intersection, generics + constraints, built-in generics (Partial/Omit/Record), typ-inference (nicht über-annotieren), typen für REST-API mit express, zod runtime-validation + z.infer, any-vs-unknown-vs-never, best practices, selbstlernen API-zu-TS-migration kap7 docker (~24 → 24 slides): auf-meinem-rechner-läufts problem, container-vs-VM tabelle, docker basics (image/container/dockerfile/registry), dockerfile hallo-welt, schichten + cache-optimierung, multi-stage build, base-image-größen tabelle (alpine ~180MB vs node ~1.1GB), .dockerignore, compose.yml beispiel, compose-commands, networking (service-namen als hostnamen), volumes (named vs bind), env+secrets, github-actions docker-build, registry-übersicht, health-checks, best-practices, selbstlernen app-dockerizen kap8 best_practices (~30 → 28 slides): sinnvolle commits + conventional commits format, .gitignore template, branches+PRs workflow, README-template mit 6 sektionen, SOLID 5 prinzipien tabelle, S-single-responsibility + D-dependency-inversion mit code, 12-factor wichtigste (codebase/deps/config/processes/disposability), factor 3 config-in-env, factor 6 stateless-processes, SemVer MAJOR.MINOR.PATCH, package.json caret/tilde/exakt, code-reviews geben+nehmen + checkliste, doku-quellen tabelle (MDN/nodejs/npm/tc39/caniuse), stack-overflow gut-nutzen, AI-assistenten pro/contra (verstehen-nicht-kopieren), selbstlernen projekt-hygiene-check build geht durch alle 8 kapitel-html+pdf render-spot-check: cover-slide-dark-mode-invert, git-setup code-syntax-highlight, css-anatomie code-blocks — alle sauber kein deploy, nur build+verify
This commit is contained in:
+474
-1434
File diff suppressed because it is too large
Load Diff
+362
-201
@@ -8,217 +8,373 @@ footer: "Michael Czechowski – SoSe 2026"
|
||||
---
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--color-foreground: #1a1a2e;
|
||||
--color-highlight: #d63384;
|
||||
--color-dimmed: #4a4a6a;
|
||||
}
|
||||
section.invert {
|
||||
--color-foreground: #fff;
|
||||
}
|
||||
section {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
h1 {
|
||||
color: #a02060;
|
||||
}
|
||||
section.invert h1 {
|
||||
color: #fff;
|
||||
}
|
||||
h2 {
|
||||
color: #1f2937;
|
||||
}
|
||||
section.invert h2 {
|
||||
color: #f48fb1;
|
||||
}
|
||||
pre {
|
||||
background: #0f0f23;
|
||||
color: #f48fb1;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #d63384;
|
||||
}
|
||||
pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
code {
|
||||
background: #1a1a2e;
|
||||
color: #f48fb1;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
a {
|
||||
color: var(--color-highlight);
|
||||
}
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; --color-dimmed: #4a4a6a; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
section.invert h2 { color: #f48fb1; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _header: '' -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Web Engineering
|
||||
|
||||
**DHBW Stuttgart** · Informatik / Wirtschaftsinformatik
|
||||
|
||||
**Sommersemester 2026** · Sitzung 2 — HTML & CSS (Frameworks)
|
||||
# Kapitel 2 — CSS Extended
|
||||
## Box-Model · Flexbox · Grid · Responsive · Animations
|
||||
|
||||
---
|
||||
|
||||
# Ressourcen zum Selbstlernen
|
||||
# Recap Kap 1
|
||||
|
||||
- **CODE CRISPIES** — [codecrispi.es](https://codecrispi.es/)
|
||||
- **Online Code-Editor** — [codepen.io](https://codepen.io/pen/)
|
||||
- **MDN** (Mozilla Developer Network) — [developer.mozilla.org/de/](https://developer.mozilla.org/de/)
|
||||
- **Flexbox-Spiel** — [flexboxfroggy.com](https://flexboxfroggy.com/)
|
||||
- **Grid-Spiel** — [cssgridgarden.com](https://cssgridgarden.com/)
|
||||
| Bereich | Was wir kennen |
|
||||
|---------|----------------|
|
||||
| Anatomie | `selector { property: value }` |
|
||||
| Selektoren | element, `.class`, `#id`, `:hover` |
|
||||
| Spezifizität | inline > id > class > element |
|
||||
| Farben | hex, rgb, hsl, oklch |
|
||||
| Einheiten | px, rem, em, %, vw, vh |
|
||||
|
||||
**Heute:** Layout + Animation in Tiefe.
|
||||
|
||||
---
|
||||
|
||||
# Inhalt
|
||||
# Box-Model — die Erinnerung
|
||||
|
||||
Aufbauend auf den Grundlagen aus Sitzung 1.
|
||||
```
|
||||
┌──────────────────┐
|
||||
│ margin │
|
||||
│ ┌────────────┐ │
|
||||
│ │ border │ │
|
||||
│ │ ┌────────┐ │ │
|
||||
│ │ │padding │ │ │
|
||||
│ │ │content │ │ │
|
||||
│ │ └────────┘ │ │
|
||||
│ └────────────┘ │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
1. Attribut-Selektoren
|
||||
2. Combinators (vertieft)
|
||||
3. Pseudo-Klassen — erweitert
|
||||
4. Shorthand Properties
|
||||
5. Flexbox-Shorthands
|
||||
6. Custom Properties (Variables)
|
||||
7. CSS Functions (`calc`, `clamp`, ...)
|
||||
8. Transition & Animation
|
||||
9. CSS Frameworks (Tailwind & Co.)
|
||||
`box-sizing: border-box` global setzen.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
|
||||
# CSS: Extended
|
||||
|
||||
---
|
||||
|
||||
# CSS Selectors – Attribut-Selektoren
|
||||
# Box-Model in Code
|
||||
|
||||
```css
|
||||
/* presence */
|
||||
[disabled] { opacity: 0.5; }
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
/* exact value */
|
||||
[type="email"] { border-color: blue; }
|
||||
.card {
|
||||
width: 320px; /* inkl. padding + border */
|
||||
padding: 1.5rem;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 0.75rem;
|
||||
margin: 1rem auto; /* Außen + horizontal zentriert */
|
||||
}
|
||||
```
|
||||
|
||||
/* contains word */
|
||||
[class~="btn"] { cursor: pointer; }
|
||||
Inspector im DevTools zeigt das Box-Model live an.
|
||||
|
||||
/* starts with */
|
||||
[href^="https"] { color: green; }
|
||||
---
|
||||
|
||||
/* ends with */
|
||||
[src$=".png"] { border: 1px solid #ccc; }
|
||||
<!-- _class: lead -->
|
||||
|
||||
/* contains substring */
|
||||
[class*="icon"] { padding-left: 20px; }
|
||||
# Flexbox
|
||||
## 1D-Layout (Reihe ODER Spalte)
|
||||
|
||||
---
|
||||
|
||||
# Flexbox — Basics
|
||||
|
||||
```css
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 1rem; /* Abstand zwischen Items */
|
||||
flex-direction: row; /* row | column */
|
||||
justify-content: space-between; /* Haupt-Achse */
|
||||
align-items: center; /* Quer-Achse */
|
||||
flex-wrap: wrap; /* umbruchen erlaubt */
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<div class="container">
|
||||
<div>1</div>
|
||||
<div>2</div>
|
||||
<div>3</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# CSS Combinators
|
||||
# Flexbox — Achsen
|
||||
|
||||
**`flex-direction: row` (default):** Haupt-Achse horizontal.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ◄── justify-content ──► │
|
||||
│ ▲ │
|
||||
│ │ │
|
||||
│ align-items │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
`column`: Achsen drehen sich 90°.
|
||||
|
||||
---
|
||||
|
||||
# Flexbox — Item-Properties
|
||||
|
||||
```css
|
||||
/* Child (direkt) */
|
||||
div > p { margin: 0; }
|
||||
.item {
|
||||
flex: 1; /* nimm gleiche Anteile vom verfügbaren Raum */
|
||||
flex: 0 0 200px; /* fest 200px, kein wachsen, kein schrumpfen */
|
||||
flex-grow: 2; /* doppelte Anteile */
|
||||
order: -1; /* visuell zuerst, im DOM hinten */
|
||||
align-self: end; /* override align-items für DIESES Item */
|
||||
}
|
||||
```
|
||||
|
||||
/* Descendant (Nachkomme) */
|
||||
div p { margin: 0; }
|
||||
**Shorthand:** `flex: <grow> <shrink> <basis>`.
|
||||
|
||||
/* Next Sibling */
|
||||
h1 + p { font-size: 1.2em; }
|
||||
---
|
||||
|
||||
/* Subsequent Sibling */
|
||||
h1 ~ p { color: #666; }
|
||||
# Typische Flex-Patterns
|
||||
|
||||
/* Selector List */
|
||||
h1, h2, h3 { font-weight: bold; }
|
||||
```css
|
||||
/* Navigation: Logo links, Items rechts */
|
||||
.nav { display: flex; justify-content: space-between; }
|
||||
|
||||
/* Karte: Bild + Text nebeneinander */
|
||||
.card { display: flex; gap: 1rem; align-items: center; }
|
||||
|
||||
/* Button-Gruppe: alle gleich groß */
|
||||
.btn-group { display: flex; gap: 0.5rem; }
|
||||
.btn-group button { flex: 1; }
|
||||
|
||||
/* Footer: gleichmäßig verteilt */
|
||||
.footer { display: flex; justify-content: space-around; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Pseudo Classes – Erweitert
|
||||
<!-- _class: lead -->
|
||||
|
||||
```css
|
||||
/* Form States */
|
||||
:valid { border-color: green; }
|
||||
:invalid { border-color: red; }
|
||||
:placeholder-shown { color: #999; }
|
||||
|
||||
/* Content */
|
||||
:before { content: "→ "; }
|
||||
:after { content: " [Wichtig]"; }
|
||||
:first-letter { font-size: 2em; }
|
||||
:first-line { font-weight: bold; }
|
||||
|
||||
/* NOT */
|
||||
:not(.disabled) { pointer-events: auto; }
|
||||
:not(:last-child) { border-bottom: 1px solid #ccc; }
|
||||
```
|
||||
# Grid
|
||||
## 2D-Layout (Reihe UND Spalte)
|
||||
|
||||
---
|
||||
|
||||
# Shorthand Properties
|
||||
# Grid — Basics
|
||||
|
||||
```css
|
||||
/* margin: top right bottom left */
|
||||
margin: 10px 20px 10px 20px;
|
||||
|
||||
/* centering */
|
||||
margin: 0 auto;
|
||||
|
||||
/* padding same */
|
||||
padding: 20px;
|
||||
|
||||
/* border: width style color */
|
||||
border: 1px solid #333;
|
||||
|
||||
/* background: color image position/size repeat */
|
||||
background: #fff url(logo.png) 0 0 no-repeat;
|
||||
|
||||
/* font: style variant weight size/line-height family */
|
||||
font: italic normal bold 16px/1.5 Arial, sans-serif;
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 1fr; /* 3 Spalten, Verhältnis 1:2:1 */
|
||||
grid-template-rows: auto 1fr auto; /* Header, Body, Footer */
|
||||
gap: 1rem;
|
||||
}
|
||||
```
|
||||
|
||||
`fr` = Fraction Unit — verbleibender Raum aufgeteilt.
|
||||
|
||||
---
|
||||
|
||||
# Flexbox Shorthands
|
||||
# Grid — Areas (named layout)
|
||||
|
||||
```css
|
||||
/* flex: grow shrink basis */
|
||||
flex: 1 0 auto;
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"nav main"
|
||||
"footer footer";
|
||||
grid-template-columns: 200px 1fr;
|
||||
}
|
||||
|
||||
/* place-items: align-items justify-items */
|
||||
place-items: center stretch;
|
||||
|
||||
/* place-content: align-content justify-content */
|
||||
place-content: space-between center;
|
||||
|
||||
/* gap: row-gap column-gap */
|
||||
gap: 20px 40px;
|
||||
.layout header { grid-area: header; }
|
||||
.layout nav { grid-area: nav; }
|
||||
.layout main { grid-area: main; }
|
||||
.layout footer { grid-area: footer; }
|
||||
```
|
||||
|
||||
Lesbarer als Spalten-/Zeilen-Nummern.
|
||||
|
||||
---
|
||||
|
||||
# CSS Custom Properties (Variables)
|
||||
# Grid — Auto-Layout
|
||||
|
||||
```css
|
||||
/* So viele Spalten wie reinpassen, jede min. 200px */
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
```
|
||||
|
||||
Mobile: 1 Spalte. Tablet: 2-3. Desktop: 4-5. **Ohne Media Queries.**
|
||||
|
||||
---
|
||||
|
||||
# Flexbox vs. Grid
|
||||
|
||||
| | Flexbox | Grid |
|
||||
|--|---------|------|
|
||||
| Dimension | 1D | 2D |
|
||||
| Use-Case | Navigation, Karten-Reihe, Button-Gruppe | Seiten-Layout, komplexes Raster |
|
||||
| Item-Größe | flexibel | meist fest |
|
||||
| Direction | row/column | rows + columns |
|
||||
|
||||
**Beide in einem Layout möglich:** Grid für Seite, Flexbox in Komponenten.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Responsive Design
|
||||
|
||||
---
|
||||
|
||||
# Mobile-First mit Media Queries
|
||||
|
||||
```css
|
||||
/* Default: Mobile */
|
||||
.sidebar { display: none; }
|
||||
.content { padding: 1rem; }
|
||||
|
||||
/* Tablet aufwärts */
|
||||
@media (min-width: 768px) {
|
||||
.sidebar { display: block; width: 240px; }
|
||||
.content { padding: 2rem; }
|
||||
}
|
||||
|
||||
/* Desktop */
|
||||
@media (min-width: 1280px) {
|
||||
.sidebar { width: 320px; }
|
||||
}
|
||||
```
|
||||
|
||||
**Mobile-First**: kleinste Größe als Default, dann größer.
|
||||
|
||||
---
|
||||
|
||||
# Viewport-Meta
|
||||
|
||||
```html
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
```
|
||||
|
||||
**Pflicht** im `<head>`. Ohne dieses Meta zoomt iPhone & Android alles auf 980 px Breite.
|
||||
|
||||
---
|
||||
|
||||
# Fluid Sizing — ohne Media Queries
|
||||
|
||||
```css
|
||||
/* Font: skaliert zwischen 14px und 22px */
|
||||
h1 { font-size: clamp(1.4rem, 2.5vw + 1rem, 2.5rem); }
|
||||
|
||||
/* Padding: skaliert mit Viewport */
|
||||
.card { padding: clamp(1rem, 3vw, 3rem); }
|
||||
|
||||
/* Breite: nie mehr als 70 Zeichen für Lesetext */
|
||||
.prose { max-width: 70ch; margin: auto; }
|
||||
```
|
||||
|
||||
`clamp(min, ideal, max)` ist modernes Allzweck-Werkzeug.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Animationen
|
||||
|
||||
---
|
||||
|
||||
# Transitions — sanfter Wechsel
|
||||
|
||||
```css
|
||||
button {
|
||||
background: hotpink;
|
||||
transition: background 0.2s ease, transform 0.15s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: deeppink;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
```
|
||||
|
||||
**Was animiert performant:** `transform`, `opacity`. **Was nicht:** `width`, `height`, `top/left`.
|
||||
|
||||
---
|
||||
|
||||
# Keyframe-Animationen
|
||||
|
||||
```css
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.1); opacity: 0.7; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.notification {
|
||||
animation: pulse 1.5s ease infinite;
|
||||
}
|
||||
```
|
||||
|
||||
`animation: <name> <duration> <timing> <iteration>`.
|
||||
|
||||
---
|
||||
|
||||
# Reduced Motion — Pflicht
|
||||
|
||||
Nicht alle wollen Animationen. (Vestibuläre Probleme, ADHS, Migräne.)
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Standard-Bestandteil** jeder Seite — ein User-Setting respektieren.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# CSS-Variablen + Modern
|
||||
|
||||
---
|
||||
|
||||
# Custom Properties (CSS Variables)
|
||||
|
||||
```css
|
||||
:root {
|
||||
--primary-color: #d63384;
|
||||
--spacing: 20px;
|
||||
--radius: 8px;
|
||||
--primary: hotpink;
|
||||
--gap: 1rem;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: var(--primary-color);
|
||||
padding: var(--spacing);
|
||||
button {
|
||||
background: var(--primary);
|
||||
padding: var(--gap);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
/* Dark Mode: nur Variablen umsetzen */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --primary: deeppink; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -226,67 +382,72 @@ gap: 20px 40px;
|
||||
# CSS Functions
|
||||
|
||||
```css
|
||||
/* calc */
|
||||
width: calc(100% - 40px);
|
||||
|
||||
/* min/max/clamp */
|
||||
width: min(100%, 600px);
|
||||
font-size: clamp(14px, 2vw, 20px);
|
||||
|
||||
/* var with fallback */
|
||||
color: var(--text-color, #333);
|
||||
|
||||
/* rgb/rgba/hsl/hsla */
|
||||
color: hsl(200, 100%, 50%);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Transition & Animation
|
||||
|
||||
```css
|
||||
/* transition: property duration timing-function */
|
||||
transition: all 0.3s ease-in-out;
|
||||
transition: background 0.2s, transform 0.1s;
|
||||
|
||||
/* animation */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
.box {
|
||||
width: calc(100% - 2rem); /* Mathe */
|
||||
font-size: clamp(1rem, 2vw, 2rem); /* min, ideal, max */
|
||||
background: linear-gradient(45deg, hotpink, deeppink);
|
||||
color: oklch(0.7 0.2 30); /* moderner Farbraum */
|
||||
filter: blur(2px) brightness(1.2); /* Bildeffekte */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# CSS Frameworks
|
||||
# CSS-Frameworks
|
||||
|
||||
| Framework | Stil |
|
||||
|-----------|------|
|
||||
| **Bootstrap** | Komponenten + Grid, opinionated |
|
||||
| **Tailwind CSS** | Utility-First, atomar |
|
||||
| **Bulma** | klassisch, ohne JS |
|
||||
| **shadcn/ui** | kopierbare Komponenten (Tailwind-basiert) |
|
||||
| **Flowbite** | Tailwind-Komponentenbib. |
|
||||
|
||||
---
|
||||
|
||||
# Tailwind CSS – Beispiel
|
||||
| **Tailwind CSS** | Utility-Klassen direkt im HTML |
|
||||
| **Bootstrap** | Komponenten-Bibliothek (klassisch) |
|
||||
| **Pico** | klassen-frei, semantisches HTML reicht |
|
||||
| **Open Props** | CSS-Variablen-Set |
|
||||
|
||||
```html
|
||||
<button class="bg-blue-600 hover:bg-blue-700
|
||||
text-white font-semibold
|
||||
px-4 py-2 rounded-lg
|
||||
transition shadow-sm">
|
||||
Klick mich
|
||||
<!-- Tailwind: alle Styles als Klassen -->
|
||||
<button class="bg-pink-500 hover:bg-pink-600 px-4 py-2 rounded-md">
|
||||
Klick
|
||||
</button>
|
||||
```
|
||||
|
||||
- Keine eigenen CSS-Klassen mehr
|
||||
- JIT-Compiler erzeugt nur genutzte Klassen
|
||||
- Design-System via `tailwind.config.js`
|
||||
---
|
||||
|
||||
# Tailwind — Argumente dafür/dagegen
|
||||
|
||||
**Pro:**
|
||||
- Schnell, weniger Context-Switch (HTML + CSS in einer Datei)
|
||||
- Konsistent (Design-System eingebaut)
|
||||
- Tree-Shaking → kleines Bundle
|
||||
|
||||
**Contra:**
|
||||
- HTML voller Klassen — schwerer zu lesen
|
||||
- Lernkurve eigene
|
||||
- Versions-Lock-in mit Setup
|
||||
|
||||
**Faustregel:** für neue Projekte ausprobieren, vorhandene Projekte nicht zwanghaft migrieren.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||
# Selbstlernen — Layout-Übung
|
||||
|
||||
1. **Flexbox-Spiel:** [flexboxfroggy.com](https://flexboxfroggy.com) — alle 24 Level durch.
|
||||
2. **Grid-Spiel:** [cssgridgarden.com](https://cssgridgarden.com) — 28 Level durch.
|
||||
3. **Eigene Karten-Galerie:** `repeat(auto-fit, minmax(...))` Pattern anwenden.
|
||||
|
||||
**Bonus:** Dark-Mode mit CSS-Variablen + `prefers-color-scheme`.
|
||||
|
||||
---
|
||||
|
||||
# Zusammenfassung
|
||||
|
||||
**Heute gelernt:**
|
||||
- Box-Model + `box-sizing: border-box` global
|
||||
- **Flexbox** für 1D-Layouts (Navigation, Karten-Reihen)
|
||||
- **Grid** für 2D-Layouts (Seiten-Layout, Galerien)
|
||||
- Mobile-First Media Queries + `clamp()` für fluid
|
||||
- Transitions + Keyframes — `prefers-reduced-motion` respektieren
|
||||
- CSS-Variablen für Theming
|
||||
- Tailwind & Co. als moderner Framework-Stil
|
||||
|
||||
**Nächste Stunde:** Node.js — JavaScript außerhalb des Browsers.
|
||||
|
||||
+291
-214
@@ -8,277 +8,354 @@ footer: "Michael Czechowski – SoSe 2026"
|
||||
---
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--color-foreground: #1a1a2e;
|
||||
--color-highlight: #d63384;
|
||||
--color-dimmed: #4a4a6a;
|
||||
}
|
||||
section.invert {
|
||||
--color-foreground: #fff;
|
||||
}
|
||||
section {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
h1 {
|
||||
color: #a02060;
|
||||
}
|
||||
section.invert h1 {
|
||||
color: #fff;
|
||||
}
|
||||
h2 {
|
||||
color: #1f2937;
|
||||
}
|
||||
section.invert h2 {
|
||||
color: #f48fb1;
|
||||
}
|
||||
pre {
|
||||
background: #0f0f23;
|
||||
color: #f48fb1;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #d63384;
|
||||
}
|
||||
pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
code {
|
||||
background: #1a1a2e;
|
||||
color: #f48fb1;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
a {
|
||||
color: var(--color-highlight);
|
||||
}
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; --color-dimmed: #4a4a6a; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _header: '' -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Web Engineering
|
||||
|
||||
**DHBW Stuttgart** · Informatik / Wirtschaftsinformatik
|
||||
|
||||
**Sommersemester 2026** · Sitzung 4 — Node.js: Scripting, Running and Building
|
||||
# Kapitel 3 — Node.js Basics
|
||||
## JavaScript außerhalb des Browsers
|
||||
|
||||
---
|
||||
|
||||
# Ressourcen zum Selbstlernen
|
||||
# Was ist Node.js?
|
||||
|
||||
- **CODE CRISPIES** — [codecrispi.es](https://codecrispi.es/)
|
||||
- **Online Code-Editor** — [codepen.io](https://codepen.io/pen/)
|
||||
- **MDN** (Mozilla Developer Network) — [developer.mozilla.org/de/](https://developer.mozilla.org/de/)
|
||||
- **Flexbox-Spiel** — [flexboxfroggy.com](https://flexboxfroggy.com/)
|
||||
- **Grid-Spiel** — [cssgridgarden.com](https://cssgridgarden.com/)
|
||||
**Ryan Dahl, 2009.** JavaScript-Runtime auf V8 (Chrome's JS-Engine).
|
||||
|
||||
Vorher: JS war Browser-only.
|
||||
Mit Node: JS auch als Server, CLI-Tools, Build-Skripte.
|
||||
|
||||
**Heute:** Node ist Standard-Backend für viele Web-Apps. Netflix, LinkedIn, NASA, Walmart.
|
||||
|
||||
---
|
||||
|
||||
# Inhalt
|
||||
# Warum Node?
|
||||
|
||||
1. Node.js: was und warum
|
||||
2. `node --check`, `node -e` (One-Liner)
|
||||
3. Module: `require` / `import`, `__dirname` / `__filename`
|
||||
4. HTTP-Server (built-in)
|
||||
5. `fs` — Dateien lesen / schreiben
|
||||
6. `package.json` — Aufbau und Lifecycle
|
||||
7. npm Scripts
|
||||
8. Module exportieren
|
||||
| Vorteil | Was bedeutet das |
|
||||
|---------|------------------|
|
||||
| **JS überall** | gleiche Sprache Frontend + Backend |
|
||||
| **NPM** | größtes Package-Ökosystem (~3 Mio Packages) |
|
||||
| **Non-blocking I/O** | viele Verbindungen gleichzeitig |
|
||||
| **Schnell genug** | V8 optimiert JS aggressiv |
|
||||
| **Cross-platform** | Linux, macOS, Windows |
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
# Node Versions-Manager
|
||||
|
||||
# Node.js – Scripting, Running and Building
|
||||
**fnm** (oder `nvm`, `volta`) — pro Projekt Node-Version.
|
||||
|
||||
---
|
||||
```sh
|
||||
# Installieren (macOS/Linux)
|
||||
brew install fnm # oder curl-script
|
||||
|
||||
# Node.js – Was ist das?
|
||||
# Aktuelle LTS
|
||||
fnm install --lts
|
||||
fnm use lts-latest # 24.x
|
||||
|
||||
- JavaScript Runtime (V8 Engine)
|
||||
- Server-side JavaScript
|
||||
- npm Package Manager
|
||||
- Ideal für: APIs, CLI Tools, Automation
|
||||
|
||||
---
|
||||
|
||||
<!-- _header: '' -->
|
||||
<!-- _footer: '' -->
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
# node --check und node -e
|
||||
|
||||
```bash
|
||||
# Syntax check only
|
||||
node --check script.js
|
||||
|
||||
# One-liner
|
||||
node -e "console.log('Hallo Welt')"
|
||||
# Pro Projekt
|
||||
echo "24" > .nvmrc
|
||||
fnm use # liest .nvmrc
|
||||
```
|
||||
|
||||
**Aktuell:** Node 24 LTS.
|
||||
|
||||
---
|
||||
|
||||
# node -e Beispiele
|
||||
# Projekt initialisieren
|
||||
|
||||
```bash
|
||||
# String
|
||||
node -e "console.log('Hello ' + process.argv[2])" Welt
|
||||
```sh
|
||||
mkdir mein-projekt && cd mein-projekt
|
||||
npm init -y # erzeugt package.json
|
||||
|
||||
# File reading (ESM via --input-type)
|
||||
node --input-type=module -e \
|
||||
"import { readFileSync } from 'node:fs'; \
|
||||
console.log(readFileSync('data.json', 'utf8'))"
|
||||
# Abhängigkeiten installieren
|
||||
npm install express # → dependencies
|
||||
npm install -D vitest # → devDependencies (nur dev)
|
||||
|
||||
# JSON parsen
|
||||
node -e "console.log(JSON.parse(process.argv[1]))" '{"a":1}'
|
||||
# Skripte ausführen
|
||||
npm run start
|
||||
npm test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Module laden – ESM (modern)
|
||||
|
||||
`"type": "module"` in `package.json` einschalten.
|
||||
|
||||
```javascript
|
||||
// Eigene Datei (Endung .js erforderlich!)
|
||||
import { add, greet } from "./utils.js";
|
||||
|
||||
// npm Modul (default + named import)
|
||||
import express from "express";
|
||||
import { Router } from "express";
|
||||
|
||||
// Built-in (mit "node:"-Präfix)
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
```
|
||||
|
||||
<small>**Legacy CJS:** `const express = require("express")` — funktioniert noch, aber ESM ist heute Standard.</small>
|
||||
`-y` = alle Defaults akzeptieren.
|
||||
|
||||
---
|
||||
|
||||
# Pfad-Auflösung in ESM
|
||||
|
||||
```javascript
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const configPath = join(__dirname, "..", "config.json");
|
||||
```
|
||||
|
||||
<small>In ESM gibt es **kein** automatisches `__dirname` / `__filename` (anders als CJS). Über `import.meta.url` rekonstruieren.</small>
|
||||
|
||||
---
|
||||
|
||||
# HTTP Server mit Node.js (Built-in)
|
||||
|
||||
**ESM** (modern, `"type": "module"` in `package.json`):
|
||||
```javascript
|
||||
import { createServer } from "node:http";
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
res.writeHead(200, { "Content-Type": "text/html" });
|
||||
res.end("<h1>Hallo Welt!</h1>");
|
||||
});
|
||||
|
||||
server.listen(3000, () => {
|
||||
console.log("Server läuft auf Port 3000");
|
||||
});
|
||||
```
|
||||
|
||||
<small>**CommonJS** (legacy): `const http = require("http");` — funktioniert noch, aber neue Projekte nutzen ESM.</small>
|
||||
|
||||
---
|
||||
|
||||
# fs – Dateien lesen/schreiben
|
||||
|
||||
```javascript
|
||||
import { readFile, readFileSync, writeFile } from "node:fs";
|
||||
import { readFile as readFileAsync } from "node:fs/promises";
|
||||
|
||||
// Promise-API (empfohlen, modern)
|
||||
const data = await readFileAsync("data.json", "utf8");
|
||||
console.log(JSON.parse(data));
|
||||
|
||||
// Callback-API
|
||||
readFile("data.json", "utf8", (err, data) => {
|
||||
if (err) throw err;
|
||||
console.log(JSON.parse(data));
|
||||
});
|
||||
|
||||
// Sync (nur für Setup / Scripts)
|
||||
const content = readFileSync("data.json", "utf8");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# package.json – Das Herzstück
|
||||
# package.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mein-projekt",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "node --watch index.js",
|
||||
"test": "node --test"
|
||||
"dev": "node --watch src/server.js",
|
||||
"start": "node src/server.js",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.0"
|
||||
"express": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.0"
|
||||
"vitest": "^2.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`"type": "module"` aktiviert ESM.
|
||||
|
||||
---
|
||||
|
||||
# npm Scripts
|
||||
# ESM — modern
|
||||
|
||||
```bash
|
||||
# npm run <script>
|
||||
npm run start
|
||||
npm run dev
|
||||
```javascript
|
||||
// utils.js
|
||||
export function greet(name) {
|
||||
return `Hallo ${name}`;
|
||||
}
|
||||
|
||||
# Mit Argumenten
|
||||
npm run build -- --production
|
||||
export const VERSION = "1.0";
|
||||
|
||||
# Lifecycle scripts
|
||||
npm install → postinstall
|
||||
npm start → start (direkt)
|
||||
// main.js
|
||||
import { greet, VERSION } from './utils.js';
|
||||
console.log(greet('Ada'), VERSION);
|
||||
```
|
||||
|
||||
**Pflicht:** `.js`-Endung in import-Pfad.
|
||||
|
||||
---
|
||||
|
||||
# CommonJS — Legacy
|
||||
|
||||
```javascript
|
||||
// utils.cjs
|
||||
function greet(name) {
|
||||
return `Hallo ${name}`;
|
||||
}
|
||||
module.exports = { greet };
|
||||
|
||||
// main.cjs
|
||||
const { greet } = require('./utils.cjs');
|
||||
```
|
||||
|
||||
**Wann CJS?** Alte Codebases, npm-Packages, die nicht migriert sind. **Default heute:** ESM.
|
||||
|
||||
---
|
||||
|
||||
# Built-in Modules
|
||||
|
||||
```javascript
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { createServer } from 'node:http';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
```
|
||||
|
||||
`node:` Prefix macht explizit klar — kein npm-Package.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# HTTP-Server
|
||||
|
||||
---
|
||||
|
||||
# Eingebauter HTTP-Server
|
||||
|
||||
```javascript
|
||||
import { createServer } from 'node:http';
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Hallo Welt');
|
||||
});
|
||||
|
||||
server.listen(3000, () => {
|
||||
console.log('Server läuft auf :3000');
|
||||
});
|
||||
```
|
||||
|
||||
Nackt — ohne Framework. **Funktioniert.**
|
||||
|
||||
---
|
||||
|
||||
# Express — der Klassiker
|
||||
|
||||
```javascript
|
||||
import express from 'express';
|
||||
const app = express();
|
||||
|
||||
app.use(express.json()); // parsed JSON-Body
|
||||
app.use(express.static('public')); // statische Dateien
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send('Hallo Welt');
|
||||
});
|
||||
|
||||
app.post('/api/echo', (req, res) => {
|
||||
res.json({ youSent: req.body });
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Node.js Module – exports (ESM)
|
||||
# REST mit Express
|
||||
|
||||
```javascript
|
||||
// utils.js — named exports
|
||||
export const add = (a, b) => a + b;
|
||||
export const greet = (name) => `Hallo ${name}!`;
|
||||
const users = [{ id: 1, name: 'Ada' }];
|
||||
|
||||
// utils.js — default export
|
||||
export default {
|
||||
add,
|
||||
greet
|
||||
app.get('/users', (req, res) => res.json(users));
|
||||
|
||||
app.get('/users/:id', (req, res) => {
|
||||
const user = users.find(u => u.id == req.params.id);
|
||||
user ? res.json(user) : res.sendStatus(404);
|
||||
});
|
||||
|
||||
app.post('/users', (req, res) => {
|
||||
const user = { id: users.length + 1, ...req.body };
|
||||
users.push(user);
|
||||
res.status(201).json(user);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Alternative Frameworks
|
||||
|
||||
| Framework | Stärke |
|
||||
|-----------|--------|
|
||||
| **Express** | etabliert, größte Community |
|
||||
| **Fastify** | schneller, schemas eingebaut |
|
||||
| **Hono** | leichtgewichtig, Edge-deployable |
|
||||
| **Koa** | von Express-Autoren, modern |
|
||||
|
||||
Für DHBW: **Express** oder **Hono**.
|
||||
|
||||
---
|
||||
|
||||
# Routing-Pattern
|
||||
|
||||
```javascript
|
||||
import { Router } from 'express';
|
||||
const userRouter = Router();
|
||||
|
||||
userRouter.get('/', (req, res) => { /* alle Users */ });
|
||||
userRouter.get('/:id', (req, res) => { /* ein User */ });
|
||||
userRouter.post('/', (req, res) => { /* User anlegen */ });
|
||||
userRouter.put('/:id', (req, res) => { /* User update */ });
|
||||
userRouter.delete('/:id', (req, res) => { /* User löschen */ });
|
||||
|
||||
app.use('/users', userRouter); // mountet unter /users/*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Fetch in Node
|
||||
|
||||
---
|
||||
|
||||
# fetch — global ab Node 18
|
||||
|
||||
```javascript
|
||||
// GET
|
||||
const res = await fetch('https://api.github.com/users/torvalds');
|
||||
const data = await res.json();
|
||||
console.log(data.public_repos);
|
||||
|
||||
// POST
|
||||
const post = await fetch('https://api.example.com/posts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: 'Hallo' })
|
||||
});
|
||||
```
|
||||
|
||||
Genau wie im Browser. **Kein extra Package** (vorher: `node-fetch`, `axios`).
|
||||
|
||||
---
|
||||
|
||||
# Error-Handling
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('API call failed:', error.message);
|
||||
throw error; // re-throw für caller
|
||||
}
|
||||
```
|
||||
|
||||
`res.ok` ist `true` für 2xx-Statuscodes.
|
||||
|
||||
---
|
||||
|
||||
# Environment Variables
|
||||
|
||||
```javascript
|
||||
// .env (gitignored!)
|
||||
DATABASE_URL=postgres://...
|
||||
API_KEY=sk-...
|
||||
PORT=3000
|
||||
|
||||
// src/config.js
|
||||
import 'dotenv/config';
|
||||
|
||||
export const config = {
|
||||
port: process.env.PORT || 3000,
|
||||
dbUrl: process.env.DATABASE_URL,
|
||||
apiKey: process.env.API_KEY,
|
||||
};
|
||||
```
|
||||
|
||||
```javascript
|
||||
// nutzen
|
||||
import { add, greet } from "./utils.js"; // named
|
||||
import utils from "./utils.js"; // default
|
||||
import utils, { add } from "./utils.js"; // beides
|
||||
```
|
||||
**Niemals** API-Keys ins Repo committen.
|
||||
|
||||
<small>**Legacy CJS:** `module.exports = { add, greet }` + `const { add } = require("./utils")`.</small>
|
||||
---
|
||||
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||
# Selbstlernen — Mini-API
|
||||
|
||||
1. **Projekt initialisieren:** `npm init -y`, ESM aktivieren
|
||||
2. **Express installieren**
|
||||
3. **REST-API** für eine Ressource (Books / Movies / TODO)
|
||||
- `GET /items`, `GET /items/:id`, `POST /items`, `DELETE /items/:id`
|
||||
4. **In-Memory-Store** als Array (echte DB kommt Kap 4)
|
||||
5. **Mit Hoppscotch oder curl** testen
|
||||
|
||||
**Bonus:** `npm run dev` mit `--watch`-Flag.
|
||||
|
||||
---
|
||||
|
||||
# Zusammenfassung
|
||||
|
||||
**Heute gelernt:**
|
||||
- Node.js als JS-Runtime außerhalb Browser
|
||||
- **fnm + Node 24 LTS** als Versions-Setup
|
||||
- **npm init**, package.json, scripts, dependencies
|
||||
- **ESM** (import/export) als modernes Modul-System
|
||||
- Built-in HTTP-Server + **Express** für REST
|
||||
- **fetch** global ab Node 18
|
||||
- **.env + dotenv** für Secrets
|
||||
|
||||
**Nächste Stunde:** async/await, Filesystem, Datenbank, Auth.
|
||||
|
||||
+278
-194
@@ -8,289 +8,373 @@ footer: "Michael Czechowski – SoSe 2026"
|
||||
---
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--color-foreground: #1a1a2e;
|
||||
--color-highlight: #d63384;
|
||||
--color-dimmed: #4a4a6a;
|
||||
}
|
||||
section.invert {
|
||||
--color-foreground: #fff;
|
||||
}
|
||||
section {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
h1 {
|
||||
color: #a02060;
|
||||
}
|
||||
section.invert h1 {
|
||||
color: #fff;
|
||||
}
|
||||
h2 {
|
||||
color: #1f2937;
|
||||
}
|
||||
section.invert h2 {
|
||||
color: #f48fb1;
|
||||
}
|
||||
pre {
|
||||
background: #0f0f23;
|
||||
color: #f48fb1;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #d63384;
|
||||
}
|
||||
pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
code {
|
||||
background: #1a1a2e;
|
||||
color: #f48fb1;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
a {
|
||||
color: var(--color-highlight);
|
||||
}
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; --color-dimmed: #4a4a6a; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _header: '' -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Web Engineering
|
||||
|
||||
**DHBW Stuttgart** · Informatik / Wirtschaftsinformatik
|
||||
|
||||
**Sommersemester 2026** · Sitzung 5 — Express API, CRUD und Middlewares
|
||||
# Kapitel 4 — Node.js Advanced
|
||||
## async · Files · Database · Auth · Errors
|
||||
|
||||
---
|
||||
|
||||
# Ressourcen zum Selbstlernen
|
||||
<!-- _class: lead -->
|
||||
|
||||
- **CODE CRISPIES** — [codecrispi.es](https://codecrispi.es/)
|
||||
- **Online Code-Editor** — [codepen.io](https://codepen.io/pen/)
|
||||
- **MDN** (Mozilla Developer Network) — [developer.mozilla.org/de/](https://developer.mozilla.org/de/)
|
||||
- **Flexbox-Spiel** — [flexboxfroggy.com](https://flexboxfroggy.com/)
|
||||
- **Grid-Spiel** — [cssgridgarden.com](https://cssgridgarden.com/)
|
||||
# Asynchron + Promises
|
||||
|
||||
---
|
||||
|
||||
# Inhalt
|
||||
# Synchron vs. Asynchron
|
||||
|
||||
1. Node.js — Fun Facts & alternative Runtimes
|
||||
2. `process` — Signal-Handling, Graceful Shutdown
|
||||
3. Express — Setup, Routing
|
||||
4. Middlewares (eigene + built-in)
|
||||
5. CRUD-Endpunkte (Create, Read, Update, Delete)
|
||||
6. Error Handling
|
||||
7. WebSockets
|
||||
**Synchron** (blockierend):
|
||||
```javascript
|
||||
const data = readFileSync('huge.txt'); // wartet
|
||||
console.log('weiter'); // erst danach
|
||||
```
|
||||
|
||||
**Asynchron** (non-blocking):
|
||||
```javascript
|
||||
const data = await readFile('huge.txt'); // wartet ohne blockieren
|
||||
console.log('weiter'); // erst danach
|
||||
```
|
||||
|
||||
Node's Stärke: **non-blocking I/O** für viele parallele Operationen.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
|
||||
# Node.js – Advanced
|
||||
|
||||
---
|
||||
|
||||
# node.js – Fun Facts
|
||||
|
||||
- Entwickelt von Ryan Dahl in 2009
|
||||
- Erste serverseitige Laufzeitumgebung in JavaScript (C++)
|
||||
- Inspiriert von Nginx (Non-blocking I/O)
|
||||
|
||||
**Alternativen heute:**
|
||||
- **Deno** (2020): Rust, Tokio
|
||||
- **Bun** (2022): Zig
|
||||
|
||||
---
|
||||
|
||||
# process – Prozess-Steuerung
|
||||
# Promise — Anatomie
|
||||
|
||||
```javascript
|
||||
process.on("SIGINT", () => {
|
||||
console.log("Received SIGINT. Closing server...");
|
||||
server.close(() => {
|
||||
console.log("Server closed.");
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
const promise = fetch('https://api.example.com');
|
||||
|
||||
// Signale
|
||||
process.exit(0); // Erfolgreich
|
||||
process.exit(1); // Fehler
|
||||
// then-style (alt)
|
||||
promise
|
||||
.then(res => res.json())
|
||||
.then(data => console.log(data))
|
||||
.catch(err => console.error(err));
|
||||
|
||||
// async/await (modern)
|
||||
try {
|
||||
const res = await fetch('https://api.example.com');
|
||||
const data = await res.json();
|
||||
console.log(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# node.js Module System
|
||||
# Sequenziell vs. Parallel
|
||||
|
||||
```javascript
|
||||
// ES Modules (modern, "type": "module")
|
||||
import express from "express";
|
||||
import { port } from "./src/config/constants.js";
|
||||
import { readFileSync } from "node:fs";
|
||||
// SEQUENZIELL — langsam (warten + warten + warten)
|
||||
const a = await fetch('/api/a');
|
||||
const b = await fetch('/api/b');
|
||||
const c = await fetch('/api/c');
|
||||
// Gesamtzeit: a + b + c
|
||||
|
||||
// PARALLEL — schnell (alle gleichzeitig starten)
|
||||
const [a, b, c] = await Promise.all([
|
||||
fetch('/api/a'),
|
||||
fetch('/api/b'),
|
||||
fetch('/api/c'),
|
||||
]);
|
||||
// Gesamtzeit: max(a, b, c)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Promise-Methoden
|
||||
|
||||
| Methode | Was |
|
||||
|---------|-----|
|
||||
| `Promise.all([…])` | alle erfolgreich ODER ein Fehler |
|
||||
| `Promise.allSettled([…])` | alle, Resultat = { status, value/reason } |
|
||||
| `Promise.race([…])` | erstes Resultat gewinnt (auch Fehler) |
|
||||
| `Promise.any([…])` | erstes erfolgreiches, ignore Fehler |
|
||||
|
||||
```javascript
|
||||
// CommonJS (legacy)
|
||||
const express = require("express");
|
||||
const fs = require("fs");
|
||||
const fastest = await Promise.race([
|
||||
fetch('/api'),
|
||||
new Promise((_, rej) => setTimeout(() => rej('timeout'), 3000))
|
||||
]);
|
||||
```
|
||||
|
||||
→ Neue Projekte: ESM. Built-ins immer mit `node:`-Präfix.
|
||||
|
||||
---
|
||||
|
||||
# HTTP Request mit node-fetch
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Filesystem
|
||||
|
||||
---
|
||||
|
||||
# fs/promises — Files lesen + schreiben
|
||||
|
||||
```javascript
|
||||
// Node.js 18+ hat fetch eingebaut
|
||||
const res = await fetch('https://api.example.com/data');
|
||||
const data = await res.json();
|
||||
import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
|
||||
|
||||
// Ältere Versionen: node-fetch
|
||||
import fetch from 'node-fetch';
|
||||
// Lesen
|
||||
const text = await readFile('input.txt', 'utf-8');
|
||||
const buf = await readFile('image.png'); // Buffer
|
||||
|
||||
// Schreiben
|
||||
await writeFile('output.txt', 'Hallo Welt');
|
||||
await writeFile('data.json', JSON.stringify({ x: 1 }, null, 2));
|
||||
|
||||
// Verzeichnis lesen
|
||||
const files = await readdir('./uploads');
|
||||
|
||||
// Verzeichnis anlegen
|
||||
await mkdir('./tmp', { recursive: true });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Web Server, Builder, Bundler
|
||||
|
||||
```bash
|
||||
# Boilerplates erstellen
|
||||
npm create vite@latest my-app -- --template react
|
||||
npm create svelte@latest
|
||||
npm create astro@latest
|
||||
|
||||
# nuxt
|
||||
npm install nuxt
|
||||
npm run dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _header: '' -->
|
||||
<!-- _footer: '' -->
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
# Express – Grundlagen
|
||||
# Streams für große Dateien
|
||||
|
||||
```javascript
|
||||
import express from 'express';
|
||||
const app = express();
|
||||
import { createReadStream, createWriteStream } from 'node:fs';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server läuft auf Port ${port}`);
|
||||
});
|
||||
// 10 GB Logfile ohne RAM-Explosion verarbeiten
|
||||
await pipeline(
|
||||
createReadStream('huge.log'),
|
||||
// ... transform-stream ...
|
||||
createWriteStream('output.log')
|
||||
);
|
||||
```
|
||||
|
||||
Streams = Daten **stückweise** verarbeiten, nicht alles in RAM.
|
||||
|
||||
---
|
||||
|
||||
# Express – Middleware
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Datenbank
|
||||
|
||||
---
|
||||
|
||||
# Welche DB für DHBW-Projekte?
|
||||
|
||||
| DB | Wann |
|
||||
|----|------|
|
||||
| **SQLite** | klein, embedded, eine Datei. Lokal/dev/demo. |
|
||||
| **PostgreSQL** | Standard für „echte" Apps. Open Source, mächtig. |
|
||||
| **MongoDB** | NoSQL, dokumentenorientiert. Selten erste Wahl. |
|
||||
| **Redis** | Cache, Queue, schnell. |
|
||||
|
||||
Für Prüfungsleistung: **SQLite** (kein Setup) oder **Postgres** (production-realistisch).
|
||||
|
||||
---
|
||||
|
||||
# SQLite mit `better-sqlite3`
|
||||
|
||||
```javascript
|
||||
app.use(express.json());
|
||||
import Database from 'better-sqlite3';
|
||||
const db = new Database('app.db');
|
||||
|
||||
app.use('/api', (req, res, next) => {
|
||||
console.log('Time:', Date.now());
|
||||
next();
|
||||
});
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`).run();
|
||||
|
||||
app.use((req, res, next) => {
|
||||
console.log('Middleware 2');
|
||||
next();
|
||||
});
|
||||
// Insert
|
||||
const insert = db.prepare('INSERT INTO users (name) VALUES (?)');
|
||||
insert.run('Ada');
|
||||
|
||||
// Query
|
||||
const users = db.prepare('SELECT * FROM users').all();
|
||||
const ada = db.prepare('SELECT * FROM users WHERE id = ?').get(1);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Express – CRUD Routes
|
||||
# ORM / Query-Builder
|
||||
|
||||
Für komplexere Apps: ORM statt Raw-SQL.
|
||||
|
||||
| Tool | Typ |
|
||||
|------|-----|
|
||||
| **Drizzle** | TypeScript-first, modern, lightweight |
|
||||
| **Prisma** | umfangreich, eigenes Schema |
|
||||
| **Knex** | Query-Builder, kein ORM |
|
||||
| **TypeORM** | klassisches ORM |
|
||||
|
||||
```typescript
|
||||
// Drizzle Beispiel
|
||||
const result = await db.select().from(users).where(eq(users.id, 1));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# CRUD-Pattern mit Express + SQLite
|
||||
|
||||
```javascript
|
||||
app.get('/api/:key', async (req, res) => {
|
||||
const context = cache.get(req.params.key);
|
||||
res.json({ context });
|
||||
app.get('/users', (req, res) => {
|
||||
const users = db.prepare('SELECT * FROM users').all();
|
||||
res.json(users);
|
||||
});
|
||||
|
||||
app.post('/api/:key', async (req, res) => {
|
||||
const payload = req.body;
|
||||
res.json({ created: cache.set(req.params.key, payload) });
|
||||
app.get('/users/:id', (req, res) => {
|
||||
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
|
||||
user ? res.json(user) : res.sendStatus(404);
|
||||
});
|
||||
|
||||
app.put('/api/:key', async (req, res) => {
|
||||
const payload = req.body;
|
||||
res.json({ updated: cache.update(req.params.key, payload) });
|
||||
});
|
||||
|
||||
app.delete('/api/:key', async (req, res) => {
|
||||
cache.remove(req.params.key);
|
||||
res.json({ deleted: req.params.key });
|
||||
app.post('/users', (req, res) => {
|
||||
const { name } = req.body;
|
||||
const result = db.prepare('INSERT INTO users (name) VALUES (?)').run(name);
|
||||
res.status(201).json({ id: result.lastInsertRowid, name });
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Express – server.listen
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Authentifizierung
|
||||
|
||||
---
|
||||
|
||||
# Auth-Strategien
|
||||
|
||||
| Strategie | Wann |
|
||||
|-----------|------|
|
||||
| **Session-Cookie** | klassische Web-App, Server-Side Render |
|
||||
| **JWT** (JSON Web Token) | API + SPA, stateless |
|
||||
| **OAuth** | „Mit Google einloggen", Drittpartei-Login |
|
||||
| **Magic Link** | Email statt Passwort, modern |
|
||||
| **Passkeys** | passwordless, FIDO2, Zukunft |
|
||||
|
||||
Für DHBW-Projekt: **Session-Cookie** (express-session) ODER **JWT**.
|
||||
|
||||
---
|
||||
|
||||
# Passwörter — *niemals plaintext*
|
||||
|
||||
```javascript
|
||||
const server = app.listen(port, () => {
|
||||
console.log(`Server läuft auf Port ${port}`);
|
||||
});
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
server.on('error', (err) => {
|
||||
console.error('Server error:', err);
|
||||
});
|
||||
// Registrieren
|
||||
const hashed = await bcrypt.hash('mein-passwort', 12);
|
||||
db.prepare('INSERT INTO users (email, password) VALUES (?, ?)').run(email, hashed);
|
||||
|
||||
server.close(() => {
|
||||
console.log('Server geschlossen');
|
||||
});
|
||||
// Login
|
||||
const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email);
|
||||
const valid = await bcrypt.compare(password, user.password);
|
||||
```
|
||||
|
||||
**bcrypt** oder **argon2** für Passwort-Hashing. Niemals MD5/SHA1.
|
||||
|
||||
---
|
||||
|
||||
# WebSockets
|
||||
# JWT — kurzer Überblick
|
||||
|
||||
```javascript
|
||||
import { WebSocketServer } from 'ws';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const wss = new WebSocketServer({ port: 8080 });
|
||||
// Signieren (beim Login)
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id, role: user.role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '1d' }
|
||||
);
|
||||
res.cookie('token', token, { httpOnly: true, secure: true });
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
console.log('Client verbunden');
|
||||
// Verifizieren (Middleware)
|
||||
const token = req.cookies.token;
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = payload;
|
||||
```
|
||||
|
||||
ws.on('message', (data) => {
|
||||
console.log('Empfangen:', data.toString());
|
||||
ws.send('Pong');
|
||||
});
|
||||
});
|
||||
`httpOnly + secure + sameSite` — XSS/CSRF-Schutz.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Error-Handling
|
||||
|
||||
---
|
||||
|
||||
# try/catch + Error-Klassen
|
||||
|
||||
```javascript
|
||||
class NotFoundError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'NotFoundError';
|
||||
this.status = 404;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await getUser(id);
|
||||
if (!user) throw new NotFoundError(`User ${id} not found`);
|
||||
res.json(user);
|
||||
} catch (err) {
|
||||
if (err instanceof NotFoundError) return res.status(404).json({ error: err.message });
|
||||
console.error(err);
|
||||
res.sendStatus(500);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _header: '' -->
|
||||
<!-- _footer: '' -->
|
||||
# Globaler Express-Error-Handler
|
||||
|
||||

|
||||
```javascript
|
||||
// Am Ende aller Routes!
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err);
|
||||
|
||||
if (err.status) {
|
||||
return res.status(err.status).json({ error: err.message });
|
||||
}
|
||||
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
});
|
||||
```
|
||||
|
||||
4 Argumente: `(err, req, res, next)` → Express erkennt Error-Handler.
|
||||
|
||||
---
|
||||
|
||||
<!-- _header: '' -->
|
||||
<!-- _footer: '' -->
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||

|
||||
# Selbstlernen — REST-API mit DB
|
||||
|
||||
Erweitert eure Kap-3-API:
|
||||
|
||||
1. **SQLite-DB** mit `better-sqlite3`
|
||||
2. **CRUD** mit DB statt In-Memory-Array
|
||||
3. **Validierung** (z.B. mit `zod`) für POST-Body
|
||||
4. **Auth:** einfache Login-Route mit bcrypt + JWT
|
||||
5. **Global Error-Handler** für saubere Responses
|
||||
|
||||
**Bonus:** Async-Race-Condition simulieren + Promise.all nutzen.
|
||||
|
||||
---
|
||||
|
||||
<!-- _header: '' -->
|
||||
<!-- _footer: '' -->
|
||||
|
||||

|
||||
# Zusammenfassung
|
||||
|
||||
**Heute gelernt:**
|
||||
- **async/await + Promise** sauber anwenden, sequenziell vs. parallel
|
||||
- **fs/promises + Streams** für Dateien
|
||||
- **SQLite + Prepared Statements** als pragmatische DB
|
||||
- **bcrypt** für Passwörter, **JWT** als Token-Pattern
|
||||
- **Error-Klassen + Globaler Handler** in Express
|
||||
|
||||
**Nächste Stunde:** Testing — Vitest, TDD, Integration, E2E.
|
||||
|
||||
+294
-285
@@ -9,366 +9,375 @@ title: Testing
|
||||
---
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--color-foreground: #1a1a2e;
|
||||
--color-highlight: #d63384;
|
||||
--color-dimmed: #4a4a6a;
|
||||
}
|
||||
section.invert {
|
||||
--color-foreground: #fff;
|
||||
}
|
||||
section {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
h1 {
|
||||
color: #a02060;
|
||||
}
|
||||
section.invert h1 {
|
||||
color: #fff;
|
||||
}
|
||||
h2 {
|
||||
color: #1f2937;
|
||||
}
|
||||
section.invert h2 {
|
||||
color: #f48fb1;
|
||||
}
|
||||
pre {
|
||||
background: #0f0f23;
|
||||
color: #f48fb1;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #d63384;
|
||||
}
|
||||
pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
code {
|
||||
background: #1a1a2e;
|
||||
color: #f48fb1;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
a {
|
||||
color: var(--color-highlight);
|
||||
}
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _header: '' -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Web Engineering
|
||||
|
||||
**DHBW Stuttgart** · Informatik / Wirtschaftsinformatik
|
||||
|
||||
**Sommersemester 2026** · Sitzung 7 — Testing
|
||||
# Kapitel 5 — Testing
|
||||
## Unit · Integration · E2E · TDD
|
||||
|
||||
---
|
||||
|
||||
# Ressourcen zum Selbstlernen
|
||||
# Warum testen?
|
||||
|
||||
- **CODE CRISPIES** — [codecrispi.es](https://codecrispi.es/)
|
||||
- **Online Code-Editor** — [codepen.io](https://codepen.io/pen/)
|
||||
- **MDN** (Mozilla Developer Network) — [developer.mozilla.org/de/](https://developer.mozilla.org/de/)
|
||||
- **Flexbox-Spiel** — [flexboxfroggy.com](https://flexboxfroggy.com/)
|
||||
- **Grid-Spiel** — [cssgridgarden.com](https://cssgridgarden.com/)
|
||||
| Vorteil | Was bedeutet das |
|
||||
|---------|------------------|
|
||||
| **Confidence** | Refactor ohne Angst |
|
||||
| **Regression-Schutz** | alter Bug taucht nicht wieder auf |
|
||||
| **Living Documentation** | Tests zeigen, was Code soll |
|
||||
| **CI/CD-Voraussetzung** | automatisch vor jedem Deploy |
|
||||
|
||||
**„Untested Code is broken by design."**
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
|
||||
# Testing
|
||||
|
||||
## Unit · Integration · End-to-End
|
||||
|
||||
---
|
||||
|
||||
# Inhalt
|
||||
|
||||
1. Test Automation Pyramid
|
||||
2. Test Automation Trophy
|
||||
3. Static Code Analysis
|
||||
4. Unit Tests
|
||||
5. Integration Tests
|
||||
6. End-to-End Tests (API + Client)
|
||||
7. Live Coding
|
||||
|
||||
---
|
||||
|
||||
# Test Automation Pyramid
|
||||
# Test-Pyramide
|
||||
|
||||
```
|
||||
▲ manual / exploratory
|
||||
▲ ▲ E2E (UI + API)
|
||||
▲▲▲▲ Integration (class + real deps)
|
||||
▲▲▲▲▲▲ Unit (class + mocked deps)
|
||||
▲▲▲▲▲▲▲▲ Static analysis (compiler, linter)
|
||||
/\
|
||||
/E2E\ wenige (langsam, fragil)
|
||||
/-----\
|
||||
/ Integ \ einige (mittel)
|
||||
/---------\
|
||||
/ Unit \ viele (schnell, isoliert)
|
||||
/-------------\
|
||||
```
|
||||
|
||||
- Oben: wenige, langsam, teuer, brüchig
|
||||
- Unten: viele, schnell, billig, stabil
|
||||
**Faustregel:** 70 % Unit, 20 % Integration, 10 % E2E.
|
||||
|
||||
---
|
||||
|
||||
# Test Automation V-Modell
|
||||
# Vitest — modernes JS-Testing
|
||||
|
||||
```sh
|
||||
npm install -D vitest
|
||||
```
|
||||
Requirements ────────────► Acceptance Test
|
||||
▼ ▲
|
||||
Architecture ────────► Integration Test
|
||||
▼ ▲
|
||||
Modular Design ──────► Unit Test
|
||||
▼ ▲
|
||||
Implementation
|
||||
```
|
||||
|
||||
Jede Design-Phase hat Testpendant. Testfälle früh ableiten.
|
||||
|
||||
---
|
||||
|
||||
# Test Automation Trophy (Kent C. Dodds)
|
||||
|
||||
```
|
||||
┌─────────┐
|
||||
│ E2E │ ← klein
|
||||
├─────────┤
|
||||
│ Integr. │ ← GROSS (Hauptfokus)
|
||||
├─────────┤
|
||||
│ Unit │ ← mittel
|
||||
├─────────┤
|
||||
│ Static │ ← Basis (TS, ESLint)
|
||||
└─────────┘
|
||||
```
|
||||
|
||||
Andere Gewichtung: **Integration im Zentrum**, Static als breite Basis.
|
||||
|
||||
---
|
||||
|
||||
# Testing Frameworks Übersicht
|
||||
|
||||
**Static Code Analysis**
|
||||
- ESLint, Prettier, SonarQube, TypeScript
|
||||
|
||||
**Unit + Integration**
|
||||
- jest, mocha, **vitest**
|
||||
|
||||
**End-to-End**
|
||||
- **Cypress**, **Playwright**, supertest (API only)
|
||||
|
||||
**Visual / Interaction**
|
||||
- Storybook, Chromatic
|
||||
|
||||
---
|
||||
|
||||
# Static Code Analysis
|
||||
|
||||
- Läuft in Millisekunden
|
||||
- Findet Syntaxfehler, ungenutzte Variablen, undefined refs
|
||||
- Kein Runtime-Overhead
|
||||
- **Erste Verteidigungslinie**
|
||||
|
||||
```bash
|
||||
npm i -D eslint prettier
|
||||
npx eslint --init
|
||||
npx prettier --write .
|
||||
```
|
||||
|
||||
`SonarQube`: zentraler Server, Code Smells, Security, Coverage.
|
||||
|
||||
---
|
||||
|
||||
# Unit Tests – Eigenschaften
|
||||
|
||||
- **Isolation** – jede Dependency gemockt
|
||||
- **Deterministisch** – gleicher Input → gleicher Output
|
||||
- **Schnell** – kein I/O, kein Netzwerk, kein FS
|
||||
- **Fokussiert** – eine Funktion pro Test
|
||||
|
||||
---
|
||||
|
||||
# Unit Test – Beispiel (vitest)
|
||||
|
||||
```javascript
|
||||
// cache.unit.test.js
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { Cache } from "./cache.js";
|
||||
// utils.test.js
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { greet } from './utils.js';
|
||||
|
||||
describe("Cache", () => {
|
||||
it("creates and retrieves data with custom key", () => {
|
||||
const cache = new Cache();
|
||||
const data = { user: "wolfgang", city: "stuttgart" };
|
||||
describe('greet', () => {
|
||||
it('returns greeting with name', () => {
|
||||
expect(greet('Ada')).toBe('Hallo Ada');
|
||||
});
|
||||
|
||||
it('handles empty string', () => {
|
||||
expect(greet('')).toBe('Hallo ');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
const key = cache.create(data, "custom-key");
|
||||
```sh
|
||||
npx vitest
|
||||
npx vitest --coverage
|
||||
```
|
||||
|
||||
expect(key).toBe("custom-key");
|
||||
expect(cache.get("custom-key")).toEqual(data);
|
||||
---
|
||||
|
||||
# Test-Struktur (AAA)
|
||||
|
||||
```javascript
|
||||
it('adds two numbers', () => {
|
||||
// ARRANGE — Setup
|
||||
const a = 2;
|
||||
const b = 3;
|
||||
|
||||
// ACT — die Operation
|
||||
const result = add(a, b);
|
||||
|
||||
// ASSERT — Erwartung
|
||||
expect(result).toBe(5);
|
||||
});
|
||||
```
|
||||
|
||||
**Eine Erwartung pro Test** (Faustregel).
|
||||
|
||||
---
|
||||
|
||||
# Vitest-Matchers
|
||||
|
||||
| Matcher | Was |
|
||||
|---------|-----|
|
||||
| `.toBe(x)` | strict equal (`===`) |
|
||||
| `.toEqual(x)` | deep equal (Objects, Arrays) |
|
||||
| `.toBeNull()` / `.toBeUndefined()` | |
|
||||
| `.toBeTruthy()` / `.toBeFalsy()` | |
|
||||
| `.toContain(item)` | Array enthält |
|
||||
| `.toHaveLength(n)` | |
|
||||
| `.toThrow()` | wirft Fehler |
|
||||
| `.toMatchSnapshot()` | Snapshot-Test |
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# TDD — Test-Driven Development
|
||||
|
||||
---
|
||||
|
||||
# TDD-Cycle: Red → Green → Refactor
|
||||
|
||||
1. **Red:** Test schreiben (schlägt fehl)
|
||||
2. **Green:** minimaler Code, damit Test besteht
|
||||
3. **Refactor:** Code säubern, Test bleibt grün
|
||||
|
||||
**Kent Beck, 2002.** Steile Lernkurve, aber stark fokussiert.
|
||||
|
||||
```javascript
|
||||
// 1. Red — Test ohne Implementierung
|
||||
it('isEven returns true for even', () => {
|
||||
expect(isEven(4)).toBe(true);
|
||||
});
|
||||
// → ReferenceError: isEven is not defined
|
||||
|
||||
// 2. Green — minimale Lösung
|
||||
function isEven(n) { return n % 2 === 0; }
|
||||
|
||||
// 3. Refactor — Edge Cases, Lesbarkeit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Integration-Tests
|
||||
|
||||
---
|
||||
|
||||
# API testen mit Supertest
|
||||
|
||||
```javascript
|
||||
import request from 'supertest';
|
||||
import { app } from './app.js';
|
||||
|
||||
describe('GET /users', () => {
|
||||
it('returns users array', async () => {
|
||||
const res = await request(app).get('/users');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toBeInstanceOf(Array);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /users', () => {
|
||||
it('creates a user', async () => {
|
||||
const res = await request(app)
|
||||
.post('/users')
|
||||
.send({ name: 'Ada' });
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.name).toBe('Ada');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Integration Tests – Eigenschaften
|
||||
|
||||
- **Reale Komponenten** – echte Cache + Express-Instanzen
|
||||
- **Supertest-Magie** – simuliert HTTP ohne Server-Startup
|
||||
- **In-Process** – alles im selben Node-Prozess
|
||||
- **Kein Netzwerk** – keine TCP-Connections
|
||||
|
||||
→ teste Zusammenspiel mehrerer Module ohne Deployment.
|
||||
|
||||
---
|
||||
|
||||
# Integration Test – Beispiel (supertest)
|
||||
# Test-Datenbank
|
||||
|
||||
```javascript
|
||||
// app.integration.test.js
|
||||
import request from "supertest";
|
||||
import { app, cache } from "./app.js";
|
||||
import { beforeAll, afterEach, afterAll } from 'vitest';
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
test("full CRUD lifecycle with real cache", async () => {
|
||||
const res = await request(app)
|
||||
.post("/api/entries")
|
||||
.send({ name: "integration-test", status: "active" });
|
||||
let db;
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
const { key } = res.body;
|
||||
|
||||
expect(cache.keys()).toContain(key);
|
||||
beforeAll(() => {
|
||||
db = new Database(':memory:'); // in-RAM, super schnell
|
||||
db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.exec('DELETE FROM users'); // sauberer Start
|
||||
});
|
||||
|
||||
afterAll(() => db.close());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# End-to-End Tests (API)
|
||||
|
||||
- **Echter Server** – HTTP auf TCP-Port
|
||||
- **Echtes Netzwerk** – `fetch` über localhost
|
||||
- **Echte Umgebung** – CORS, Middleware-Stack
|
||||
- **User-Perspektive** – exakt was Clients sehen
|
||||
`:memory:`-DB = jeder Test isoliert, blitzschnell.
|
||||
|
||||
---
|
||||
|
||||
# E2E API Test – Beispiel
|
||||
# Mocking
|
||||
|
||||
```javascript
|
||||
// index.e2e.test.js
|
||||
import { createServer } from "node:http";
|
||||
import { app } from "./app.js";
|
||||
import { vi, it, expect } from 'vitest';
|
||||
|
||||
let server;
|
||||
const TEST_PORT = 8081;
|
||||
const baseUrl = `http://localhost:${TEST_PORT}`;
|
||||
|
||||
beforeAll(async () => {
|
||||
server = createServer(app);
|
||||
await new Promise((r) => server.listen(TEST_PORT, r));
|
||||
});
|
||||
|
||||
afterAll(() => server.close());
|
||||
|
||||
test("complete user journey", async () => {
|
||||
const res = await fetch(`${baseUrl}/api`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: "e2e", timestamp: Date.now() }),
|
||||
it('calls fetch with right URL', async () => {
|
||||
const mockFetch = vi.spyOn(global, 'fetch').mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ name: 'Ada' })
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
|
||||
await getUserData(1);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api/users/1');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# End-to-End Tests (Client)
|
||||
|
||||
- **Echter Browser** – Chromium / Firefox / WebKit
|
||||
- **Echtes DOM** – Klicks, Tippen, Scrollen
|
||||
- **Echtes Rendering** – CSS, async Operationen
|
||||
- **Cross-Origin** – Client- und Server-Kommunikation
|
||||
|
||||
Tools: **Cypress**, **Playwright**
|
||||
**Mock sparsam** — übermockt-Tests testen Mocks, nicht Code.
|
||||
|
||||
---
|
||||
|
||||
# Cypress – Minimalbeispiel
|
||||
<!-- _class: lead -->
|
||||
|
||||
# E2E — End-to-End
|
||||
|
||||
---
|
||||
|
||||
# Playwright — moderne E2E
|
||||
|
||||
```sh
|
||||
npm init playwright@latest
|
||||
```
|
||||
|
||||
```javascript
|
||||
// cypress/e2e/counter.cy.js
|
||||
describe("Counter App", () => {
|
||||
it("increments on click", () => {
|
||||
cy.visit("http://localhost:3000");
|
||||
cy.contains("button", "Clicked 0 times").click();
|
||||
cy.contains("button", "Clicked 1 times");
|
||||
});
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('login flow', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
await page.getByLabel('Email').fill('ada@example.com');
|
||||
await page.getByLabel('Password').fill('secret123');
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
await expect(page.getByText('Hallo Ada')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
```bash
|
||||
npx cypress open # GUI
|
||||
npx cypress run # headless / CI
|
||||
```
|
||||
Browser tatsächlich öffnen + interagieren. **Realer User-Flow.**
|
||||
|
||||
---
|
||||
|
||||
# Playwright – Minimalbeispiel
|
||||
# Playwright-Locators
|
||||
|
||||
```javascript
|
||||
// tests/counter.spec.js
|
||||
import { test, expect } from "@playwright/test";
|
||||
// Bevorzugt: rollenbasiert (A11y-konform!)
|
||||
page.getByRole('button', { name: 'Submit' });
|
||||
page.getByLabel('Username');
|
||||
page.getByText('Welcome');
|
||||
|
||||
test("counter increments", async ({ page }) => {
|
||||
await page.goto("http://localhost:3000");
|
||||
await page.getByRole("button").click();
|
||||
await expect(page.getByRole("button")).toContainText("Clicked 1 times");
|
||||
});
|
||||
// Fallback: CSS-Selektor
|
||||
page.locator('.submit-btn');
|
||||
page.locator('#username');
|
||||
```
|
||||
|
||||
```bash
|
||||
npx playwright test
|
||||
npx playwright test --ui
|
||||
```
|
||||
**Rollenbasierte Locators** finden Bugs, die A11y betreffen.
|
||||
|
||||
---
|
||||
|
||||
# Coverage Reports
|
||||
# E2E vs. Unit — Trade-offs
|
||||
|
||||
```bash
|
||||
npx vitest run --coverage
|
||||
```
|
||||
| | Unit | E2E |
|
||||
|--|------|-----|
|
||||
| Geschwindigkeit | ms | s |
|
||||
| Isolation | hoch | gering |
|
||||
| Realismus | gering | hoch |
|
||||
| Wartung | wenig | viel |
|
||||
| Wann | jede Funktion | kritische Flows |
|
||||
|
||||
```
|
||||
File | % Stmts | % Branch | % Funcs | % Lines
|
||||
--------------|---------|----------|---------|--------
|
||||
cache.js | 95.2 | 80.0 | 100.0 | 95.2
|
||||
app.js | 88.7 | 75.0 | 90.0 | 88.7
|
||||
```
|
||||
|
||||
- `lcov.info` → CI / SonarQube
|
||||
- HTML Report → `coverage/index.html`
|
||||
E2E nicht für jede Detail-Logik — für **happy path + Login + Bezahlung**.
|
||||
|
||||
---
|
||||
|
||||
# Best Practices
|
||||
<!-- _class: lead -->
|
||||
|
||||
- **AAA**: Arrange · Act · Assert
|
||||
- Tests sind **Dokumentation** – beschreibend benennen
|
||||
- Ein Verhalten pro Test
|
||||
- Keine Logik im Test (keine `if`/`for`)
|
||||
- **Test Doubles**: stub, mock, spy, fake
|
||||
- CI: Tests bei jedem Push
|
||||
# Coverage + CI
|
||||
|
||||
---
|
||||
|
||||
# Live Coding
|
||||
# Coverage-Report
|
||||
|
||||
Repo: `node-cache-api`
|
||||
|
||||
```bash
|
||||
git clone https://github.com/nextlevelshit/node-cache-api
|
||||
cd node-cache-api
|
||||
npm install
|
||||
npm test # unit + integration
|
||||
npm run test:e2e # e2e
|
||||
```sh
|
||||
npx vitest --coverage
|
||||
```
|
||||
|
||||
```
|
||||
File | % Stmts | % Branch | % Funcs | Uncovered
|
||||
-------------+---------+----------+---------+-----------
|
||||
utils.js | 100 | 100 | 100 |
|
||||
api.js | 75.3 | 60 | 80 | 42-48
|
||||
auth.js | 91.2 | 85 | 100 | 15
|
||||
```
|
||||
|
||||
**100 % ≠ keine Bugs.** Aber 0 % = sicher Bugs.
|
||||
|
||||
---
|
||||
|
||||
# Coverage-Ziel realistisch
|
||||
|
||||
| Code-Typ | Realistisch |
|
||||
|----------|-------------|
|
||||
| Pure Funktionen | 100 % |
|
||||
| Business-Logik | 80-90 % |
|
||||
| API-Routes | 70-80 % |
|
||||
| UI-Komponenten | 60-70 % |
|
||||
| Drittpartei-Wrapper | 0 % |
|
||||
| Konfig-Files | 0 % |
|
||||
|
||||
**Faustregel:** über 80 % Gesamt = gut. Unter 50 % = gefährlich.
|
||||
|
||||
---
|
||||
|
||||
# CI: Tests auf jedem Push
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
name: Test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
- run: npm run test:e2e
|
||||
```
|
||||
|
||||
Tests sind grün → PR mergebar. Sonst: rot, mergen verboten.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||
# Selbstlernen — Tests schreiben
|
||||
|
||||
Erweitert eure API:
|
||||
|
||||
1. **3+ Unit-Tests** für eure Helper-Funktionen (`utils.test.js`)
|
||||
2. **3+ Integration-Tests** für eure REST-Endpunkte (mit Supertest)
|
||||
3. **In-Memory-DB** in `beforeAll`/`afterEach`
|
||||
4. **1 E2E-Test** mit Playwright (z.B. Login-Flow)
|
||||
5. **Coverage-Report** generieren
|
||||
|
||||
**Bonus:** GitHub-Actions-Workflow für CI.
|
||||
|
||||
---
|
||||
|
||||
# Zusammenfassung
|
||||
|
||||
**Heute gelernt:**
|
||||
- **Test-Pyramide:** viele Unit, einige Integration, wenige E2E
|
||||
- **Vitest** als modernes Test-Runner-Setup
|
||||
- **AAA-Pattern** + Matchers + Lifecycle-Hooks
|
||||
- **TDD-Cycle** Red → Green → Refactor
|
||||
- **Supertest** für API-Integration-Tests
|
||||
- **Playwright** für E2E mit rollenbasierten Locators
|
||||
- **Coverage** als Werkzeug, nicht als Ziel
|
||||
- **CI** mit GitHub-Actions
|
||||
|
||||
**Nächste Stunde:** TypeScript — Typen über JavaScript.
|
||||
|
||||
+271
-342
@@ -9,423 +9,352 @@ title: TypeScript
|
||||
---
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--color-foreground: #1a1a2e;
|
||||
--color-highlight: #d63384;
|
||||
--color-dimmed: #4a4a6a;
|
||||
}
|
||||
section.invert {
|
||||
--color-foreground: #fff;
|
||||
}
|
||||
section {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
h1 {
|
||||
color: #a02060;
|
||||
}
|
||||
section.invert h1 {
|
||||
color: #fff;
|
||||
}
|
||||
h2 {
|
||||
color: #1f2937;
|
||||
}
|
||||
section.invert h2 {
|
||||
color: #f48fb1;
|
||||
}
|
||||
pre {
|
||||
background: #0f0f23;
|
||||
color: #f48fb1;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #d63384;
|
||||
}
|
||||
pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
code {
|
||||
background: #1a1a2e;
|
||||
color: #f48fb1;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
a {
|
||||
color: var(--color-highlight);
|
||||
}
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _header: '' -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Web Engineering
|
||||
|
||||
**DHBW Stuttgart** · Informatik / Wirtschaftsinformatik
|
||||
|
||||
**Sommersemester 2026** · Sitzung 8 — TypeScript
|
||||
# Kapitel 6 — TypeScript
|
||||
## Typen über JavaScript
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
# Was ist TypeScript?
|
||||
|
||||
# TypeScript
|
||||
**Microsoft, 2012.** Aufsatz auf JavaScript mit **statischer Typprüfung.**
|
||||
|
||||
## JavaScript on Steroids
|
||||
- Schreibst TypeScript (`.ts`)
|
||||
- Compiler (`tsc`) prüft Typen + erzeugt JavaScript (`.js`)
|
||||
- Browser/Node führt das JS aus
|
||||
|
||||
**Heute:** Standard für mittlere + große JS-Projekte. ~95 % der React-Repos.
|
||||
|
||||
---
|
||||
|
||||
# Inhalt
|
||||
# Warum TypeScript?
|
||||
|
||||
1. TypeScript: was und warum
|
||||
2. `tsc` + `ts-node`
|
||||
3. `tsconfig.json`
|
||||
4. Type Primitives
|
||||
5. Union Types, Type Alias, Interface
|
||||
6. Enum, Generics, Type Casting
|
||||
7. Compilation: types stripped
|
||||
8. Decorators + reflect-metadata
|
||||
| Vorteil | Was bedeutet das |
|
||||
|---------|------------------|
|
||||
| **Fehler vor Laufzeit** | Compiler fängt typos, falsche Aufrufe |
|
||||
| **Bessere IDE** | Autocompletion, Refactoring, Jump-to-Definition |
|
||||
| **Selbst-Doku** | Typ-Signaturen sind ausführbare Doku |
|
||||
| **Refactoring sicher** | Compiler findet alle Auswirkungen |
|
||||
|
||||
**Nachteil:** Build-Step + Lernkurve.
|
||||
|
||||
---
|
||||
|
||||
# TypeScript – Was?
|
||||
# Setup
|
||||
|
||||
- **Superset von JavaScript** (Microsoft, 2012)
|
||||
- Jede `.js` lässt sich zu `.ts` umbenennen
|
||||
- **Type Errors zur Compile-/Transpile-Zeit**
|
||||
- Neuere ECMAScript-Features verfügbar
|
||||
- **Im Browser:** läuft nicht direkt – wird zu JS kompiliert
|
||||
|
||||
---
|
||||
|
||||
# Setup: tsc + ts-node
|
||||
|
||||
```bash
|
||||
npm i -D typescript ts-node @types/node
|
||||
npx tsc --init # erzeugt tsconfig.json
|
||||
```sh
|
||||
npm install -D typescript @types/node tsx
|
||||
npx tsc --init # tsconfig.json erzeugen
|
||||
```
|
||||
|
||||
**Run:**
|
||||
```bash
|
||||
npx ts-node index.ts # statt: node index.js
|
||||
```
|
||||
`tsx` = TypeScript direkt ausführen ohne Compile-Step (Dev).
|
||||
|
||||
**Build:**
|
||||
```bash
|
||||
npx tsc # kompiliert nach ./dist
|
||||
node dist/index.js
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "tsx --watch src/server.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/server.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# tsconfig.json
|
||||
# tsconfig.json — Wichtigste
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`strict: true`** = alle strengen Checks an. **Pflicht** für neue Projekte.
|
||||
|
||||
---
|
||||
|
||||
# Type Primitives
|
||||
# Basis-Typen
|
||||
|
||||
```typescript
|
||||
let loading: boolean = false;
|
||||
let count: number = 42;
|
||||
let pi: number = 3.14;
|
||||
let name: string = "DHBW ist fetzig!";
|
||||
let empty: null = null;
|
||||
let missing: undefined = undefined;
|
||||
let name: string = "Ada";
|
||||
let age: number = 56;
|
||||
let admin: boolean = true;
|
||||
let nothing: null = null;
|
||||
let unset: undefined = undefined;
|
||||
|
||||
function log(msg: string): void {
|
||||
console.log(msg);
|
||||
let hobbies: string[] = ["math", "chess"];
|
||||
let tuple: [string, number] = ["Ada", 56];
|
||||
|
||||
let anyValue: any = "vermeiden"; // KEIN typ-check
|
||||
let unknownValue: unknown = await fetch(); // sicherer als any
|
||||
```
|
||||
|
||||
`any` deaktiviert TS-Schutz — vermeiden.
|
||||
|
||||
---
|
||||
|
||||
# Object-Typen + Interfaces
|
||||
|
||||
```typescript
|
||||
// inline
|
||||
function greet(user: { name: string; age?: number }) {
|
||||
return `Hallo ${user.name}`;
|
||||
}
|
||||
```
|
||||
|
||||
`any`, `unknown`, `never`, `void` sind weitere TS-Spezialtypen.
|
||||
|
||||
---
|
||||
|
||||
# Union Types
|
||||
|
||||
```typescript
|
||||
let id: number | string;
|
||||
|
||||
id = 101; // ok
|
||||
id = "101"; // ok
|
||||
id = true; // TypeError (nicht erlaubt)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Type Alias
|
||||
|
||||
```typescript
|
||||
type NumOrString = number | string;
|
||||
|
||||
let id: NumOrString;
|
||||
id = 101;
|
||||
id = "abc";
|
||||
|
||||
type UserId = string;
|
||||
type Status = "active" | "inactive" | "pending";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Interface
|
||||
|
||||
```typescript
|
||||
// Interface
|
||||
interface User {
|
||||
name: string;
|
||||
age?: number; // optional
|
||||
readonly id: number; // schreibgeschützt
|
||||
}
|
||||
|
||||
const ada: User = { id: 1, name: "Ada", age: 56 };
|
||||
|
||||
// Type-Alias (Interface mit anderem Namen, mehr Flexibilität)
|
||||
type Status = "pending" | "active" | "deleted";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Function-Typen
|
||||
|
||||
```typescript
|
||||
// Argument + Return
|
||||
function add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// Arrow + ausgeschrieben
|
||||
const multiply: (a: number, b: number) => number = (a, b) => a * b;
|
||||
|
||||
// async-Funktionen geben Promise zurück
|
||||
async function fetchUser(id: number): Promise<User> {
|
||||
const res = await fetch(`/users/${id}`);
|
||||
return res.json();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Union + Intersection
|
||||
|
||||
```typescript
|
||||
// Union — "ODER"
|
||||
type Id = string | number;
|
||||
function lookup(id: string | number) { /* ... */ }
|
||||
|
||||
// Discriminated Union
|
||||
type Shape =
|
||||
| { kind: "circle"; radius: number }
|
||||
| { kind: "square"; size: number };
|
||||
|
||||
function area(s: Shape) {
|
||||
if (s.kind === "circle") return Math.PI * s.radius ** 2;
|
||||
return s.size ** 2; // TS weiß: hier ist's square
|
||||
}
|
||||
|
||||
// Intersection — "UND"
|
||||
type Admin = User & { permissions: string[] };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Generics — typ-flexible Funktionen
|
||||
|
||||
```typescript
|
||||
// T ist ein Platzhalter
|
||||
function first<T>(arr: T[]): T | undefined {
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
first([1, 2, 3]); // T = number
|
||||
first(["a", "b"]); // T = string
|
||||
|
||||
// Generic-Constraints
|
||||
function longest<T extends { length: number }>(a: T, b: T): T {
|
||||
return a.length >= b.length ? a : b;
|
||||
}
|
||||
|
||||
longest("hello", "hi"); // OK
|
||||
longest([1, 2], [3, 4, 5]); // OK
|
||||
longest(5, 10); // FEHLER: number hat kein length
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Built-in Generic-Typen
|
||||
|
||||
```typescript
|
||||
// Array<T> = T[]
|
||||
const nums: Array<number> = [1, 2, 3];
|
||||
|
||||
// Promise<T>
|
||||
async function getUser(): Promise<User> { /* ... */ }
|
||||
|
||||
// Partial<T> — alle Properties optional
|
||||
type UserUpdate = Partial<User>;
|
||||
|
||||
// Required<T> — alle Pflicht
|
||||
// Pick<T, K> — nur ausgewählte
|
||||
// Omit<T, K> — ohne ausgewählte
|
||||
type PublicUser = Omit<User, "password">;
|
||||
|
||||
// Record<K, V> — Dictionary
|
||||
const config: Record<string, string> = { host: "...", port: "8080" };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Typ-Inference
|
||||
|
||||
TS errät den Typ oft selbst — **nicht überall annotieren.**
|
||||
|
||||
```typescript
|
||||
let name = "Ada"; // automatisch: string
|
||||
const numbers = [1, 2, 3]; // automatisch: number[]
|
||||
|
||||
function double(n: number) { // Return-Typ inferiert: number
|
||||
return n * 2;
|
||||
}
|
||||
|
||||
// Wann annotieren?
|
||||
// - Funktion-Args (immer)
|
||||
// - Public-API Return-Typen (Pflicht für Library)
|
||||
// - wenn Inference falsch oder unklar
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Typen für REST-API
|
||||
|
||||
```typescript
|
||||
// Schema einmal definieren
|
||||
interface CreateUserBody {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface UserResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
email?: string; // optional
|
||||
readonly createdAt: Date; // schreibgeschützt
|
||||
greet(msg: string): void;
|
||||
email: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const u: User = {
|
||||
id: 1,
|
||||
name: "Lisa",
|
||||
createdAt: new Date(),
|
||||
greet(msg) { console.log(msg); }
|
||||
};
|
||||
// Express-Route typisiert
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
app.post('/users', (req: Request<{}, {}, CreateUserBody>, res: Response<UserResponse>) => {
|
||||
const { name, email } = req.body; // typsicher
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Interface vs Type
|
||||
# Zod — Runtime-Validation
|
||||
|
||||
**Interface**
|
||||
- nur Objektformen
|
||||
- kann mehrfach deklariert (Merging)
|
||||
- bei perf-kritischen Checks oft schneller
|
||||
|
||||
**Type Alias**
|
||||
- alles: Primitive, Union, Tuple, Function-Signaturen
|
||||
- nicht erweiterbar nach Definition
|
||||
|
||||
→ Faustregel: **Interfaces für API-Shapes, Types für alles andere.**
|
||||
|
||||
---
|
||||
|
||||
# Enum
|
||||
TypeScript prüft zur Compile-Time. **Was kommt aus dem Netz?**
|
||||
|
||||
```typescript
|
||||
enum Direction {
|
||||
Up = 1,
|
||||
Down, // 2
|
||||
Left, // 3
|
||||
Right // 4
|
||||
}
|
||||
import { z } from 'zod';
|
||||
|
||||
function walk(d: Direction): string {
|
||||
switch (d) {
|
||||
case Direction.Up: return "bow";
|
||||
case Direction.Down: return "stern";
|
||||
case Direction.Left: return "windward";
|
||||
case Direction.Right: return "leeward";
|
||||
}
|
||||
}
|
||||
const UserSchema = z.object({
|
||||
name: z.string().min(2),
|
||||
email: z.string().email(),
|
||||
age: z.number().int().positive().optional(),
|
||||
});
|
||||
|
||||
walk(Direction.Up); // "bow"
|
||||
type User = z.infer<typeof UserSchema>; // Typ aus Schema
|
||||
|
||||
app.post('/users', (req, res) => {
|
||||
const result = UserSchema.safeParse(req.body);
|
||||
if (!result.success) return res.status(400).json(result.error);
|
||||
|
||||
const user = result.data; // typsicher + validated
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Generics
|
||||
# `any` vs `unknown` vs `never`
|
||||
|
||||
```typescript
|
||||
class GenericContainer<T> {
|
||||
private readonly value: T;
|
||||
let a: any = "test";
|
||||
a.toFixed(); // OK — TS prüft NICHTS
|
||||
|
||||
constructor(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
let u: unknown = "test";
|
||||
u.toFixed(); // FEHLER — erst type-narrow
|
||||
if (typeof u === 'number') u.toFixed(); // OK
|
||||
|
||||
getValue(): T {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
const numbers = new GenericContainer<number>(10);
|
||||
const strings = new GenericContainer<string>("Hello");
|
||||
|
||||
console.log(numbers.getValue()); // 10
|
||||
console.log(strings.getValue()); // "Hello"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Type Casting
|
||||
|
||||
```typescript
|
||||
let someValue: any = "this is a string";
|
||||
|
||||
let len: number = (someValue as string).length;
|
||||
console.log(len); // 16
|
||||
|
||||
// Alt-Syntax (nicht in TSX):
|
||||
let len2: number = (<string>someValue).length;
|
||||
```
|
||||
|
||||
`as unknown as T` für "double cast" – nur Notbehelf!
|
||||
|
||||
---
|
||||
|
||||
# Compilation: Types werden gestripped
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
type Result = "pass" | "fail";
|
||||
|
||||
function verify(result: Result) {
|
||||
if (result === "pass") console.log("Passed");
|
||||
else console.log("Failed");
|
||||
function fail(msg: string): never {
|
||||
throw new Error(msg); // kommt nie zurück
|
||||
}
|
||||
```
|
||||
|
||||
**JavaScript (nach `tsc`):**
|
||||
```javascript
|
||||
function verify(result) {
|
||||
if (result === "pass") console.log("Passed");
|
||||
else console.log("Failed");
|
||||
}
|
||||
```
|
||||
|
||||
**Types existieren nur zur Compile-Zeit.** Kein Runtime-Check.
|
||||
**`unknown` statt `any`** — sicherer Default für fremde Daten.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
# Best Practices
|
||||
|
||||
# Decorators
|
||||
|
||||
## Meta-Programmierung mit TypeScript
|
||||
- **`strict: true`** in tsconfig — keine Diskussion
|
||||
- **Argument-Typen** annotieren, Return-Typen nur public-API
|
||||
- **Interfaces** für Objekt-Strukturen, **Type-Aliases** für Unions
|
||||
- **`unknown`** statt `any`
|
||||
- **Zod / Valibot** für Runtime-Validation
|
||||
- **Typen nicht doppelt:** ein Schema, davon Typ ableiten (`z.infer`)
|
||||
- **CI-Step:** `tsc --noEmit` muss grün sein
|
||||
|
||||
---
|
||||
|
||||
# TypeScript: Decorators
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||
Vier Typen:
|
||||
# Selbstlernen — eure API zu TS migrieren
|
||||
|
||||
- **Class Decorators** – auf ganze Klasse
|
||||
- **Property Decorators** – auf Felder
|
||||
- **Method Decorators** – auf Methoden
|
||||
- **Parameter Decorators** – auf Methoden-Parameter
|
||||
1. **TypeScript installieren:** `npm install -D typescript @types/node @types/express tsx`
|
||||
2. **tsconfig.json** mit `strict: true`
|
||||
3. **`.js` → `.ts`** umbenennen, fehlende Typen ergänzen
|
||||
4. **Zod-Schema** für POST-Bodies
|
||||
5. **CI-Step:** `tsc --noEmit` in Tests
|
||||
|
||||
Genutzt von: Angular, NestJS, TypeORM, class-validator.
|
||||
**Bonus:** Drizzle-ORM verwenden (TypeScript-first).
|
||||
|
||||
---
|
||||
|
||||
# Decorators aktivieren
|
||||
# Zusammenfassung
|
||||
|
||||
**`tsconfig.json`:**
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Root-File:**
|
||||
```typescript
|
||||
import "reflect-metadata";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Decorator-Signaturen
|
||||
|
||||
```typescript
|
||||
type ClassDecorator = <T extends Function>(target: T) => T | void;
|
||||
|
||||
type PropertyDecorator = (
|
||||
target: Object,
|
||||
propertyKey: string | symbol
|
||||
) => void;
|
||||
|
||||
type MethodDecorator = <T>(
|
||||
target: Object,
|
||||
propertyKey: string | symbol,
|
||||
descriptor: TypedPropertyDescriptor<T>
|
||||
) => TypedPropertyDescriptor<T> | void;
|
||||
|
||||
type ParameterDecorator = (
|
||||
target: Object,
|
||||
propertyKey: string | symbol | undefined,
|
||||
parameterIndex: number
|
||||
) => void;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Decorator – Beispiel
|
||||
|
||||
```typescript
|
||||
function Log(target: any, key: string, desc: PropertyDescriptor) {
|
||||
const original = desc.value;
|
||||
desc.value = function (...args: any[]) {
|
||||
console.log(`→ ${key}(${JSON.stringify(args)})`);
|
||||
return original.apply(this, args);
|
||||
};
|
||||
}
|
||||
|
||||
class Calculator {
|
||||
@Log
|
||||
add(a: number, b: number) {
|
||||
return a + b;
|
||||
}
|
||||
}
|
||||
|
||||
new Calculator().add(2, 3); // logs: → add([2,3])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Reflect: Meta-Daten lesen
|
||||
|
||||
```typescript
|
||||
import "reflect-metadata";
|
||||
|
||||
Reflect.defineMetadata("role", "admin", target);
|
||||
Reflect.hasMetadata("role", target); // true
|
||||
Reflect.getMetadata("role", target); // "admin"
|
||||
```
|
||||
|
||||
Frameworks (NestJS, TypeORM) nutzen das, um zur **Runtime** Type-Informationen zu rekonstruieren – z. B. für DI, ORM-Mapping, Validierung.
|
||||
|
||||
---
|
||||
|
||||
# Anwendung: NestJS-Beispiel
|
||||
|
||||
```typescript
|
||||
@Controller("users")
|
||||
export class UsersController {
|
||||
constructor(private users: UsersService) {}
|
||||
|
||||
@Get(":id")
|
||||
findOne(@Param("id") id: string) {
|
||||
return this.users.find(+id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`@Controller`, `@Get`, `@Param` sind Decorators – Metadaten ⇒ Routing.
|
||||
**Heute gelernt:**
|
||||
- TS als statischer Typ-Aufsatz auf JS
|
||||
- **`strict: true`** als Baseline
|
||||
- Basis-Typen, Interfaces, Type-Aliases
|
||||
- **Union** + **Generics** als zentrale Konzepte
|
||||
- Built-in Generics (Partial, Omit, Record)
|
||||
- **Typ-Inference** nutzen — nicht über-annotieren
|
||||
- **Zod** für Runtime-Validation
|
||||
- `unknown` statt `any`
|
||||
|
||||
**Nächste Stunde:** Docker — Container für reproducible Setups.
|
||||
|
||||
+268
-232
@@ -9,339 +9,375 @@ title: Docker und Service Orchestration
|
||||
---
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--color-foreground: #1a1a2e;
|
||||
--color-highlight: #d63384;
|
||||
--color-dimmed: #4a4a6a;
|
||||
}
|
||||
section.invert {
|
||||
--color-foreground: #fff;
|
||||
}
|
||||
section {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
h1 {
|
||||
color: #a02060;
|
||||
}
|
||||
section.invert h1 {
|
||||
color: #fff;
|
||||
}
|
||||
h2 {
|
||||
color: #1f2937;
|
||||
}
|
||||
section.invert h2 {
|
||||
color: #f48fb1;
|
||||
}
|
||||
pre {
|
||||
background: #0f0f23;
|
||||
color: #f48fb1;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #d63384;
|
||||
}
|
||||
pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
code {
|
||||
background: #1a1a2e;
|
||||
color: #f48fb1;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
a {
|
||||
color: var(--color-highlight);
|
||||
}
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _header: '' -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Web Engineering
|
||||
|
||||
**DHBW Stuttgart** · Informatik / Wirtschaftsinformatik
|
||||
|
||||
**Sommersemester 2026** · Sitzung 9 — Docker, Proxies and DBs
|
||||
# Kapitel 7 — Docker
|
||||
## Container für reproducible Setups
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
# „Auf meinem Rechner läuft's"
|
||||
|
||||
# Docker
|
||||
Das klassische Software-Problem:
|
||||
|
||||
## Service Orchestration
|
||||
- Entwickler: Node 22 + Postgres 16 lokal
|
||||
- Server: Node 18 + Postgres 14
|
||||
- → bricht beim Deploy
|
||||
|
||||
**Lösung:** App + Abhängigkeiten als **Container** verpacken.
|
||||
|
||||
---
|
||||
|
||||
# Inhalt
|
||||
# Container vs. VM
|
||||
|
||||
1. Architektur: Reverse Proxy + API + DB
|
||||
2. Container vs VM
|
||||
3. `Dockerfile`
|
||||
4. `compose.yml`
|
||||
5. Docker CLI / Compose Befehle
|
||||
6. Live Demo: `dhbw-docker`
|
||||
| | Container | Virtual Machine |
|
||||
|--|-----------|-----------------|
|
||||
| Startzeit | ms | min |
|
||||
| Größe | MB | GB |
|
||||
| Kernel | shared (Host-Linux) | eigener |
|
||||
| Isolation | Prozess-Level | Hardware-Level |
|
||||
| Overhead | minimal | hoch |
|
||||
|
||||
Container = leichtgewichtige Prozess-Isolation.
|
||||
|
||||
---
|
||||
|
||||
# Architektur
|
||||
# Docker — die Idee
|
||||
|
||||
```
|
||||
Browser (localhost:10007)
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ Reverse Proxy │ (nginx / traefik)
|
||||
└─────────┬────────┘
|
||||
┌────┴────┐
|
||||
▼ ▼
|
||||
┌─────────┐ ┌────────┐
|
||||
│ Web │ │ API │
|
||||
│ App │ │ (Node) │
|
||||
└────┬────┘ └────┬───┘
|
||||
└─────┬──────┘
|
||||
▼
|
||||
┌────────┐
|
||||
│ DB │ (Postgres)
|
||||
└────────┘
|
||||
- **Image** = Schnappschuss (App + Files + OS-Layer)
|
||||
- **Container** = laufende Instanz eines Images
|
||||
- **Dockerfile** = Anleitung zum Image-Bauen
|
||||
- **Registry** = Image-Speicher (Docker Hub, GHCR)
|
||||
|
||||
```sh
|
||||
docker pull node:24
|
||||
docker run node:24 node --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Container vs VM
|
||||
|
||||
| | VM | Container |
|
||||
|---|----|-----------|
|
||||
| Kernel | eigener | Host-Kernel |
|
||||
| Boot | Minuten | Sekunden |
|
||||
| Größe | GB | MB |
|
||||
| Isolation | stark | namespaces, cgroups |
|
||||
| Image | komplett | Layer (diff) |
|
||||
|
||||
→ **Docker** = Container-Engine + Image-Format + Registry.
|
||||
|
||||
---
|
||||
|
||||
# Dockerfile – Express API
|
||||
# Dockerfile — Hallo Welt
|
||||
|
||||
```dockerfile
|
||||
FROM node:lts-slim AS builder
|
||||
FROM node:24-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./package*.json ./
|
||||
RUN npm ci --no-audit --no-fund --no-progress --loglevel=error
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY ./ ./
|
||||
COPY . .
|
||||
|
||||
ENV NODE_ENV="production"
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["npm", "start"]
|
||||
EXPOSE 3000
|
||||
CMD ["node", "src/server.js"]
|
||||
```
|
||||
|
||||
**`npm ci`** statt `npm i` für reproduzierbare Builds.
|
||||
```sh
|
||||
docker build -t mein-app .
|
||||
docker run -p 3000:3000 mein-app
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Dockerfile-Schichten verstehen
|
||||
|
||||
Jedes `RUN`/`COPY`/`ADD` = neue Schicht.
|
||||
|
||||
```dockerfile
|
||||
# Schlecht — npm install nach JEDER Code-Änderung
|
||||
COPY . .
|
||||
RUN npm ci
|
||||
|
||||
# Gut — package.json zuerst, npm install gecached
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
```
|
||||
|
||||
**Build-Cache** spart Minuten.
|
||||
|
||||
---
|
||||
|
||||
# Multi-Stage Build
|
||||
|
||||
```dockerfile
|
||||
FROM node:lts-slim AS builder
|
||||
# Stage 1: Build mit allem
|
||||
FROM node:24-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
RUN npm ci && npm run build
|
||||
|
||||
FROM node:lts-slim AS runtime
|
||||
# Stage 2: nur Runtime
|
||||
FROM node:24-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY package*.json ./
|
||||
EXPOSE 8080
|
||||
CMD ["node", "dist/index.js"]
|
||||
EXPOSE 3000
|
||||
CMD ["node", "dist/server.js"]
|
||||
```
|
||||
|
||||
→ kleines Final-Image, keine Build-Tools im Runtime.
|
||||
Finales Image: kleiner, sicherer (kein Build-Tools drin).
|
||||
|
||||
---
|
||||
|
||||
# Image-Größen reduzieren
|
||||
|
||||
| Base | Größe |
|
||||
|------|------:|
|
||||
| `node:24` | ~1.1 GB |
|
||||
| `node:24-slim` | ~280 MB |
|
||||
| `node:24-alpine` | **~180 MB** |
|
||||
| Distroless | ~150 MB |
|
||||
|
||||
**`alpine`** = Alpine Linux, sehr klein. **Distroless** = nur Runtime, kein Shell.
|
||||
|
||||
---
|
||||
|
||||
# .dockerignore
|
||||
|
||||
Verhindert dass Müll ins Image kommt:
|
||||
|
||||
```
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.env
|
||||
*.log
|
||||
coverage
|
||||
build/
|
||||
dist/
|
||||
.DS_Store
|
||||
README.md
|
||||
.github/
|
||||
```
|
||||
|
||||
Spart Build-Kontext und verhindert Geheimnis-Leaks.
|
||||
Schneller Build, kleineres Image, weniger Secrets im Image.
|
||||
|
||||
---
|
||||
|
||||
# compose.yml – Services
|
||||
<!-- _class: lead -->
|
||||
|
||||
# docker compose
|
||||
|
||||
---
|
||||
|
||||
# Multi-Container — die Realität
|
||||
|
||||
Eine App braucht oft:
|
||||
- App-Server
|
||||
- Datenbank
|
||||
- Cache (Redis)
|
||||
- Reverse Proxy
|
||||
|
||||
**docker compose** orchestriert das.
|
||||
|
||||
---
|
||||
|
||||
# compose.yml — Beispiel
|
||||
|
||||
```yaml
|
||||
name: dhbw-docker-app
|
||||
|
||||
services:
|
||||
api:
|
||||
build: ./api
|
||||
container_name: dhbw-api
|
||||
environment:
|
||||
DATABASE_USERNAME: ${DATABASE_USERNAME}
|
||||
DATABASE_PASSWORD: ${DATABASE_PASSWORD}
|
||||
DATABASE_NAME: ${DATABASE_NAME}
|
||||
DATABASE_HOST: db
|
||||
DATABASE_PORT: 5432
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "10000:8080"
|
||||
networks: [net, data]
|
||||
- "3000:3000"
|
||||
environment:
|
||||
DATABASE_URL: postgres://user:pass@db:5432/app
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
```
|
||||
- db
|
||||
|
||||
---
|
||||
|
||||
# compose.yml – Datenbank + Proxy
|
||||
|
||||
```yaml
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: ${DATABASE_USERNAME}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||
POSTGRES_DB: ${DATABASE_NAME}
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: pass
|
||||
POSTGRES_DB: app
|
||||
volumes:
|
||||
- dbdata:/var/lib/postgresql/data
|
||||
networks: [data]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $$DATABASE_USERNAME"]
|
||||
interval: 5s
|
||||
retries: 5
|
||||
|
||||
proxy:
|
||||
image: nginx:alpine
|
||||
ports: ["10007:80"]
|
||||
volumes: ["./nginx.conf:/etc/nginx/nginx.conf:ro"]
|
||||
networks: [net]
|
||||
depends_on: [api]
|
||||
- db-data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
dbdata:
|
||||
|
||||
networks:
|
||||
net:
|
||||
data:
|
||||
db-data:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Docker Compose – Befehle
|
||||
# compose-Commands
|
||||
|
||||
```bash
|
||||
docker compose -p "dhbw-docker-app" \
|
||||
-f compose.yml \
|
||||
--env-file .env \
|
||||
up \
|
||||
--build \
|
||||
--remove-orphans \
|
||||
--force-recreate
|
||||
```
|
||||
```sh
|
||||
docker compose up # alles starten (Foreground)
|
||||
docker compose up -d # detached (background)
|
||||
docker compose down # alles stoppen + entfernen
|
||||
docker compose down -v # ... und Volumes löschen
|
||||
|
||||
| Flag | Bedeutung |
|
||||
|------|-----------|
|
||||
| `-p` | Projektname |
|
||||
| `-f` | mehrere Compose-Files möglich |
|
||||
| `--env-file` | `.env` einlesen |
|
||||
| `--build` | Images neu bauen |
|
||||
| `--remove-orphans` | verwaiste Container weg |
|
||||
| `--force-recreate` | Container neu starten |
|
||||
|
||||
---
|
||||
|
||||
# Compose – Alltag
|
||||
|
||||
```bash
|
||||
docker compose up -d # detached starten
|
||||
docker compose ps # Status
|
||||
docker compose logs -f api # Logs streamen
|
||||
docker compose exec api sh # Shell im Container
|
||||
docker compose down -v # Stop + Volumes löschen
|
||||
docker compose restart api # Neustart einzelner Service
|
||||
docker compose logs -f app # Logs streamen
|
||||
docker compose exec app sh # Shell im laufenden Container
|
||||
docker compose ps # Status sehen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Health Checks
|
||||
# Networking in compose
|
||||
|
||||
Standard: alle Services im selben `default`-Netzwerk.
|
||||
|
||||
```yaml
|
||||
api:
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
services:
|
||||
app:
|
||||
environment:
|
||||
# `db` ist der Service-Name → DNS-Hostname
|
||||
DATABASE_URL: postgres://...@db:5432/app
|
||||
```
|
||||
|
||||
`depends_on: { condition: service_healthy }` wartet, bis Health passt.
|
||||
**Service-Namen werden zu Hostnamen.** Kein localhost zwischen Containern.
|
||||
|
||||
---
|
||||
|
||||
# Networks: Segmentierung
|
||||
|
||||
- **`net`** – öffentlich erreichbar (Proxy — App)
|
||||
- **`data`** – nur intern (App — DB)
|
||||
- DB ist **nicht** vom Internet erreichbar
|
||||
- Service-DNS: `api`, `db`, `proxy` (Container-Name = Hostname)
|
||||
|
||||
→ Defense in Depth.
|
||||
|
||||
---
|
||||
|
||||
# Volumes – Daten persistent
|
||||
# Volumes — Daten überleben
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
dbdata: # benannt
|
||||
- ./code:/app # bind mount (dev)
|
||||
- /app/node_modules # anonymous (überschreibt bind)
|
||||
db-data: # named volume (managed by Docker)
|
||||
|
||||
services:
|
||||
db:
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data # named
|
||||
- ./backups:/backups # bind mount
|
||||
```
|
||||
|
||||
`docker compose down` ohne `-v` lässt Volumes leben.
|
||||
**Named Volumes** für DB-Daten. **Bind Mounts** für lokale Config.
|
||||
|
||||
---
|
||||
|
||||
# Live Demo
|
||||
# Environment + Secrets
|
||||
|
||||
Repo: https://github.com/nextlevelshit/dhbw-docker
|
||||
|
||||
```bash
|
||||
git clone https://github.com/nextlevelshit/dhbw-docker
|
||||
cd dhbw-docker
|
||||
cp .env.example .env
|
||||
docker compose up --build
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
env_file: .env # ganze .env-Datei laden
|
||||
environment:
|
||||
NODE_ENV: production # einzeln setzen
|
||||
DATABASE_URL: ${DATABASE_URL} # vom Host
|
||||
```
|
||||
|
||||
→ http://localhost:10007
|
||||
**Secrets nicht ins compose.yml.** Lieber `.env` (gitignored) oder Docker Secrets.
|
||||
|
||||
---
|
||||
|
||||
# 100 Concepts of Docker (Exkurs)
|
||||
<!-- _class: lead -->
|
||||
|
||||
- Image · Container · Volume · Network
|
||||
- Dockerfile · Layer Cache · BuildKit
|
||||
- Compose · Stack · Swarm · Kubernetes
|
||||
- Registry · Tag · Digest
|
||||
- Dev/Prod-Parity (12-Factor)
|
||||
- Secrets · Configs
|
||||
- Logging Drivers
|
||||
- Health Checks · Restart Policies
|
||||
# CI/CD
|
||||
|
||||
---
|
||||
|
||||
# Docker in GitHub Actions
|
||||
|
||||
```yaml
|
||||
name: Build + Push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- uses: docker/build-push-action@v5
|
||||
with:
|
||||
push: true
|
||||
tags: user/app:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Container-Registry
|
||||
|
||||
| Registry | Stärke |
|
||||
|----------|--------|
|
||||
| **Docker Hub** | Standard, kostenlos für Public |
|
||||
| **GHCR** (GitHub) | mit Repo verbunden |
|
||||
| **GitLab Registry** | mit Repo verbunden |
|
||||
| **AWS ECR** | für AWS-Hosting |
|
||||
|
||||
```sh
|
||||
# Tag + Push
|
||||
docker tag mein-app ghcr.io/user/mein-app:v1.0.0
|
||||
docker push ghcr.io/user/mein-app:v1.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Health-Checks
|
||||
|
||||
```dockerfile
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
|
||||
```
|
||||
|
||||
```javascript
|
||||
// In Express
|
||||
app.get('/health', (req, res) => res.json({ ok: true }));
|
||||
```
|
||||
|
||||
Orchestrators (k8s, ECS, Coolify) entscheiden via HealthCheck, ob neu starten.
|
||||
|
||||
---
|
||||
|
||||
# Best Practices
|
||||
|
||||
- **`alpine`** oder **distroless** als Base
|
||||
- **Multi-stage builds** für kleinere Images
|
||||
- **`.dockerignore`** anlegen
|
||||
- **Non-root user** im Container (`USER node`)
|
||||
- **`COPY` nach `RUN npm ci`** für Cache-Hit
|
||||
- **Tags statt `latest`** für Reproducibility
|
||||
- **Health-Checks** für Orchestration
|
||||
- **Secrets via env, niemals im Image**
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||
# Selbstlernen — App dockerizen
|
||||
|
||||
1. **Dockerfile** für eure Node-API (multi-stage)
|
||||
2. **.dockerignore** mit node_modules, .env, .git
|
||||
3. **compose.yml** mit App + Postgres
|
||||
4. **`docker compose up`** lokal starten
|
||||
5. **Health-Check** auf `/health`-Endpoint
|
||||
|
||||
**Bonus:** GitHub-Actions-Workflow, der Image bei jedem Push baut.
|
||||
|
||||
---
|
||||
|
||||
# Zusammenfassung
|
||||
|
||||
**Heute gelernt:**
|
||||
- Container vs. VM — Prozess-Isolation, leicht + schnell
|
||||
- **Dockerfile** mit `FROM`, `COPY`, `RUN`, `CMD`
|
||||
- **Multi-stage Builds** für kleine Production-Images
|
||||
- **alpine** + Distroless als Base
|
||||
- **docker compose** für Multi-Container-Setups
|
||||
- **Volumes** für persistente Daten
|
||||
- **Service-Namen als Hostnamen** im compose-Netzwerk
|
||||
- **CI/CD** mit GitHub Actions + Registry
|
||||
|
||||
**Nächste Stunde:** Best Practices — Git, README, SOLID, 12-Factor.
|
||||
|
||||
+385
-261
@@ -9,353 +9,477 @@ title: Best Practices
|
||||
---
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--color-foreground: #1a1a2e;
|
||||
--color-highlight: #d63384;
|
||||
--color-dimmed: #4a4a6a;
|
||||
}
|
||||
section.invert {
|
||||
--color-foreground: #fff;
|
||||
}
|
||||
section {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
h1 {
|
||||
color: #a02060;
|
||||
}
|
||||
section.invert h1 {
|
||||
color: #fff;
|
||||
}
|
||||
h2 {
|
||||
color: #1f2937;
|
||||
}
|
||||
section.invert h2 {
|
||||
color: #f48fb1;
|
||||
}
|
||||
pre {
|
||||
background: #0f0f23;
|
||||
color: #f48fb1;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #d63384;
|
||||
}
|
||||
pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
code {
|
||||
background: #1a1a2e;
|
||||
color: #f48fb1;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
a {
|
||||
color: var(--color-highlight);
|
||||
}
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _header: '' -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Web Engineering
|
||||
|
||||
**DHBW Stuttgart** · Informatik / Wirtschaftsinformatik
|
||||
|
||||
**Sommersemester 2026** · Sitzung 10 — Wrap-up & Projektsupport
|
||||
# Kapitel 8 — Best Practices
|
||||
## Git · README · SOLID · 12-Factor · SemVer
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Best Practices
|
||||
|
||||
## Wrap-up & Projektsupport
|
||||
# Git-Hygiene
|
||||
|
||||
---
|
||||
|
||||
# Inhalt
|
||||
|
||||
1. Semantic Versioning
|
||||
2. Git Commit Messages (Udacity Style)
|
||||
3. 12 Factor App
|
||||
4. Make it Work · Make it Right · Make it Fast
|
||||
5. Präsentations-Tipps
|
||||
6. Feedback (Sandwich-Methode)
|
||||
7. Prüfungsleistung Recap
|
||||
|
||||
---
|
||||
|
||||
# Semantic Versioning (SemVer 2.0.0)
|
||||
|
||||
```
|
||||
MAJOR.MINOR.PATCH
|
||||
│ │ └─ Bugfix, abwärtskompatibel
|
||||
│ └────── Neue Funktion, abwärtskompatibel
|
||||
└──────────── Breaking Change
|
||||
```
|
||||
|
||||
**Beispiele:**
|
||||
- `1.4.2` → `1.4.3` Bugfix
|
||||
- `1.4.3` → `1.5.0` neues Feature
|
||||
- `1.5.0` → `2.0.0` Breaking Change
|
||||
|
||||
Pre-Release: `1.0.0-alpha.1`, `1.0.0-rc.2`
|
||||
|
||||
→ https://semver.org/
|
||||
|
||||
---
|
||||
|
||||
# package.json: Versionsranges
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"express": "^4.19.2", // ≥4.19.2 < 5.0.0 (gleiche MAJOR)
|
||||
"lodash": "~4.17.21", // ≥4.17.21 < 4.18.0 (gleiche MINOR)
|
||||
"react": "19.0.0" // exakt
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`package-lock.json` pinnt die **tatsächlich installierten** Versionen.
|
||||
|
||||
---
|
||||
|
||||
# Git Commit Messages – Udacity Style
|
||||
|
||||
```
|
||||
<type>: <Subject>
|
||||
<blank>
|
||||
<Body>
|
||||
<blank>
|
||||
<Footer>
|
||||
```
|
||||
|
||||
**Type:** `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
|
||||
|
||||
**Subject:** Imperativ, ≤50 Zeichen, klein, kein Punkt
|
||||
**Body:** **Was** und **Warum** – nicht **Wie**
|
||||
**Footer:** Issue-Refs, Breaking Changes
|
||||
|
||||
---
|
||||
|
||||
# Commit-Beispiele
|
||||
|
||||
**Gut:**
|
||||
```
|
||||
feat: add user logout endpoint
|
||||
|
||||
Allows authenticated clients to invalidate their session token
|
||||
on the server side instead of only deleting the cookie.
|
||||
|
||||
Closes #142
|
||||
```
|
||||
# Sinnvolle Commits
|
||||
|
||||
**Schlecht:**
|
||||
```
|
||||
fixed stuff
|
||||
WIP
|
||||
update
|
||||
fix
|
||||
wip
|
||||
asdf
|
||||
final
|
||||
final2
|
||||
final-final
|
||||
```
|
||||
|
||||
→ https://udacity.github.io/git-styleguide/
|
||||
**Gut:**
|
||||
```
|
||||
fix(auth): handle expired JWT correctly
|
||||
feat(api): add /users/:id/posts endpoint
|
||||
docs(readme): update Docker setup instructions
|
||||
test(utils): cover edge cases in greet()
|
||||
```
|
||||
|
||||
**Eine Commit-Message = Was + Warum, nicht Wie.**
|
||||
|
||||
---
|
||||
|
||||
# Conventional Commits
|
||||
|
||||
```
|
||||
feat(auth): add OAuth2 login
|
||||
fix(api): return 404 instead of 500 on missing user
|
||||
docs: update README install steps
|
||||
chore(deps): bump express from 4.18 to 4.19
|
||||
BREAKING CHANGE: drop Node 16 support
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
Vorteile: automatische Changelogs, automatische Version-Bumps (`semantic-release`).
|
||||
**Types:**
|
||||
- `feat:` neue Funktion
|
||||
- `fix:` Bugfix
|
||||
- `docs:` Doku
|
||||
- `refactor:` Code-Restrukturierung
|
||||
- `test:` Tests
|
||||
- `chore:` Build/Tools
|
||||
- `perf:` Performance
|
||||
- `style:` Formatierung
|
||||
|
||||
Automatisches Changelog + SemVer-Bump möglich.
|
||||
|
||||
---
|
||||
|
||||
# 12 Factor App
|
||||
# .gitignore — was nie rein gehört
|
||||
|
||||
Methodik für moderne, skalierbare SaaS-Apps – https://12factor.net/
|
||||
```
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
| # | Faktor |
|
||||
|---|--------|
|
||||
| 1 | Codebase – ein Repo, viele Deploys |
|
||||
| 2 | Dependencies – explizit deklariert (`package.json`) |
|
||||
| 3 | Config – via Environment-Variablen |
|
||||
| 4 | Backing Services – als Attached Resources |
|
||||
| 5 | Build, Release, Run – getrennt |
|
||||
| 6 | Processes – stateless |
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
.next/
|
||||
.cache/
|
||||
|
||||
# Secrets
|
||||
.env
|
||||
.env.local
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
# Editor + OS
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
```
|
||||
|
||||
Vorlagen: github.com/github/gitignore
|
||||
|
||||
---
|
||||
|
||||
# 12 Factor (2/2)
|
||||
# Branches + PRs
|
||||
|
||||
| # | Faktor |
|
||||
|---|--------|
|
||||
| 7 | Port Binding – self-contained, exportiert HTTP |
|
||||
| 8 | Concurrency – horizontal skalieren via Prozesse |
|
||||
| 9 | Disposability – schneller Start, Graceful Shutdown |
|
||||
| 10 | Dev/Prod Parity – möglichst gleiche Umgebung |
|
||||
| 11 | Logs – als Event-Streams (`stdout`) |
|
||||
| 12 | Admin Processes – als One-Off |
|
||||
```sh
|
||||
# Neuer Feature-Branch
|
||||
git checkout -b feat/user-profile
|
||||
|
||||
# Arbeit, Commits
|
||||
git add .
|
||||
git commit -m "feat(profile): add avatar upload"
|
||||
|
||||
# Push + PR
|
||||
git push -u origin feat/user-profile
|
||||
# → PR via GitHub/Gitea-UI
|
||||
|
||||
# Nach Merge: aufräumen
|
||||
git checkout main
|
||||
git pull
|
||||
git branch -d feat/user-profile
|
||||
```
|
||||
|
||||
**Faustregel:** kurzlebige Branches (Tage, nicht Monate).
|
||||
|
||||
---
|
||||
|
||||
# Config via ENV
|
||||
<!-- _class: lead -->
|
||||
|
||||
# README
|
||||
|
||||
---
|
||||
|
||||
# README — das Visitenkarten-Dokument
|
||||
|
||||
Wenn jemand euer Projekt im Browser sieht: **die README ist die erste und oft einzige Berührung.**
|
||||
|
||||
Schlechte README = Repo nicht verwendet.
|
||||
|
||||
**Eine gute README beantwortet:**
|
||||
1. Was tut das Projekt?
|
||||
2. Wie installiere ich es?
|
||||
3. Wie starte ich es?
|
||||
4. Wie teste ich es?
|
||||
5. Wie deploye ich es?
|
||||
6. Lizenz?
|
||||
|
||||
---
|
||||
|
||||
# README-Template
|
||||
|
||||
```markdown
|
||||
# Mein Projekt
|
||||
|
||||
> Eine Zeile Beschreibung.
|
||||
|
||||
## Features
|
||||
|
||||
- ✓ etwas
|
||||
- ✓ etwas anderes
|
||||
|
||||
## Setup
|
||||
|
||||
```sh
|
||||
git clone https://...
|
||||
cd projekt
|
||||
cp .env.example .env
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```sh
|
||||
npm test
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
## Stack
|
||||
|
||||
Node 24, Express, SQLite, Vitest.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# SOLID — OO-Prinzipien
|
||||
|
||||
---
|
||||
|
||||
# SOLID — die fünf
|
||||
|
||||
| | Prinzip | Bedeutung |
|
||||
|--|---------|-----------|
|
||||
| **S** | Single Responsibility | jede Klasse/Modul = eine Verantwortung |
|
||||
| **O** | Open / Closed | offen für Erweiterung, geschlossen für Änderung |
|
||||
| **L** | Liskov Substitution | Subtyp ersetzbar durch Basistyp |
|
||||
| **I** | Interface Segregation | kleine, fokussierte Interfaces |
|
||||
| **D** | Dependency Inversion | abhängig von Abstraktionen |
|
||||
|
||||
Robert C. Martin („Uncle Bob"), 2000er.
|
||||
|
||||
---
|
||||
|
||||
# S — Single Responsibility
|
||||
|
||||
```javascript
|
||||
// schlecht
|
||||
const db = "postgres://user:secret@prod-db:5432/app";
|
||||
// Schlecht — alles in einer Klasse
|
||||
class UserManager {
|
||||
validate() { /* ... */ }
|
||||
saveToDb() { /* ... */ }
|
||||
sendEmail() { /* ... */ }
|
||||
generateReport() { /* ... */ }
|
||||
}
|
||||
|
||||
// gut
|
||||
const db = process.env.DATABASE_URL;
|
||||
// Besser — getrennte Verantwortlichkeiten
|
||||
class UserValidator { /* ... */ }
|
||||
class UserRepository { /* ... */ }
|
||||
class EmailService { /* ... */ }
|
||||
class ReportGenerator { /* ... */ }
|
||||
```
|
||||
|
||||
```bash
|
||||
# .env (NICHT committen!)
|
||||
DATABASE_URL=postgres://user:secret@db:5432/app
|
||||
NODE_ENV=production
|
||||
PORT=8080
|
||||
---
|
||||
|
||||
# D — Dependency Inversion
|
||||
|
||||
```javascript
|
||||
// Schlecht — direkt von konkreter Klasse
|
||||
class OrderService {
|
||||
charge(order) {
|
||||
const stripe = new StripeClient();
|
||||
stripe.charge(order.amount);
|
||||
}
|
||||
}
|
||||
|
||||
// Besser — Abhängigkeit injizieren
|
||||
class OrderService {
|
||||
constructor(paymentProvider) {
|
||||
this.payment = paymentProvider;
|
||||
}
|
||||
charge(order) {
|
||||
this.payment.charge(order.amount);
|
||||
}
|
||||
}
|
||||
|
||||
new OrderService(new StripeClient()); // im Production
|
||||
new OrderService(new MockClient()); // im Test
|
||||
```
|
||||
|
||||
`.env.example` mit Dummy-Werten committen.
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# 12-Factor App
|
||||
|
||||
---
|
||||
|
||||
# Make It Work · Right · Fast
|
||||
# 12-Factor — die wichtigsten
|
||||
|
||||
```
|
||||
1. Make It Work → MVP, hässlich aber funktional
|
||||
2. Make It Right → Refactor, Tests, Clean Code
|
||||
3. Make It Fast → Profiling, Optimierung
|
||||
| # | Faktor | Was |
|
||||
|---|--------|-----|
|
||||
| 1 | Codebase | ein Repo pro App, mehrere Deploys |
|
||||
| 2 | Dependencies | explizit deklariert (package.json) |
|
||||
| 3 | **Config** | über Environment Variables, nicht im Code |
|
||||
| 4 | Backing Services | als Resources behandeln (z.B. DB-URL) |
|
||||
| 6 | **Processes** | stateless |
|
||||
| 9 | Disposability | schnell startbar + stoppbar |
|
||||
| 12 | Admin Processes | als One-Off im selben Stack |
|
||||
|
||||
Heroku, 2011. Heute Industrie-Standard für Cloud-Apps.
|
||||
|
||||
---
|
||||
|
||||
# Factor 3 — Config in env
|
||||
|
||||
```javascript
|
||||
// SCHLECHT — Hardcoded
|
||||
const db = new Pool({ host: 'localhost', user: 'dev' });
|
||||
|
||||
// GUT — aus env
|
||||
const db = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
```
|
||||
|
||||
Don't optimize what doesn't work.
|
||||
Don't refactor what isn't working yet.
|
||||
|
||||
→ https://wiki.c2.com/?MakeItWorkMakeItRightMakeItFast
|
||||
`.env` lokal, **Environment-Variablen** auf Server. Gleicher Code überall.
|
||||
|
||||
---
|
||||
|
||||
# KISS · DRY · YAGNI · SOLID
|
||||
# Factor 6 — Stateless Processes
|
||||
|
||||
- **KISS** – Keep It Simple, Stupid
|
||||
- **DRY** – Don't Repeat Yourself
|
||||
- **YAGNI** – You Aren't Gonna Need It
|
||||
- **SOLID**:
|
||||
- **S**ingle Responsibility
|
||||
- **O**pen/Closed
|
||||
- **L**iskov Substitution
|
||||
- **I**nterface Segregation
|
||||
- **D**ependency Inversion
|
||||
**Schlecht:** Session-State im RAM.
|
||||
```javascript
|
||||
const sessions = new Map(); // pro Server-Instanz!
|
||||
```
|
||||
→ Load-Balancer mit 2 Servern: User-Session geht verloren beim 2. Request.
|
||||
|
||||
→ Faustregeln, keine Dogmen.
|
||||
**Gut:** State in DB / Redis.
|
||||
```javascript
|
||||
await redis.set(`session:${id}`, JSON.stringify(data));
|
||||
```
|
||||
|
||||
Server-Restart → kein Datenverlust. Horizontale Skalierung möglich.
|
||||
|
||||
---
|
||||
|
||||
# Präsentation – Aufbau
|
||||
<!-- _class: lead -->
|
||||
|
||||
- **Einstieg** mit aktuellem Bezug (optional)
|
||||
- **Problemdefinition**: welches Bedürfnis / welches Problem?
|
||||
- Was hat **blockiert**, wie gelöst?
|
||||
- **Geschichte mit Spannungsbogen**
|
||||
- Vorher klären: Fragen während oder nach der Präsentation?
|
||||
# SemVer — semantisches Versioning
|
||||
|
||||
---
|
||||
|
||||
# Präsentation – Pitfalls
|
||||
# SemVer — Format
|
||||
|
||||
- Am Thema vorbeireden
|
||||
- Folien vorlesen
|
||||
- Live-Demo ohne Backup-Video
|
||||
- "Sieht man hier nicht so gut, aber..."
|
||||
- Zu viel Text pro Folie
|
||||
- Keine klare Botschaft pro Slide
|
||||
```
|
||||
MAJOR.MINOR.PATCH
|
||||
1 . 4 . 3
|
||||
```
|
||||
|
||||
→ **1 Folie = 1 Aussage.**
|
||||
| Teil | Bump bei |
|
||||
|------|----------|
|
||||
| **MAJOR** | breaking change (API-incompatible) |
|
||||
| **MINOR** | neues Feature (rückwärts-kompatibel) |
|
||||
| **PATCH** | Bugfix (rückwärts-kompatibel) |
|
||||
|
||||
**Pre-Release:** `1.0.0-beta.1`, `1.0.0-rc.2`.
|
||||
|
||||
---
|
||||
|
||||
# Feedback – Sandwich-Methode
|
||||
# SemVer in package.json
|
||||
|
||||
1. **Eröffnung** — Lob / positive Beobachtungen
|
||||
2. **Kern** — Kritik + konkreter Verbesserungsvorschlag
|
||||
3. **Abschluss** — Lob / wertschätzender Ausklang
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"express": "^5.0.0", // ≥ 5.0.0, < 6.0.0
|
||||
"lodash": "~4.17.0", // ≥ 4.17.0, < 4.18.0
|
||||
"react": "5.2.1", // exakt
|
||||
"vite": "*" // jede (vermeiden)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pitfalls:**
|
||||
- Am Thema vorbeireden
|
||||
- Bei Unklarheiten keine Fragen stellen
|
||||
- Subjektives Feedback ("Farbe gefällt mir nicht")
|
||||
|
||||
→ **Konkret, sachlich, umsetzbar.**
|
||||
**Faustregel:** `^` (Caret) ist Default — major-version-lock.
|
||||
|
||||
---
|
||||
|
||||
# Prüfungsleistung – Recap
|
||||
<!-- _class: lead -->
|
||||
|
||||
**Grundanforderungen (75 Punkte):**
|
||||
|
||||
| Punkte | Kriterium |
|
||||
|--------|-----------|
|
||||
| 20 | Idee, Konzeption, Planung |
|
||||
| 5 | Plattformunabhängigkeit |
|
||||
| 25 | Clean Code (KISS, SOLID, DI, Testing, Error Handling) |
|
||||
| 15 | Präsentation |
|
||||
| 10 | Dokumentation (README, etc.) |
|
||||
|
||||
**Zusatzpunkte (max. 10):** TypeScript, Docker, Dev/Prod-Parity, `.env`, npm-Publish, Domain, HTTPS, Responsive Design.
|
||||
|
||||
→ https://librete.ch/DHBW/pruefungsleistung
|
||||
# Code-Reviews
|
||||
|
||||
---
|
||||
|
||||
# Abgabe + Termine
|
||||
# Constructive Code-Reviews
|
||||
|
||||
- **Code-Upload:** bis 27.07.
|
||||
- **Präsentation:** 17.07. (in Gruppen, ~10 Min)
|
||||
- Pitch (~1 Min)
|
||||
- Agenda
|
||||
- Idee / Problem
|
||||
- Learnings & Pitfalls
|
||||
- "It is what it is" – Doku
|
||||
**Geben:**
|
||||
- Konkret: Zeilennummern + Beispiel
|
||||
- Wertschätzend: gut gemachte Stellen erwähnen
|
||||
- Fragend statt fordernd: „Wäre `X` hier robuster?"
|
||||
- Klar zwischen MUST/SHOULD/CONSIDER unterscheiden
|
||||
|
||||
**Nehmen:**
|
||||
- Egoless: Code ≠ Person
|
||||
- Frage zurück bei Unklarheit
|
||||
- Bedanken für detaillierte Feedback
|
||||
|
||||
---
|
||||
|
||||
# 70 Things I Regret as a Developer (Auswahl)
|
||||
# Code-Review-Checkliste
|
||||
|
||||
- Nicht früher Tests geschrieben zu haben
|
||||
- Zu viel Zeit mit "perfektem" Setup statt Code
|
||||
- Nicht öfter `git commit`
|
||||
- Nicht früher in Pull Requests reviewt
|
||||
- Keine Doku gepflegt
|
||||
- Eigene Lösungen für Standard-Probleme
|
||||
- Frameworks gehyped, Grundlagen vernachlässigt
|
||||
- "Funktioniert bei mir" akzeptiert
|
||||
|
||||
→ Lehre: **Disziplin > Talent.**
|
||||
| Bereich | Was prüfen |
|
||||
|---------|------------|
|
||||
| **Correctness** | macht's, was es soll? Edge-cases? |
|
||||
| **Readability** | Namen, Struktur, Kommentare? |
|
||||
| **Tests** | abgedeckt? Sinnvolle Assertions? |
|
||||
| **Security** | Input-Validation, Secrets, Auth |
|
||||
| **Performance** | N+1, Sync-IO, Memory-Leak |
|
||||
| **A11y** (Frontend) | Semantik, Kontrast, Keyboard |
|
||||
| **Doku** | README aktualisiert? CHANGELOG? |
|
||||
|
||||
---
|
||||
|
||||
# Weiterlesen
|
||||
<!-- _class: lead -->
|
||||
|
||||
- https://12factor.net/
|
||||
- https://semver.org/
|
||||
- https://udacity.github.io/git-styleguide/
|
||||
- https://www.conventionalcommits.org/
|
||||
- https://refactoring.guru/
|
||||
- https://martinfowler.com/
|
||||
- MDN Web Docs · web.dev · web.dev/learn
|
||||
# Doku lesen lernen
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
# Wo finde ich was?
|
||||
|
||||
# Viel Erfolg mit den Projekten!
|
||||
| Ziel | Quelle |
|
||||
|------|--------|
|
||||
| Web-APIs (HTML, CSS, JS) | **MDN** — developer.mozilla.org |
|
||||
| Node-APIs | **nodejs.org/docs** |
|
||||
| npm-Package | `npmjs.com/package/<name>` + GitHub README |
|
||||
| Sprach-Standards (ES) | **tc39.es** |
|
||||
| Browser-Kompatibilität | **caniuse.com** |
|
||||
| Fehler-Suche | Stack Overflow + GitHub Issues |
|
||||
|
||||
Fragen? Probleme? → Telegram, Mail, Sprechstunde.
|
||||
**Faustregel:** offizielle Doku zuerst, dann Stack Overflow.
|
||||
|
||||
---
|
||||
|
||||
# Stack Overflow gut nutzen
|
||||
|
||||
**Bevor du fragst:**
|
||||
- Existiert die Frage schon? (Suche oben)
|
||||
- Gibt es ein Minimal-Reproduction-Beispiel?
|
||||
- Welche Versionen (Node, OS, Library)?
|
||||
- Was hast du schon versucht?
|
||||
|
||||
**Bei der Frage:**
|
||||
- Konkreter Titel (nicht "JavaScript Hilfe")
|
||||
- Code-Block für Code
|
||||
- Was war erwartet, was passiert
|
||||
- Error-Message als Text (nicht Screenshot)
|
||||
|
||||
---
|
||||
|
||||
# AI-Assistenten (ChatGPT, Claude, Copilot)
|
||||
|
||||
**Pro:**
|
||||
- Schneller Antworten zu allgemeinen Fragen
|
||||
- Boilerplate-Code in Sekunden
|
||||
- Erklärung fremden Codes
|
||||
|
||||
**Contra:**
|
||||
- Halluziniert APIs, die nicht existieren
|
||||
- Verstärkt Anti-Patterns aus dem Training
|
||||
- Lernkurve geht zurück, wenn nur kopiert wird
|
||||
|
||||
**Faustregel:** als Sparring-Partner, nicht als Wahrheitsquelle. **Verstehen, nicht kopieren.**
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||
# Selbstlernen — Projekt-Hygiene-Check
|
||||
|
||||
Geht euer Projekt durch:
|
||||
|
||||
1. **README:** hat alle 6 Sektionen?
|
||||
2. **.gitignore:** komplett, keine Secrets im Repo?
|
||||
3. **Conventional Commits:** letzte 10 Commits durchgehen
|
||||
4. **package.json:** `version`, `description`, `scripts`?
|
||||
5. **Tests:** Coverage > 70 %?
|
||||
6. **Dockerfile + compose.yml:** funktionieren?
|
||||
7. **Environment:** `.env.example` vorhanden, `.env` gitignored?
|
||||
|
||||
**Bonus:** GitHub-Actions-Workflow für Tests + Build.
|
||||
|
||||
---
|
||||
|
||||
# Abschluss
|
||||
|
||||
**Über 8 Termine gelernt:**
|
||||
1. Web Engineering — Foundation
|
||||
2. CSS Extended — Layout + Animation
|
||||
3. Node.js Basics — Runtime + REST
|
||||
4. Node.js Advanced — async + DB + Auth
|
||||
5. Testing — Unit + Integration + E2E
|
||||
6. TypeScript — Typen + Schemas
|
||||
7. Docker — Container + compose
|
||||
8. Best Practices — Git + README + SOLID + 12-Factor
|
||||
|
||||
**Nächste Schritte:** eigenes Projekt für die Prüfungsleistung. Code-Abgabe + Präsentation.
|
||||
|
||||
Reference in New Issue
Block a user