dhbw: import original web-eng inhalte (testing, typescript, docker, best practices) + framework-übersicht, native html, prüfungsleistung-detail, sose 2026
This commit is contained in:
@@ -4,7 +4,7 @@ theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2025"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
title: Web Engineering
|
||||
---
|
||||
|
||||
@@ -98,11 +98,52 @@ a {
|
||||
|
||||
---
|
||||
|
||||
# Prüfungsleistung
|
||||
# Prüfungsleistung – Übersicht
|
||||
|
||||
**LN** – Lernnachweis (50%)
|
||||
- Projekte / Hausaufgaben
|
||||
- Präsentation
|
||||
Eigenes Projekt: **Web-App** ODER **Backend** (API/CLI).
|
||||
|
||||
**Grundanforderungen (75 Punkte)**
|
||||
|
||||
| Punkte | Bereich |
|
||||
|--------|---------|
|
||||
| 20 | Idee, Konzeption, Planung |
|
||||
| 5 | Plattformunabhängigkeit |
|
||||
| 25 | Clean Code (KISS, SOLID, DI, Testing, Error Handling) |
|
||||
| 15 | Präsentation |
|
||||
| 10 | Dokumentation |
|
||||
|
||||
**Zusatzpunkte (max. 10):** TypeScript, Docker, Dev/Prod-Parity, `.env`, npm-Publish, Domain, HTTPS, Responsive Design.
|
||||
|
||||
---
|
||||
|
||||
# Prüfungsleistung – Idee & Konzeption (20 P.)
|
||||
|
||||
- "Powerpoint"-Präsentation + Folien
|
||||
- Aussagekräftiger Arbeitstitel + Beschreibung
|
||||
- **Elevator Pitch** (max. 1 Min.)
|
||||
- Repo-Link (GitHub / GitLab / Codeberg / BitBucket)
|
||||
- **Logs**: wie hat sich Projekt verändert vs. Ursprungsidee?
|
||||
- Schematischer Projektaufbau (UML o. Ä.)
|
||||
|
||||
---
|
||||
|
||||
# Prüfungsleistung – Clean Code (25 P.)
|
||||
|
||||
- `README.md` (clone, start, contribute)
|
||||
- **KISS** – Keep It Simple, Stupid
|
||||
- **SOLID** – SRP, OCP, LSP, ISP, DIP
|
||||
- **DI** – Dependency Injection
|
||||
- **Testing-Pyramide** (Unit, Integration, E2E)
|
||||
- **Exception/Error Handling**
|
||||
|
||||
---
|
||||
|
||||
# Prüfungsleistung – Abgabe & Termine
|
||||
|
||||
- **Code-Upload:** bis 27.07.
|
||||
- **Präsentation:** 17.07. (Gruppen, ~10 Min.)
|
||||
|
||||
Details: https://git.dailysh.it/DHBW/pruefungsleistung
|
||||
|
||||
---
|
||||
|
||||
@@ -411,6 +452,205 @@ async function loadProducts() {
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
# JavaScript Frameworks – Kategorien
|
||||
|
||||
| Kategorie | Beispiele |
|
||||
|-----------|-----------|
|
||||
| **CSS Frameworks** | Tailwind, Bootstrap, shadcn/ui |
|
||||
| **Frontend Frameworks** | React, Vue, Svelte, Astro |
|
||||
| **Rendering / Meta** | Next.js, Nuxt, Gatsby |
|
||||
| **Build Tools / Bundler** | Webpack, Vite, Parcel, esbuild |
|
||||
| **Backend Frameworks** | Express, Fastify, NestJS |
|
||||
|
||||
---
|
||||
|
||||
# Vanilla JS – Counter
|
||||
|
||||
```html
|
||||
<button id="myButton">Clicked 0 times</button>
|
||||
<script>
|
||||
let count = 0;
|
||||
const btn = document.getElementById('myButton');
|
||||
btn.addEventListener('click', () => {
|
||||
count++;
|
||||
btn.textContent = `Clicked ${count} times`;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
✓ hohe Kompatibilität, simpel
|
||||
✗ State-Management, Re-Rendering wird schnell komplex
|
||||
|
||||
---
|
||||
|
||||
# React – Counter (JSX)
|
||||
|
||||
```jsx
|
||||
import { useState } from "react";
|
||||
|
||||
function MyButton() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return (
|
||||
<button onClick={() => setCount(count + 1)}>
|
||||
Clicked {count} times
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- **Component-Based** Architecture
|
||||
- **Virtual DOM**, JSX, Hooks
|
||||
- großes Ökosystem (React Router, Redux, …)
|
||||
|
||||
---
|
||||
|
||||
# Vue 3 – Composition API
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
const count = ref(0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button @click="count++">
|
||||
Clicked {{ count }} times
|
||||
</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
- **Reactive Data Binding**
|
||||
- Single-File Components (`.vue`)
|
||||
- Vue Router, Pinia (State)
|
||||
|
||||
---
|
||||
|
||||
# Svelte – Counter
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = 0;
|
||||
</script>
|
||||
|
||||
<button on:click={() => count += 1}>
|
||||
Clicked {count} {count === 1 ? 'time' : 'times'}
|
||||
</button>
|
||||
```
|
||||
|
||||
- **Compiler-Based** – kein Runtime-Framework
|
||||
- Reactive Assignments (`let`)
|
||||
- Built-in Animationen
|
||||
|
||||
---
|
||||
|
||||
# Astro – Content-First
|
||||
|
||||
```astro
|
||||
---
|
||||
const greeting = "Hallo DHBW";
|
||||
---
|
||||
<html>
|
||||
<body>
|
||||
<h1>{greeting}</h1>
|
||||
<MyReactButton client:load />
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
- **Islands Architecture** – Komponenten mehrerer Frameworks mischen
|
||||
- Default: kein JS im Output (nur statisches HTML)
|
||||
- Hydration on demand (`client:load`, `client:visible`)
|
||||
|
||||
---
|
||||
|
||||
# Rendering Frameworks
|
||||
|
||||
| | Frontend | SSR | SSG | File Routing |
|
||||
|---|---|---|---|---|
|
||||
| **Next.js** | React | ✓ | ✓ | `/app/page.js` |
|
||||
| **Nuxt** | Vue | ✓ | ✓ | `/pages/*.vue` |
|
||||
| **Gatsby** | React | – | ✓ | GraphQL Data |
|
||||
|
||||
---
|
||||
|
||||
# Build Tools – Webpack
|
||||
|
||||
- Module Bundling (JS, CSS, Images)
|
||||
- Module Loaders + großes Plugin-Ökosystem
|
||||
- Code Splitting, HMR
|
||||
- Konfigurations-lastig (`webpack.config.js`)
|
||||
|
||||
→ Standard für viele Legacy-Setups.
|
||||
|
||||
---
|
||||
|
||||
# Build Tools – Vite
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:4567",
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
- Rollup unter der Haube, **HMR mit ES Modules**
|
||||
- Tree-Shaking, Code Splitting, React + Vue out-of-the-box
|
||||
|
||||
---
|
||||
|
||||
# Build Tools – Parcel
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-project",
|
||||
"source": "src/index.html",
|
||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||
"scripts": {
|
||||
"start": "parcel",
|
||||
"build": "parcel build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"parcel": "latest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Zero-Config**
|
||||
- Auto-Resolution, HMR
|
||||
- TS / CSS-Preprocessors out-of-the-box
|
||||
|
||||
---
|
||||
|
||||
# Microfrontends
|
||||
|
||||
- Mehrere unabhängige Frontend-Apps in einer Page
|
||||
- Module Federation (Webpack 5)
|
||||
- **single-spa**: https://single-spa.js.org/
|
||||
- Anwendungsfall: große Teams, unterschiedliche Tech-Stacks pro Domain
|
||||
|
||||
→ Komplexitätskosten: nur bei echtem Skalierungsbedarf.
|
||||
|
||||
---
|
||||
|
||||
# Demo-Repos
|
||||
|
||||
- https://github.com/nextlevelshit/dhbw-client-js (Counter, multi-framework)
|
||||
- https://github.com/nextlevelshit/node-cache-api (API + Tests)
|
||||
- https://github.com/nextlevelshit/dhbw-docker (Compose-Stack)
|
||||
|
||||
---
|
||||
|
||||
# Fragen?
|
||||
|
||||
@@ -4,7 +4,7 @@ theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2025"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
---
|
||||
|
||||
<style>
|
||||
@@ -259,6 +259,184 @@ transition: background 0.2s, transform 0.1s;
|
||||
|
||||
---
|
||||
|
||||
# CSS Specificity
|
||||
|
||||
Welcher Selektor gewinnt? **Punktezählung:**
|
||||
|
||||
```
|
||||
ID – 0,1,0,0
|
||||
Class / Attr / Pseudo-Class – 0,0,1,0
|
||||
Tag / Pseudo-Element – 0,0,0,1
|
||||
Inline Style – 1,0,0,0
|
||||
!important – setzt alles außer Kraft
|
||||
```
|
||||
|
||||
```css
|
||||
#main .btn:hover /* 0,1,2,0 */
|
||||
.btn.primary /* 0,0,2,0 */
|
||||
button[type="submit"] /* 0,0,1,1 */
|
||||
```
|
||||
|
||||
→ Faustregel: möglichst flach. `!important` vermeiden.
|
||||
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
```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
|
||||
</button>
|
||||
```
|
||||
|
||||
- Keine eigenen CSS-Klassen mehr
|
||||
- JIT-Compiler erzeugt nur genutzte Klassen
|
||||
- Design-System via `tailwind.config.js`
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _backgroundColor: #001520 -->
|
||||
|
||||
# No JavaScript, No Cry
|
||||
|
||||
## Native HTML statt JS-Komponenten
|
||||
|
||||
---
|
||||
|
||||
# Native HTML – Was früher JS brauchte
|
||||
|
||||
| Komponente | Native Alternative |
|
||||
|------------|--------------------|
|
||||
| Accordion / Collapsible | `<details>` + `<summary>` |
|
||||
| Modal / Dialog | `<dialog>` |
|
||||
| Date Picker | `<input type="date">` |
|
||||
| Time Picker | `<input type="time">` |
|
||||
| Color Picker | `<input type="color">` |
|
||||
| Range Slider | `<input type="range">` |
|
||||
| Progress Bar | `<progress>` |
|
||||
| Auto-Complete | `<datalist>` |
|
||||
|
||||
→ Baseline Widely Available (Chrome, Firefox, Safari, Edge).
|
||||
|
||||
---
|
||||
|
||||
# `<details>` + `<summary>`
|
||||
|
||||
```html
|
||||
<details>
|
||||
<summary>Mehr Infos</summary>
|
||||
<p>Hier steht der ausklappbare Inhalt.</p>
|
||||
</details>
|
||||
```
|
||||
|
||||
- Keine Zeile JS
|
||||
- Tastatur-zugänglich
|
||||
- Animierbar mit `::details-content` (CSS only)
|
||||
|
||||
---
|
||||
|
||||
# `<dialog>` – Native Modal
|
||||
|
||||
```html
|
||||
<dialog id="myDialog">
|
||||
<form method="dialog">
|
||||
<p>Wirklich löschen?</p>
|
||||
<button value="cancel">Abbrechen</button>
|
||||
<button value="ok">OK</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<button onclick="myDialog.showModal()">Öffnen</button>
|
||||
```
|
||||
|
||||
- `showModal()` blockt Hintergrund + ESC schließt
|
||||
- Backdrop via `dialog::backdrop`
|
||||
- Form mit `method="dialog"` schließt + liefert `returnValue`
|
||||
|
||||
---
|
||||
|
||||
# `<input type="date">` & Co.
|
||||
|
||||
```html
|
||||
<input type="date" min="2026-01-01" max="2026-12-31">
|
||||
<input type="time" step="60">
|
||||
<input type="color" value="#005f8a">
|
||||
<input type="range" min="0" max="100" step="5" value="50">
|
||||
<input type="number" min="1" max="300">
|
||||
```
|
||||
|
||||
- Plattform-native UI (Touch-Datepicker auf Mobile)
|
||||
- Validierung "for free"
|
||||
- Locale-Awareness vom Browser
|
||||
|
||||
---
|
||||
|
||||
# `<progress>` – Native Progress Bar
|
||||
|
||||
```html
|
||||
<progress value="70" max="100">70%</progress>
|
||||
|
||||
<!-- indeterminate -->
|
||||
<progress></progress>
|
||||
```
|
||||
|
||||
```css
|
||||
progress {
|
||||
width: 100%;
|
||||
height: 0.5rem;
|
||||
}
|
||||
progress::-webkit-progress-bar { background: #eee; }
|
||||
progress::-webkit-progress-value { background: #005f8a; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Form Validation – Native
|
||||
|
||||
```html
|
||||
<form>
|
||||
<input type="email" required
|
||||
pattern=".+@dhbw\.de"
|
||||
title="Bitte DHBW-Mail eingeben">
|
||||
<input type="password" minlength="8" required>
|
||||
<button>Login</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
- `required`, `min`, `max`, `pattern`, `step`
|
||||
- `:invalid` / `:valid` CSS-Pseudoklassen
|
||||
- `form.checkValidity()` / `input.setCustomValidity()`
|
||||
|
||||
---
|
||||
|
||||
# Wann doch JS?
|
||||
|
||||
- Komplexe Interaktion über mehrere Komponenten
|
||||
- State, der nicht ins DOM gehört
|
||||
- Async / Server-Sync
|
||||
- Animationen mit komplexer Logik
|
||||
- Cross-Browser-Polyfills (selten heute)
|
||||
|
||||
→ **Default: HTML/CSS. JS nur wenn nötig.**
|
||||
|
||||
---
|
||||
|
||||
# Fragen?
|
||||
|
||||
1. Was bedeutet `div > p` vs `div p`?
|
||||
|
||||
@@ -4,7 +4,7 @@ theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2025"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
---
|
||||
|
||||
<style>
|
||||
|
||||
@@ -4,7 +4,7 @@ theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2025"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
---
|
||||
|
||||
<style>
|
||||
|
||||
356
slides/dhbw/05_testing.md
Normal file
356
slides/dhbw/05_testing.md
Normal file
@@ -0,0 +1,356 @@
|
||||
---
|
||||
marp: true
|
||||
theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
title: Testing
|
||||
---
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--color-foreground: #1a1a2e;
|
||||
--color-highlight: #005f8a;
|
||||
--color-dimmed: #4a6a7a;
|
||||
}
|
||||
section.invert {
|
||||
--color-foreground: #fff;
|
||||
}
|
||||
section {
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
h1 {
|
||||
color: #005f8a;
|
||||
}
|
||||
section.invert h1 {
|
||||
color: #fff;
|
||||
}
|
||||
pre {
|
||||
background: #0f1e2e;
|
||||
color: #00b4d8;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #005f8a;
|
||||
}
|
||||
pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
code {
|
||||
background: #1a3a4a;
|
||||
color: #00b4d8;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
a {
|
||||
color: var(--color-highlight);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _header: '' -->
|
||||
<!-- _backgroundColor: #001520 -->
|
||||
|
||||
# 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
|
||||
|
||||
```
|
||||
▲ manual / exploratory
|
||||
▲ ▲ E2E (UI + API)
|
||||
▲▲▲▲ Integration (class + real deps)
|
||||
▲▲▲▲▲▲ Unit (class + mocked deps)
|
||||
▲▲▲▲▲▲▲▲ Static analysis (compiler, linter)
|
||||
```
|
||||
|
||||
- Oben: wenige, langsam, teuer, brüchig
|
||||
- Unten: viele, schnell, billig, stabil
|
||||
|
||||
---
|
||||
|
||||
# Test Automation V-Modell
|
||||
|
||||
```
|
||||
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";
|
||||
|
||||
describe("Cache", () => {
|
||||
it("creates and retrieves data with custom key", () => {
|
||||
const cache = new Cache();
|
||||
const data = { user: "wolfgang", city: "stuttgart" };
|
||||
|
||||
const key = cache.create(data, "custom-key");
|
||||
|
||||
expect(key).toBe("custom-key");
|
||||
expect(cache.get("custom-key")).toEqual(data);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 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)
|
||||
|
||||
```javascript
|
||||
// app.integration.test.js
|
||||
import request from "supertest";
|
||||
import { app, cache } from "./app.js";
|
||||
|
||||
test("full CRUD lifecycle with real cache", async () => {
|
||||
const res = await request(app)
|
||||
.post("/api/entries")
|
||||
.send({ name: "integration-test", status: "active" });
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
const { key } = res.body;
|
||||
|
||||
expect(cache.keys()).toContain(key);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
---
|
||||
|
||||
# E2E API Test – Beispiel
|
||||
|
||||
```javascript
|
||||
// index.e2e.test.js
|
||||
import { createServer } from "node:http";
|
||||
import { app } from "./app.js";
|
||||
|
||||
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() }),
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# End-to-End Tests (Client)
|
||||
|
||||
- **Echter Browser** – Chromium / Firefox / WebKit
|
||||
- **Echtes DOM** – Klicks, Tippen, Scrollen
|
||||
- **Echtes Rendering** – CSS, async Operationen
|
||||
- **Cross-Origin** – Client ↔ Server-Kommunikation
|
||||
|
||||
Tools: **Cypress**, **Playwright**
|
||||
|
||||
---
|
||||
|
||||
# Cypress – Minimalbeispiel
|
||||
|
||||
```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");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```bash
|
||||
npx cypress open # GUI
|
||||
npx cypress run # headless / CI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Playwright – Minimalbeispiel
|
||||
|
||||
```javascript
|
||||
// tests/counter.spec.js
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
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");
|
||||
});
|
||||
```
|
||||
|
||||
```bash
|
||||
npx playwright test
|
||||
npx playwright test --ui
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Coverage Reports
|
||||
|
||||
```bash
|
||||
npx vitest run --coverage
|
||||
```
|
||||
|
||||
```
|
||||
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`
|
||||
|
||||
---
|
||||
|
||||
# Best Practices
|
||||
|
||||
- **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
|
||||
|
||||
---
|
||||
|
||||
# Live Coding
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _backgroundColor: #001520 -->
|
||||
|
||||
# Fragen?
|
||||
|
||||
**Hausaufgabe:** Eigene Projektidee mit mind. 1 Unit + 1 Integration Test starten.
|
||||
422
slides/dhbw/06_typescript.md
Normal file
422
slides/dhbw/06_typescript.md
Normal file
@@ -0,0 +1,422 @@
|
||||
---
|
||||
marp: true
|
||||
theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
title: TypeScript
|
||||
---
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--color-foreground: #1a1a2e;
|
||||
--color-highlight: #005f8a;
|
||||
--color-dimmed: #4a6a7a;
|
||||
}
|
||||
section.invert {
|
||||
--color-foreground: #fff;
|
||||
}
|
||||
section {
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
h1 {
|
||||
color: #005f8a;
|
||||
}
|
||||
section.invert h1 {
|
||||
color: #fff;
|
||||
}
|
||||
pre {
|
||||
background: #0f1e2e;
|
||||
color: #00b4d8;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #005f8a;
|
||||
}
|
||||
pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
code {
|
||||
background: #1a3a4a;
|
||||
color: #00b4d8;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
a {
|
||||
color: var(--color-highlight);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _header: '' -->
|
||||
<!-- _backgroundColor: #001520 -->
|
||||
|
||||
# TypeScript
|
||||
|
||||
## JavaScript on Steroids
|
||||
|
||||
---
|
||||
|
||||
# Inhalt
|
||||
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
# TypeScript – Was?
|
||||
|
||||
- **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
|
||||
```
|
||||
|
||||
**Run:**
|
||||
```bash
|
||||
npx ts-node index.ts # statt: node index.js
|
||||
```
|
||||
|
||||
**Build:**
|
||||
```bash
|
||||
npx tsc # kompiliert nach ./dist
|
||||
node dist/index.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# tsconfig.json
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Type Primitives
|
||||
|
||||
```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;
|
||||
|
||||
function log(msg: string): void {
|
||||
console.log(msg);
|
||||
}
|
||||
```
|
||||
|
||||
`any`, `unknown`, `never`, `void` sind weitere TS-Spezialtypen.
|
||||
|
||||
---
|
||||
|
||||
# Union Types
|
||||
|
||||
```typescript
|
||||
let id: number | string;
|
||||
|
||||
id = 101; // ok
|
||||
id = "101"; // ok
|
||||
id = true; // ✗ TypeError
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 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 User {
|
||||
id: number;
|
||||
name: string;
|
||||
email?: string; // optional
|
||||
readonly createdAt: Date; // schreibgeschützt
|
||||
greet(msg: string): void;
|
||||
}
|
||||
|
||||
const u: User = {
|
||||
id: 1,
|
||||
name: "Lisa",
|
||||
createdAt: new Date(),
|
||||
greet(msg) { console.log(msg); }
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Interface vs Type
|
||||
|
||||
**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
|
||||
enum Direction {
|
||||
Up = 1,
|
||||
Down, // 2
|
||||
Left, // 3
|
||||
Right // 4
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
walk(Direction.Up); // "bow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Generics
|
||||
|
||||
```typescript
|
||||
class GenericContainer<T> {
|
||||
private readonly value: T;
|
||||
|
||||
constructor(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _backgroundColor: #001520 -->
|
||||
|
||||
# Decorators
|
||||
|
||||
## Meta-Programmierung mit TypeScript
|
||||
|
||||
---
|
||||
|
||||
# TypeScript: Decorators
|
||||
|
||||
Vier Typen:
|
||||
|
||||
- **Class Decorators** – auf ganze Klasse
|
||||
- **Property Decorators** – auf Felder
|
||||
- **Method Decorators** – auf Methoden
|
||||
- **Parameter Decorators** – auf Methoden-Parameter
|
||||
|
||||
Genutzt von: Angular, NestJS, TypeORM, class-validator.
|
||||
|
||||
---
|
||||
|
||||
# Decorators aktivieren
|
||||
|
||||
**`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.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _backgroundColor: #001520 -->
|
||||
|
||||
# Fragen?
|
||||
|
||||
**Hausaufgabe:** Bestehendes JS-Projekt auf TS migrieren – mind. 1 File + `tsconfig.json`.
|
||||
338
slides/dhbw/07_docker.md
Normal file
338
slides/dhbw/07_docker.md
Normal file
@@ -0,0 +1,338 @@
|
||||
---
|
||||
marp: true
|
||||
theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
title: Docker und Service Orchestration
|
||||
---
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--color-foreground: #1a1a2e;
|
||||
--color-highlight: #005f8a;
|
||||
--color-dimmed: #4a6a7a;
|
||||
}
|
||||
section.invert {
|
||||
--color-foreground: #fff;
|
||||
}
|
||||
section {
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
h1 {
|
||||
color: #005f8a;
|
||||
}
|
||||
section.invert h1 {
|
||||
color: #fff;
|
||||
}
|
||||
pre {
|
||||
background: #0f1e2e;
|
||||
color: #00b4d8;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #005f8a;
|
||||
}
|
||||
pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
code {
|
||||
background: #1a3a4a;
|
||||
color: #00b4d8;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
a {
|
||||
color: var(--color-highlight);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _header: '' -->
|
||||
<!-- _backgroundColor: #001520 -->
|
||||
|
||||
# Docker
|
||||
|
||||
## Service Orchestration
|
||||
|
||||
---
|
||||
|
||||
# Inhalt
|
||||
|
||||
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`
|
||||
|
||||
---
|
||||
|
||||
# Architektur
|
||||
|
||||
```
|
||||
Browser (localhost:10007)
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ Reverse Proxy │ (nginx / traefik)
|
||||
└─────────┬────────┘
|
||||
┌────┴────┐
|
||||
▼ ▼
|
||||
┌─────────┐ ┌────────┐
|
||||
│ Web │ │ API │
|
||||
│ App │ │ (Node) │
|
||||
└────┬────┘ └────┬───┘
|
||||
└─────┬──────┘
|
||||
▼
|
||||
┌────────┐
|
||||
│ DB │ (Postgres)
|
||||
└────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 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
|
||||
FROM node:lts-slim AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./package*.json ./
|
||||
RUN npm ci --no-audit --no-fund --no-progress --loglevel=error
|
||||
|
||||
COPY ./ ./
|
||||
|
||||
ENV NODE_ENV="production"
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["npm", "start"]
|
||||
```
|
||||
|
||||
**`npm ci`** statt `npm i` für reproduzierbare Builds.
|
||||
|
||||
---
|
||||
|
||||
# Multi-Stage Build
|
||||
|
||||
```dockerfile
|
||||
FROM node:lts-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:lts-slim AS runtime
|
||||
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"]
|
||||
```
|
||||
|
||||
→ kleines Final-Image, keine Build-Tools im Runtime.
|
||||
|
||||
---
|
||||
|
||||
# .dockerignore
|
||||
|
||||
```
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.env
|
||||
*.log
|
||||
coverage
|
||||
```
|
||||
|
||||
Spart Build-Kontext und verhindert Geheimnis-Leaks.
|
||||
|
||||
---
|
||||
|
||||
# compose.yml – Services
|
||||
|
||||
```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
|
||||
ports:
|
||||
- "10000:8080"
|
||||
networks: [net, data]
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# compose.yml – Datenbank + Proxy
|
||||
|
||||
```yaml
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: ${DATABASE_USERNAME}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||
POSTGRES_DB: ${DATABASE_NAME}
|
||||
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]
|
||||
|
||||
volumes:
|
||||
dbdata:
|
||||
|
||||
networks:
|
||||
net:
|
||||
data:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Docker Compose – Befehle
|
||||
|
||||
```bash
|
||||
docker compose -p "dhbw-docker-app" \
|
||||
-f compose.yml \
|
||||
--env-file .env \
|
||||
up \
|
||||
--build \
|
||||
--remove-orphans \
|
||||
--force-recreate
|
||||
```
|
||||
|
||||
| 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Health Checks
|
||||
|
||||
```yaml
|
||||
api:
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
```
|
||||
|
||||
`depends_on: { condition: service_healthy }` wartet, bis Health passt.
|
||||
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
dbdata: # benannt
|
||||
- ./code:/app # bind mount (dev)
|
||||
- /app/node_modules # anonymous (überschreibt bind)
|
||||
```
|
||||
|
||||
`docker compose down` ohne `-v` lässt Volumes leben.
|
||||
|
||||
---
|
||||
|
||||
# Live Demo
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
→ http://localhost:10007
|
||||
|
||||
---
|
||||
|
||||
# 100 Concepts of Docker (Exkurs)
|
||||
|
||||
- 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
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _backgroundColor: #001520 -->
|
||||
|
||||
# Fragen?
|
||||
|
||||
**Hausaufgabe:** Eigenes Projekt mit `Dockerfile` + `compose.yml` containerisieren.
|
||||
346
slides/dhbw/08_best_practices.md
Normal file
346
slides/dhbw/08_best_practices.md
Normal file
@@ -0,0 +1,346 @@
|
||||
---
|
||||
marp: true
|
||||
theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
title: Best Practices
|
||||
---
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--color-foreground: #1a1a2e;
|
||||
--color-highlight: #005f8a;
|
||||
--color-dimmed: #4a6a7a;
|
||||
}
|
||||
section.invert {
|
||||
--color-foreground: #fff;
|
||||
}
|
||||
section {
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
h1 {
|
||||
color: #005f8a;
|
||||
}
|
||||
section.invert h1 {
|
||||
color: #fff;
|
||||
}
|
||||
pre {
|
||||
background: #0f1e2e;
|
||||
color: #00b4d8;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #005f8a;
|
||||
}
|
||||
pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
code {
|
||||
background: #1a3a4a;
|
||||
color: #00b4d8;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
a {
|
||||
color: var(--color-highlight);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _header: '' -->
|
||||
<!-- _backgroundColor: #001520 -->
|
||||
|
||||
# Best Practices
|
||||
|
||||
## Wrap-up & Projektsupport
|
||||
|
||||
---
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
**Schlecht:**
|
||||
```
|
||||
fixed stuff
|
||||
WIP
|
||||
update
|
||||
asdf
|
||||
```
|
||||
|
||||
→ https://udacity.github.io/git-styleguide/
|
||||
|
||||
---
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
Vorteile: automatische Changelogs, automatische Version-Bumps (`semantic-release`).
|
||||
|
||||
---
|
||||
|
||||
# 12 Factor App
|
||||
|
||||
Methodik für moderne, skalierbare SaaS-Apps – https://12factor.net/
|
||||
|
||||
| # | 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 |
|
||||
|
||||
---
|
||||
|
||||
# 12 Factor (2/2)
|
||||
|
||||
| # | 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 |
|
||||
|
||||
---
|
||||
|
||||
# Config via ENV
|
||||
|
||||
```javascript
|
||||
// schlecht
|
||||
const db = "postgres://user:secret@prod-db:5432/app";
|
||||
|
||||
// gut
|
||||
const db = process.env.DATABASE_URL;
|
||||
```
|
||||
|
||||
```bash
|
||||
# .env (NICHT committen!)
|
||||
DATABASE_URL=postgres://user:secret@db:5432/app
|
||||
NODE_ENV=production
|
||||
PORT=8080
|
||||
```
|
||||
|
||||
`.env.example` mit Dummy-Werten committen.
|
||||
|
||||
---
|
||||
|
||||
# Make It Work · Right · Fast
|
||||
|
||||
```
|
||||
1. Make It Work → MVP, hässlich aber funktional
|
||||
2. Make It Right → Refactor, Tests, Clean Code
|
||||
3. Make It Fast → Profiling, Optimierung
|
||||
```
|
||||
|
||||
Don't optimize what doesn't work.
|
||||
Don't refactor what isn't working yet.
|
||||
|
||||
→ https://wiki.c2.com/?MakeItWorkMakeItRightMakeItFast
|
||||
|
||||
---
|
||||
|
||||
# KISS · DRY · YAGNI · SOLID
|
||||
|
||||
- **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
|
||||
|
||||
→ Faustregeln, keine Dogmen.
|
||||
|
||||
---
|
||||
|
||||
# Präsentation – Aufbau
|
||||
|
||||
- **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?
|
||||
|
||||
---
|
||||
|
||||
# Präsentation – Pitfalls
|
||||
|
||||
- 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
|
||||
|
||||
→ **1 Folie = 1 Aussage.**
|
||||
|
||||
---
|
||||
|
||||
# Feedback – Sandwich-Methode
|
||||
|
||||
```
|
||||
🍞 Lob / Positives
|
||||
🥬 Kritik + konkreter Verbesserungsvorschlag
|
||||
🍞 Lob / Abschluss
|
||||
```
|
||||
|
||||
**Pitfalls:**
|
||||
- Am Thema vorbeireden
|
||||
- Bei Unklarheiten keine Fragen stellen
|
||||
- Subjektives Feedback ("Farbe gefällt mir nicht")
|
||||
|
||||
→ **Konkret, sachlich, umsetzbar.**
|
||||
|
||||
---
|
||||
|
||||
# Prüfungsleistung – Recap
|
||||
|
||||
**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://git.dailysh.it/DHBW/pruefungsleistung
|
||||
|
||||
---
|
||||
|
||||
# Abgabe + Termine
|
||||
|
||||
- **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
|
||||
|
||||
---
|
||||
|
||||
# 70 Things I Regret as a Developer (Auswahl)
|
||||
|
||||
- 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.**
|
||||
|
||||
---
|
||||
|
||||
# Weiterlesen
|
||||
|
||||
- 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
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _backgroundColor: #001520 -->
|
||||
|
||||
# Viel Erfolg mit den Projekten!
|
||||
|
||||
Fragen? Probleme? → Telegram, Mail, Sprechstunde.
|
||||
Reference in New Issue
Block a user