640 lines
16 KiB
Markdown
640 lines
16 KiB
Markdown
# DHBW 2025: Web Engineering Prüfungsleistung
|
||
**Maximale Punktzahl: 100 Punkte**
|
||
|
||
---
|
||
|
||
## 1. Grundanforderungen (25 Punkte)
|
||
|
||
### Projektgrundlagen (15 Punkte)
|
||
- **Funktionsfähige Anwendung** (8 Punkte)
|
||
- Webapp oder Backend-Anwendung läuft fehlerfrei
|
||
- Kernfunktionalitäten sind in der Projektdokumentation beschrieben und testbar
|
||
- **Repository** (4 Punkte)
|
||
- GitHub/GitLab/CodeBerg Repository ist (öffentlich) zugänglich
|
||
- Commit-Historie zeigt kontinuierliche Entwicklung
|
||
- **Projektdokumentation** (3 Punkte)
|
||
- Aussagekräftiger Projekttitel und Beschreibung
|
||
- README.md mit Installation, Verwendung und Technologie-Stack
|
||
- Kurze Erklärung der Hauptfunktionen am Anfang relevanter Dateien
|
||
|
||
### Präsentation (20 Punkte)
|
||
- **Elevator Pitch** (5 Punkte)
|
||
- Maximal 1 Minute
|
||
- Welches Problem wird gelöst und warum wurde es vorher noch nicht gelöst?
|
||
- Zielgruppe definiert
|
||
- **Technische Präsentation** (5 Punkte)
|
||
- PowerPoint/Folien mit Technologie-Stack
|
||
- Live-Demo der Anwendung
|
||
- **Projektevolution:** (5 Punkte)
|
||
Jedes Projekt verläuft unterschiedlich.
|
||
Sofern es zutrifft, beschreibt die unterschiedlichen Phasen des Projekts.
|
||
*Wie weit seid ihr gekommen? Wo waren die Stolpersteine? Wie könnte sich eure Anwendung bei fortgesetzter Arbeit entwickeln?*
|
||
|
||
0. (Paper) Prototype
|
||
1. Make it work (PoC)
|
||
2. Make it right (MvP)
|
||
3. Make it fast (Production)
|
||
|
||
---
|
||
|
||
## 2. Clean Code Prinzipien (35 Punkte)
|
||
|
||
### Projektstruktur (10 Punkte)
|
||
- **Ordnerstruktur** (5 Punkte)
|
||
```
|
||
projekt/
|
||
├── src/
|
||
│ ├── components/ # React Components / UI
|
||
│ ├── config/ # Konfigurationen, Hard-coded Strings etc.
|
||
│ ├── services/ # Separitiere für-sich-stehende Funktionen Klassen, Clients etc.
|
||
│ ├── utils/ # Hilfsfunktionen
|
||
│ ├── main.css # Haupteinstieg in CSS Klassen, Variablen, Imports etc.
|
||
│ ├── main.js # Die eigentliche Appliktion
|
||
│ └── index.js # Haupteinstieg in die gesamte Applikation
|
||
├── public/ # Statische Assets, Ressourcen, Bilder etc.
|
||
├── tests/ # Test-Dateien
|
||
├── .gitignore
|
||
├── package.json
|
||
└── README.md
|
||
```
|
||
- Logische Trennung von Funktionalitäten
|
||
- Konsistente Benennung
|
||
|
||
- **Datei-Organisation** (5 Punkte)
|
||
- Ein Konzept pro Datei (Single Responsibility)
|
||
- Konsistente Namenskonventionen: `kebab-case` für Dateien, `camelCase` für Variablen, `PascalCase` für Komponenten/Klassen
|
||
|
||
### Code-Qualität (15 Punkte)
|
||
|
||
#### KISS Prinzip (5 Punkte)
|
||
|
||
**❌ Nicht so einfach:**
|
||
```javascript
|
||
const processData = (d) => d.filter(x => x.status === 'active').map(x => ({...x, processed: true})).reduce((acc, curr) => acc + curr.value, 0);
|
||
```
|
||
|
||
**✅ Einfach:**
|
||
```javascript
|
||
function calculateActiveTotal(data) {
|
||
const activeItems = data.filter(item => item.status === 'active');
|
||
const processedItems = activeItems.map(item => ({
|
||
...item,
|
||
processed: true
|
||
}));
|
||
return processedItems.reduce((total, item) => total + item.value, 0);
|
||
}
|
||
```
|
||
|
||
**❌ Nicht so einfach:**
|
||
```javascript
|
||
const u = users.find(u => u.id === id && u.active && !u.deleted) || null;
|
||
```
|
||
|
||
**✅ Einfach:**
|
||
```javascript
|
||
function findActiveUser(users, userId) {
|
||
return users.find(user =>
|
||
user.id === userId &&
|
||
user.active &&
|
||
!user.deleted
|
||
) || null;
|
||
}
|
||
```
|
||
|
||
#### Beschreibende Namen (5 Punkte)
|
||
|
||
**❌ Schlecht:**
|
||
```javascript
|
||
let isActive = true;
|
||
let data = [];
|
||
function calc(x, y) { return x * y; }
|
||
```
|
||
|
||
**✅ Gut:**
|
||
```javascript
|
||
let isMenuVisible = true;
|
||
let userProfiles = [];
|
||
function calculateTotalPrice(price, taxRate) { return price * taxRate; }
|
||
```
|
||
|
||
**❌ Unklar und uneindeutig:**
|
||
```javascript
|
||
const btn = document.getElementById('btn');
|
||
let flag = false;
|
||
function process(items) { /* ... */ }
|
||
```
|
||
|
||
**✅ Sprechende Variablennamen:**
|
||
```javascript
|
||
const submitButton = document.getElementById('submit-btn');
|
||
let isFormValid = false;
|
||
function validateUserInput(formData) { /* ... */ }
|
||
```
|
||
|
||
#### Funktionsverantwortlichkeiten (5 Punkte)
|
||
|
||
**❌ Zu viele Verantwortlichkeiten:**
|
||
```javascript
|
||
function handleUser(userData) {
|
||
// Validiert Daten
|
||
if (!userData.email) return false;
|
||
// Speichert in DB
|
||
database.save(userData);
|
||
// Sendet Email
|
||
sendWelcomeEmail(userData.email);
|
||
// Updated UI
|
||
updateUserList();
|
||
return true;
|
||
}
|
||
```
|
||
|
||
**✅ Eine klare Verantwortlichkeit pro Funktion:**
|
||
```javascript
|
||
function validateUserData(userData) {
|
||
return userData.email && userData.name;
|
||
}
|
||
|
||
function saveUser(userData) {
|
||
return database.save(userData);
|
||
}
|
||
|
||
function sendWelcomeEmail(email) {
|
||
return emailService.send(email, 'welcome');
|
||
}
|
||
```
|
||
|
||
### Kommentierung & Dokumentation (5 Punkte)
|
||
**JSDoc ist optional und der beste Code muss nicht kommentiert werden.**
|
||
|
||
**❌ Über-/Unterkommentiert:**
|
||
```javascript
|
||
// Erhöht i um 1
|
||
i++;
|
||
// Checked ob user existiert
|
||
if (user) {
|
||
// Macht irgendwas
|
||
doSomething();
|
||
}
|
||
```
|
||
|
||
**✅ Sinnvoll kommentiert:**
|
||
```javascript
|
||
/**
|
||
* Handles user authentication and session management
|
||
*
|
||
* OR
|
||
*
|
||
* This file manages JWT tokens and user permissions
|
||
*/
|
||
|
||
function calculateCompoundInterest(principal, rate, time) {
|
||
// Using compound interest formula: A = P(1 + r/n)^(nt)
|
||
return principal * Math.pow((1 + rate), time);
|
||
}
|
||
```
|
||
|
||
**❌ Unterkommentiert:**
|
||
```javascript
|
||
function x(a,b,c,d,e) {
|
||
return a*b+c-d/e;
|
||
}
|
||
```
|
||
|
||
**✅ Mit JSDoc dokumentiert:**
|
||
```javascript
|
||
/**
|
||
* Calculates shipping cost based on weight, distance and priority
|
||
* @param {number} weight - Package weight in kg
|
||
* @param {number} distance - Delivery distance in km
|
||
* @param {boolean} isPriority - Priority delivery flag
|
||
* @returns {number} Total shipping cost
|
||
*/
|
||
function calculateShippingCost(weight, distance, priority) {
|
||
const baseCost = weight * 2.5;
|
||
const distanceCost = distance * 0.1;
|
||
const priorityMultiplier = priority ? 1.5 : 1;
|
||
return (baseCost + distanceCost) * priorityMultiplier;
|
||
}
|
||
```
|
||
|
||
### Live-Demo (10 Punkte)
|
||
- **Fehlerfreie Demonstration** (10 Punkte)
|
||
- Anwendung läuft während der Präsentation stabil
|
||
- Alle Kernfunktionen werden erfolgreich vorgeführt
|
||
- Ungefähr 5 Minuten Demo-Zeit
|
||
|
||
---
|
||
|
||
## 3. Accessibility & UX (25 Punkte)
|
||
|
||
### Web Accessibility (15 Punkte)
|
||
|
||
#### Semantisches HTML (5 Punkte)
|
||
|
||
**❌ Nicht semantisch:**
|
||
```html
|
||
<div class="header">
|
||
<div class="nav">
|
||
<div class="nav-item">Home</div>
|
||
<div class="nav-item">About</div>
|
||
</div>
|
||
</div>
|
||
<div class="content">
|
||
<div class="title">Welcome</div>
|
||
<div class="text">Description...</div>
|
||
</div>
|
||
```
|
||
|
||
**✅ Semantisch korrekt:**
|
||
```html
|
||
<header>
|
||
<nav aria-label="Hauptnavigation">
|
||
<ul>
|
||
<li><a href="#home">Home</a></li>
|
||
<li><a href="#about">About</a></li>
|
||
</ul>
|
||
</nav>
|
||
</header>
|
||
<main>
|
||
<section>
|
||
<h1>Welcome</h1>
|
||
<p>Description...</p>
|
||
</section>
|
||
</main>
|
||
```
|
||
|
||
#### ARIA Labels & Alt-Texte (5 Punkte)
|
||
|
||
**❌ Schlecht:**
|
||
```html
|
||
<img src="chart.png" alt="chart">
|
||
<button onclick="closeMenu()">×</button>
|
||
<input type="email" required>
|
||
<div>We don't send spam</div>
|
||
```
|
||
|
||
**✅ Gut:**
|
||
```html
|
||
<img src="chart.png" alt="Verkaufszahlen 2024: 40% Steigerung gegenüber Vorjahr">
|
||
<button aria-label="Menü schließen" onclick="closeMenu()">×</button>
|
||
<input type="email" aria-describedby="email-help" required>
|
||
<div id="email-help">Wir versenden keine Spam-Mails</div>
|
||
```
|
||
|
||
#### Tastaturnavigation (5 Punkte)
|
||
- Alle interaktiven Elemente per Tab erreichbar
|
||
- Sichtbarer Focus-Indikator
|
||
- Logische Tab-Reihenfolge
|
||
|
||
**❌ Nicht tastaturzugänglich:**
|
||
```html
|
||
<div class="clickable">🍔</div>
|
||
```
|
||
|
||
```css
|
||
div.clickable {
|
||
cursor: pointer;
|
||
}
|
||
```
|
||
|
||
**✅ Tastaturzugänglich:**
|
||
```css
|
||
div.clickable:focus {
|
||
outline: 2px solid #007acc;
|
||
}
|
||
```
|
||
|
||
```html
|
||
<div class="clickable" tabindex="0" aria-label="Menü öffnen">🍔</div>
|
||
```
|
||
|
||
### Responsive Design (10 Punkte)
|
||
|
||
#### Mobile First Approach (5 Punkte - mindestens 1 Breakpoint)
|
||
```css
|
||
/* Mobile First */
|
||
.container {
|
||
width: 100%;
|
||
padding: 1rem;
|
||
}
|
||
|
||
/* Desktop */
|
||
@media (min-width: 768px) {
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 2rem;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Flexbox/Grid Layout (5 Punkte)
|
||
|
||
**❌ Veraltete Layouts:**
|
||
```css
|
||
.container {
|
||
float: left;
|
||
width: 33.33%;
|
||
}
|
||
.clearfix::after {
|
||
content: "";
|
||
display: table;
|
||
clear: both;
|
||
}
|
||
```
|
||
|
||
**✅ Moderne Layouts:**
|
||
```css
|
||
.card-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||
gap: 1.5rem;
|
||
}
|
||
.navigation {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Erweiterte Technologien & Innovation (15 Punkte)
|
||
|
||
### Plattformunabhängigkeit (5 Punkte)
|
||
- Funktioniert auf macOS, Windows, GNU/Linux
|
||
- Browser-Kompatibilität (Chrome, Firefox, Safari, Edge)
|
||
|
||
**❌ Plattformspezifisch:**
|
||
```javascript
|
||
// Nur Chrome
|
||
navigator.clipboard.writeText(text);
|
||
```
|
||
|
||
**✅ Plattformunabhängig:**
|
||
|
||
In aller Regel sind solche sog. Polyfills kaum noch eigenhändig zu implementieren.
|
||
Nichtsdestotrotz gilt es die Plattformunabhängig zu prüfen und ggf. Alternativen für die Implementierung zu finden wie im folgenden Beispiel für die Zwischenablage:
|
||
|
||
```javascript
|
||
// Fallback für ältere Browser
|
||
function copyToClipboard(text) {
|
||
if (navigator.clipboard) {
|
||
return navigator.clipboard.writeText(text);
|
||
} else {
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = text;
|
||
document.body.appendChild(textArea);
|
||
textArea.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(textArea);
|
||
}
|
||
}
|
||
```
|
||
|
||
### Entwicklungs-Tools (10 Punkte)
|
||
|
||
#### Package Management (4 Punkte)
|
||
|
||
Sämtliche nodeJS/JavaScript Projekte sollten eine `package.json` beinhalten.
|
||
`npm init` und ein Dialog öffnet sich.
|
||
|
||
```json
|
||
{
|
||
"name": "my-project",
|
||
"scripts": {
|
||
"start": "vite",
|
||
"build": "vite build",
|
||
"test": "jest",
|
||
"lint": "eslint src/"
|
||
},
|
||
"dependencies": {
|
||
"react": "^18.0.0"
|
||
},
|
||
"devDependencies": {
|
||
"vite": "^4.0.0",
|
||
"jest": "^29.0.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Build Tools & Optimierung (6 Punkte)
|
||
- Vite, Webpack, oder ähnliche Tools
|
||
- Optimierte Produktions-Builds
|
||
- Debugger und unnötige console.log()'s entfernen
|
||
- Code-Splitting
|
||
- Nur Teile statt gesamte Bibliotheken importieren
|
||
- Bspw. PostCSS für Entfernung von überschüssigem CSS
|
||
|
||
**❌ Entwicklungs-Build:**
|
||
```javascript
|
||
// Unminified, alle Dependencies geladen
|
||
import * as _ from 'lodash';
|
||
// Debugging
|
||
console.log('Debug info:', data);
|
||
debugger;
|
||
```
|
||
|
||
**✅ Produktions-Build:**
|
||
```javascript
|
||
// Minified, nur benötigte Funktionen
|
||
import { debounce } from 'lodash';
|
||
|
||
// Debug-Code entfernt in Production
|
||
```
|
||
|
||
### Zusätzliche Features (Bonus: bis zu 10 Punkte)
|
||
**Wähle mindestens 2 Bereiche:**
|
||
|
||
#### API Integration (5 Punkte)
|
||
|
||
**❌ Ohne Error Handling:**
|
||
```javascript
|
||
async function getUser(id) {
|
||
const response = await fetch(`/api/users/${id}`);
|
||
return response.json();
|
||
}
|
||
```
|
||
|
||
**✅ Mit Error Handling:**
|
||
```javascript
|
||
async function fetchUserData(userId) {
|
||
try {
|
||
const response = await fetch(`/api/users/${userId}`);
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('Fehler beim Laden der Benutzerdaten:', error);
|
||
throw new Error('Benutzerdaten konnten nicht geladen werden');
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Local Storage / State Management (5 Punkte)
|
||
|
||
**❌ Ohne Error Handling:**
|
||
```javascript
|
||
function saveData(data) {
|
||
localStorage.setItem('data', JSON.stringify(data));
|
||
}
|
||
function loadData() {
|
||
return JSON.parse(localStorage.getItem('data'));
|
||
}
|
||
```
|
||
|
||
**✅ Mit Error Handling:**
|
||
```javascript
|
||
function saveUserPreferences(prefs) {
|
||
try {
|
||
localStorage.setItem('userPrefs', JSON.stringify(prefs));
|
||
return true;
|
||
} catch (error) {
|
||
console.warn('Local Storage nicht verfügbar:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function loadUserPreferences() {
|
||
try {
|
||
const data = localStorage.getItem('userPrefs');
|
||
return data ? JSON.parse(data) : getDefaultPreferences();
|
||
} catch (error) {
|
||
console.warn('Fehler beim Laden der Einstellungen:', error);
|
||
return getDefaultPreferences();
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Progressive Web App Features (5 Punkte)
|
||
- Service Worker für Offline-Funktionalität
|
||
- Manifest.json für App-Installation
|
||
- Caching-Strategien
|
||
|
||
**❌ Ohne PWA:**
|
||
```html
|
||
<!-- Nur normale Website -->
|
||
```
|
||
|
||
**✅ Mit PWA Features:**
|
||
```html
|
||
<!-- manifest.json -->
|
||
<link rel="manifest" href="/manifest.json">
|
||
<!-- Service Worker -->
|
||
<script>
|
||
if ('serviceWorker' in navigator) {
|
||
navigator.serviceWorker.register('/sw.js');
|
||
}
|
||
</script>
|
||
```
|
||
|
||
#### Testing (10 Punkte)
|
||
|
||
Von Vornherein mitgedachte Tests helfen beim Code-Splitting.
|
||
Um beim Testen jene Funktionen eindeutig referenzieren zu können, hilft es sie in einzelne Dateien zu verteilen und Ordnern zu gruppieren.
|
||
|
||
**❌ Ohne Tests:**
|
||
```javascript
|
||
// Keine Tests vorhanden
|
||
function calculateTotal(price, tax) {
|
||
return price + (price * tax);
|
||
}
|
||
```
|
||
|
||
**✅ Mit Tests:**
|
||
```javascript
|
||
// calculate.js
|
||
export function calculateTotal(price, tax) {
|
||
if (typeof price !== 'number' || typeof tax !== 'number') {
|
||
throw new Error('Price and tax must be numbers');
|
||
}
|
||
return price + (price * tax);
|
||
}
|
||
|
||
// calculate.test.js
|
||
import { calculateTotal } from './calculate.js';
|
||
|
||
test('calculateTotal should return correct sum', () => {
|
||
expect(calculateTotal(100, 0.19)).toBeCloseTo(119);
|
||
});
|
||
|
||
test('calculateTotal should throw error for invalid input', () => {
|
||
expect(() => calculateTotal('100', 0.19)).toThrow();
|
||
});
|
||
```
|
||
|
||
#### Responsive Design Erweitert (5 Punkte - 2 oder mehr Breakpoints)
|
||
```css
|
||
/* Mobile */
|
||
.grid { display: block; }
|
||
|
||
/* Tablet */
|
||
@media (min-width: 768px) {
|
||
.grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
}
|
||
}
|
||
|
||
/* Desktop */
|
||
@media (min-width: 1024px) {
|
||
.grid {
|
||
grid-template-columns: repeat(3, 1fr);
|
||
}
|
||
}
|
||
|
||
/* Large Desktop */
|
||
@media (min-width: 1440px) {
|
||
.grid {
|
||
grid-template-columns: repeat(4, 1fr);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Security Features (5 Punkte)
|
||
|
||
**❌ Unsicher:**
|
||
```javascript
|
||
// XSS anfällig
|
||
element.innerHTML = userInput;
|
||
// Unvalidierte Eingabe
|
||
function search(query) {
|
||
return database.query(`SELECT * FROM users WHERE name = '${query}'`);
|
||
}
|
||
```
|
||
|
||
**✅ Sicher:**
|
||
```javascript
|
||
// XSS-sicher durch HTML-Escaping
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
element.innerHTML = escapeHtml(userInput);
|
||
|
||
// SQL-Injection sicher
|
||
function search(query) {
|
||
const sanitizedQuery = query.replace(/[^\w\s]/gi, '');
|
||
return database.query('SELECT * FROM users WHERE name = ?', [sanitizedQuery]);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Abgabe
|
||
|
||
- **Deadline:** 27.07.2025
|
||
- **Format:** GitHub/GitLab Repository Link + Live-Demo URL (falls gehostet)
|
||
- **Präsentation:** 18.07.2025 - 10-15 Minuten pro Gruppe + ~5 Minuten Demo
|
||
|
||
## Tipps
|
||
|
||
1. **Beginnt früh** mit der Eingrenzung des Funktionsumfangs
|
||
2. **Testet eure Anwendung** auf verschiedenen Geräten, Betriebssystemen und Browsern
|
||
3. **Fragt bei Unklarheiten** rechtzeitig nach
|
||
4. **Fokussiert euch** auf Qualität statt Quantität
|
||
5. **Behaltet Barrieren im Blick** und vermeidet sie rechtzeitig
|
||
5. **Verwendet gute Struktur** um eure Code-Beispiele zu organisieren
|