Einfache Webanwendung von Grund auf mit HTML, CSS und JavaScript erstellen: vollständige Anleitung

IAGerado por IA
Nov 19, 2025
16 min de leitura
0 reads
Sem avaliações
Tecnologia

Das Erstellen einer Webanwendung von Grund auf — nur mit HTML, CSS und JavaScript — ist eine der besten Methoden, um deine Fähigkeiten zu schärfen und den kompletten Browser-Stack zu beherrschen. In diesem Tutorial erstellst du eine vollständige, ausgefeilte und zugängliche Single-Page-Webapp: einen einfachen Aufgabenmanager, der Aufgaben erstellen, bearbeiten, als erledigt markieren, filtern und in localStorage persistieren kann. Du lernst, wie du Dateien strukturierst, semantisches HTML schreibst, mit modernem CSS gestaltest, JavaScript-Module architekturierst, Zustand verwaltest, Ereignisse über Delegation behandelst, Daten persistierst und die App statisch bereitstellst. Am Ende hast du eine professionelle App und ein wiederholbares Muster für zukünftige Projekte. Konzeptdiagramm der App-Architektur: HTML-Struktur, CSS-Schicht, JS-Module und localStorage-Persistenz

Was wir bauen werden

Wir bauen eine Todo-App mit:

  • Aufgaben hinzufügen, bearbeiten, als erledigt markieren und löschen
  • Filtern: Alle, Aktiv, Erledigt
  • Persistenz über localStorage
  • Zugängliche Markup (Labels, Rollen, Tastaturnavigation)
  • Responsives Design und ausgefeilte Interaktionen

Wichtige Entscheidungen:

  • Framework-frei bleiben (vanilla JS)
  • Module (ES Modules) zur Organisation des Codes verwenden
  • Anfangs auf Build-Tools verzichten; du kannst sie später hinzufügen
  • Rendering einfach und robust halten

Voraussetzungen und Werkzeuge

Du solltest sicher sein im Umgang mit:

  • HTML: Formulare, Semantik, grundlegendes ARIA
  • CSS: Flexbox, Custom Properties, responsive Einheiten
  • JavaScript: ES6+, DOM-API, Events, Module (import/export)

Werkzeuge:

  • Ein moderner Browser (Chrome, Firefox, Edge, Safari)
  • Ein Code-Editor (VS Code empfohlen)
  • Ein lokaler Webserver (VS Code Live Server Extension oder python -m http.server)

Plane die App

Bevor du mit dem Codieren beginnst, skizziere die Form der App und das Datenmodell.

Funktionen und Abläufe:

  • Der Nutzer tippt eine Aufgabe und drückt Enter oder klickt auf Hinzufügen
  • Die Aufgabe erscheint in der Liste mit einer Checkbox und Aktionen (Bearbeiten, Löschen)
  • Der Nutzer toggelt Erledigt, bearbeitet Text inline oder löscht die Aufgabe
  • Filter passen die Listenansicht an
  • Die App merkt sich Aufgaben zwischen Sitzungen

Datenmodell:

  • Aufgabenstruktur: { id: string, text: string, completed: boolean, createdAt: number }
  • App-Zustand: { tasks: Task[], filter: "all" | "active" | "completed" }

Rendering-Ansatz:

  • Bei jeder Änderung aus dem Zustand rendern (einfach und verlässlich)
  • Event-Delegation auf der Liste verwenden, um Item-Aktionen zu behandeln
  • View-Funktionen so oft wie möglich rein halten

Barrierefreiheit:

  • Ein form mit einem Label für das Eingabefeld verwenden
  • Buttons für Aktionen verwenden (keine Links)
  • Änderungen mit aria-live für die Aufgabenanzahl ankündigen
  • Tastaturbedienbarkeit sicherstellen

Wireframe der Oberfläche:

  • Header: Titel und markenkonforme Anzeige
  • Formular: Texteingabe + Hinzufügen-Button
  • Steuerung: Filter-Buttons + "Erledigte löschen"
  • Liste: Jedes Element hat Checkbox, Text, Bearbeiten, Löschen
  • Footer: Statistiken (verbleibende Elemente) Einfaches Wireframe: Kopfbereich, Eingabeformular, Filterzeile, Aufgabenliste, Fußzeile mit Zählern

Projekt initialisieren

Erstelle einen Ordner und die folgende Struktur:

  • index.html
  • styles.css
  • js/
    • main.js
    • state.js
    • storage.js
    • dom.js

Vom Terminal aus:

  • mkdir simple-todo-app && cd simple-todo-app
  • mkdir js
  • touch index.html styles.css js/main.js js/state.js js/storage.js js/dom.js

HTML verfassen

Wir schreiben semantisches, zugängliches HTML mit einer Vorlage für Aufgabenitems.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Simple Tasks</title>
  <link rel="stylesheet" href="styles.css" />
</head>
<body>
  <header class="app-header">
    <h1 class="visually-hidden">Simple Tasks</h1>
    <div class="brand" aria-hidden="true">Simple Tasks</div>
  </header>

  <main class="app" id="app">
    <section aria-labelledby="add-task-heading" class="panel">
      <h2 id="add-task-heading" class="panel-title">Add a task</h2>
      <form id="task-form" class="task-form" autocomplete="off">
        <label for="task-input" class="sr-only">Task</label>
        <input id="task-input" name="task" type="text" placeholder="What needs to be done?" required minlength="1" maxlength="200" />
        <button type="submit" class="btn primary">Add</button>
      </form>
    </section>

    <section aria-labelledby="controls-heading" class="panel controls-panel">
      <h2 id="controls-heading" class="panel-title">Controls</h2>
      <div class="filters" role="group" aria-label="Filter tasks">
        <button class="btn filter is-active" data-filter="all" aria-pressed="true">All</button>
        <button class="btn filter" data-filter="active" aria-pressed="false">Active</button>
        <button class="btn filter" data-filter="completed" aria-pressed="false">Completed</button>
      </div>
      <button class="btn subtle" id="clear-completed" type="button">Clear completed</button>
    </section>

    <section aria-labelledby="list-heading" class="panel">
      <h2 id="list-heading" class="panel-title">Tasks</h2>
      <ul id="todo-list" class="todo-list" aria-live="polite"></ul>

      <!-- Template for tasks -->
      <template id="task-template">
        <li class="todo-item" data-id="">
          <label class="checkbox">
            <input type="checkbox" class="toggle" aria-label="Mark complete" />
            <span class="check"></span>
          </label>
          <div class="item-text" tabindex="0"></div>
          <div class="actions">
            <button class="btn icon edit" aria-label="Edit task" title="Edit">✏️</button>
            <button class="btn icon delete" aria-label="Delete task" title="Delete">🗑️</button>
          </div>
        </li>
      </template>
    </section>

    <footer class="panel footer">
      <div id="stats" aria-live="polite" aria-atomic="true">0 items</div>
    </footer>
  </main>

  <script type="module" src="./js/main.js"></script>
</body>
</html>

Wichtige Details:

  • Das h1 ist visuell verborgen, um Semantik zu bewahren, ohne Platz zu beanspruchen. Das sichtbare Branding steht in einem "brand"-Div.
  • Das <template>-Element speichert die Item-Struktur zum Klonen für jede Aufgabe.
  • aria-live auf der Liste und den Statistiken stellt sicher, dass Screenreader Aktualisierungen erhalten.
  • Für Aktionen und Filter werden Buttons (keine Links) verwendet.

Mit modernem CSS gestalten

Wir halten es minimal, aber ausgefeilt, mit CSS-Custom-Properties, einem kleinen Reset und responsive Layout.

:root {
  --bg: #0f172a;
  --panel: #111827;
  --text: #e5e7eb;
  --muted: #9ca3af;
  --accent: #22c55e;
  --accent-2: #10b981;
  --danger: #ef4444;
  --border: #1f2937;
  --focus: #38bdf8;
  --shadow: 0 10px 30px rgba(0,0,0,.25);
  --radius: 12px;
  --space: 14px;
  --font: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
}

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0; font-family: var(--font); background: radial-gradient(1200px 800px at 20% -10%, #1e293b, transparent),
  radial-gradient(1400px 1000px at 80% 110%, #0b3b2b, transparent), var(--bg); color: var(--text);
}

.visually-hidden, .sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }

.app-header { padding: calc(var(--space) * 2) var(--space); text-align: center; }
.brand { font-weight: 800; font-size: clamp(20px, 5vw, 32px); letter-spacing: .5px; opacity: .95; }

.app { max-width: 800px; margin: 0 auto; padding: var(--space); display: grid; gap: var(--space); }
.panel { background: linear-gradient(180deg, rgba(255,255,255,.02), rgba(255,255,255,.005)); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); padding: calc(var(--space) * 1.2); }
.panel-title { margin-top: 0; margin-bottom: var(--space); font-size: 14px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }

.task-form { display: grid; grid-template-columns: 1fr auto; gap: 10px; }
#task-input {
  width: 100%; padding: 12px 14px; border-radius: 10px; border: 1px solid var(--border);
  background: #0b1220; color: var(--text); outline: none; transition: border-color .15s, box-shadow .15s;
}
#task-input:focus { border-color: var(--focus); box-shadow: 0 0 0 3px rgba(56,189,248,.25); }
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  border-radius: 10px; padding: 10px 14px; border: 1px solid var(--border);
  background: #0f172a; color: var(--text); cursor: pointer; transition: transform .04s ease, background .15s, border-color .15s; user-select: none;
}
.btn:hover { background: #162035; }
.btn:active { transform: translateY(1px); }
.btn.primary { background: linear-gradient(180deg, var(--accent), var(--accent-2)); border: none; color: #052e16; font-weight: 700; }
.btn.subtle { background: #0b1220; color: var(--muted); }
.btn.icon { padding: 8px 10px; min-width: 36px; }

.controls-panel { display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: center; }
.filters { display: inline-flex; gap: 8px; }
.filter.is-active, .filter[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }

.todo-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 8px; }
.todo-item {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center;
  gap: 12px; padding: 10px; border-radius: 10px; background: #0b1220; border: 1px solid var(--border);
}
.checkbox { position: relative; width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; }
.checkbox input { appearance: none; width: 0; height: 0; position: absolute; }
.check {
  width: 20px; height: 20px; border-radius: 6px; border: 2px solid #334155; background: #0b1220; display: inline-block; position: relative; transition: border-color .15s, background .15s;
}
.toggle:focus + .check { outline: 3px solid rgba(56,189,248,.3); }
.toggle:checked + .check { background: var(--accent); border-color: var(--accent); }
.toggle:checked + .check::after { content: "✓"; color: #052e16; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -58%); font-weight: 900; font-size: 14px; }

.item-text { outline: none; min-height: 24px; padding: 6px 8px; border-radius: 8px; }
.todo-item.completed .item-text { color: var(--muted); text-decoration: line-through; }
.item-text:focus { box-shadow: 0 0 0 3px rgba(56,189,248,.25); background: #0f172a; }

.actions { display: inline-flex; gap: 6px; }
.actions .delete:hover { border-color: var(--danger); color: var(--danger); }

.footer { display: flex; justify-content: space-between; align-items: center; color: var(--muted); }

@media (max-width: 560px) {
  .controls-panel { grid-template-columns: 1fr; }
}

Highlights:

  • Nutzt ein warmes, modernes Erscheinungsbild, geeignet für Dark Mode.
  • Sichtbare Fokusindikatoren unterstützen die Barrierefreiheit.
  • Subtile Hover- und Active-Zustände verbessern die Benutzererfahrung.

JavaScript-Module strukturieren

Wir teilen die Logik in Module auf:

  • state.js: verwaltet Zustandsübergänge (reinere Funktionen)
  • storage.js: laden/speichern in localStorage
  • dom.js: Rendering und DOM-Bindings
  • main.js: bootstrappt die App und verbindet die Events

state.js: Zustand und pure Updates

// js/state.js
export const FILTERS = { ALL: "all", ACTIVE: "active", COMPLETED: "completed" };

export function createInitialState(loadedTasks = []) {
  return {
    tasks: loadedTasks,
    filter: FILTERS.ALL
  };
}

export function addTask(state, text) {
  const newTask = {
    id: generateId(),
    text: text.trim(),
    completed: false,
    createdAt: Date.now()
  };
  return { ...state, tasks: [newTask, ...state.tasks] };
}

export function toggleTask(state, id) {
  return {
    ...state,
    tasks: state.tasks.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
  };
}

export function deleteTask(state, id) {
  return { ...state, tasks: state.tasks.filter(t => t.id !== id) };
}

export function editTask(state, id, newText) {
  const text = newText.trim();
  if (!text) return state; // avoid empty text
  return {
    ...state,
    tasks: state.tasks.map(t => (t.id === id ? { ...t, text } : t))
  };
}

export function clearCompleted(state) {
  return { ...state, tasks: state.tasks.filter(t => !t.completed) };
}

export function setFilter(state, filter) {
  if (!Object.values(FILTERS).includes(filter)) return state;
  return { ...state, filter };
}

export function getVisibleTasks(state) {
  switch (state.filter) {
    case FILTERS.ACTIVE: return state.tasks.filter(t => !t.completed);
    case FILTERS.COMPLETED: return state.tasks.filter(t => t.completed);
    default: return state.tasks;
  }
}

export function remainingCount(state) {
  return state.tasks.filter(t => !t.completed).length;
}

function generateId() {
  // 16-char base36 id: timestamp + random
  return (Date.now().toString(36) + Math.random().toString(36).slice(2, 10)).slice(0, 16);
}

Design-Hinweise:

  • Funktionen geben neue Zustandsobjekte zurück; das macht Updates vorhersehbar und testbar.
  • Der Zustand ist ein einfaches Objekt, leicht zu serialisieren.

storage.js: Persistenz

// js/storage.js
const KEY = "simple-tasks/v1";

export function loadTasks() {
  try {
    const raw = localStorage.getItem(KEY);
    if (!raw) return [];
    const parsed = JSON.parse(raw);
    if (!Array.isArray(parsed)) return [];
    // sanitize objects
    return parsed
      .filter(isTaskLike)
      .map(t => ({ id: String(t.id), text: String(t.text), completed: !!t.completed, createdAt: Number(t.createdAt) || Date.now() }));
  } catch {
    return [];
  }
}

export function saveTasks(tasks) {
  try {
    localStorage.setItem(KEY, JSON.stringify(tasks));
  } catch (err) {
    // storage quota errors can happen
    console.warn("Failed to save to localStorage", err);
  }
}

function isTaskLike(v) {
  return v && typeof v === "object" && "id" in v && "text" in v && "completed" in v;
}

Hinweise:

  • Daten werden bereinigt, um Probleme mit beschädigtem Storage zu vermeiden.

dom.js: Rendering und DOM-Helfer

// js/dom.js
import { getVisibleTasks, remainingCount } from "./state.js";

export function bindElements() {
  return {
    form: document.getElementById("task-form"),
    input: document.getElementById("task-input"),
    list: document.getElementById("todo-list"),
    template: document.getElementById("task-template"),
    filters: document.querySelectorAll(".filter"),
    clearCompletedBtn: document.getElementById("clear-completed"),
    stats: document.getElementById("stats")
  };
}

export function render(state, els) {
  // Render the list
  const tasks = getVisibleTasks(state);
  els.list.innerHTML = ""; // simple replace
  const frag = document.createDocumentFragment();

  for (const task of tasks) {
    const node = els.template.content.firstElementChild.cloneNode(true);
    node.dataset.id = task.id;
    if (task.completed) node.classList.add("completed");

    const checkbox = node.querySelector(".toggle");
    const textEl = node.querySelector(".item-text");
    checkbox.checked = task.completed;
    textEl.textContent = task.text;

    frag.appendChild(node);
  }
  els.list.appendChild(frag);

  // Render filters ARIA and active class
  for (const btn of els.filters) {
    const pressed = btn.dataset.filter === state.filter;
    btn.classList.toggle("is-active", pressed);
    btn.setAttribute("aria-pressed", String(pressed));
  }

  // Render stats
  const remaining = remainingCount(state);
  const plural = remaining === 1 ? "item" : "items";
  els.stats.textContent = `${remaining} ${plural} left`;
}

export function focusInput(els) {
  els.input.focus();
  els.input.select?.();
}

main.js: Klebercode und Event-Handler

// js/main.js
import {
  createInitialState, addTask, toggleTask, deleteTask,
  editTask, clearCompleted, setFilter, FILTERS
} from "./state.js";
import { loadTasks, saveTasks } from "./storage.js";
import { bindElements, render, focusInput } from "./dom.js";

let state = createInitialState(loadTasks());
const els = bindElements();

function update(nextState) {
  state = nextState;
  render(state, els);
  saveTasks(state.tasks);
}

function onSubmit(e) {
  e.preventDefault();
  const text = els.input.value.trim();
  if (!text) return;
  update(addTask(state, text));
  els.form.reset();
  focusInput(els);
}

function onListClick(e) {
  const item = e.target.closest(".todo-item");
  if (!item) return;
  const id = item.dataset.id;

  if (e.target.matches(".toggle")) {
    update(toggleTask(state, id));
    return;
  }
  if (e.target.matches(".delete")) {
    update(deleteTask(state, id));
    return;
  }
  if (e.target.matches(".edit")) {
    startInlineEdit(item, id);
    return;
  }
}

function startInlineEdit(item, id) {
  const textEl = item.querySelector(".item-text");
  const oldText = textEl.textContent;
  textEl.setAttribute("contenteditable", "true");
  textEl.focus();
  placeCaretEnd(textEl);

  function commit() {
    textEl.removeAttribute("contenteditable");
    const newText = textEl.textContent;
    if (newText.trim() !== oldText.trim()) {
      update(editTask(state, id, newText));
    } else {
      // re-render to normalize text and state
      render(state, els);
    }
    teardown();
  }

  function cancel() {
    textEl.removeAttribute("contenteditable");
    textEl.textContent = oldText;
    teardown();
  }

  function onKeyDown(ev) {
    if (ev.key === "Enter") {
      ev.preventDefault();
      commit();
    } else if (ev.key === "Escape") {
      ev.preventDefault();
      cancel();
    }
  }

  function onBlur() { commit(); }

  textEl.addEventListener("keydown", onKeyDown);
  textEl.addEventListener("blur", onBlur);

  function teardown() {
    textEl.removeEventListener("keydown", onKeyDown);
    textEl.removeEventListener("blur", onBlur);
  }
}

function placeCaretEnd(el) {
  const range = document.createRange();
  range.selectNodeContents(el);
  range.collapse(false);
  const sel = window.getSelection();
  sel.removeAllRanges();
  sel.addRange(range);
}

function onFilterClick(e) {
  const btn = e.target.closest(".filter");
  if (!btn) return;
  const filter = btn.dataset.filter;
  update(setFilter(state, filter));
}

function onClearCompleted() {
  update(clearCompleted(state));
}

function onListChange(e) {
  // Support toggling via keyboard when focusing checkbox
  if (e.target.matches(".toggle")) {
    const item = e.target.closest(".todo-item");
    if (!item) return;
    const id = item.dataset.id;
    update(toggleTask(state, id));
  }
}

// Bootstrap
els.form.addEventListener("submit", onSubmit);
els.list.addEventListener("click", onListClick);
els.list.addEventListener("change", onListChange);
document.querySelector(".filters").addEventListener("click", onFilterClick);
els.clearCompletedBtn.addEventListener("click", onClearCompleted);

// First render
render(state, els);

Anmerkungen:

  • update zentralisiert Rendering und Persistenz.
  • Inline-Bearbeitung nutzt contenteditable mit Enter zum Bestätigen und Escape zum Abbrechen.
  • Nach Updates wird neu gerendert, um DOM und Zustand synchron zu halten.

Lokal ausführen

Öffne einen lokalen Server, um Modul-/CORS-Probleme zu vermeiden:

  • VS Code: "Live Server" installieren und auf "Go Live" klicken
  • Python: python3 -m http.server 8000 und http://localhost:8000 öffnen
  • Node: npx http-server

Deine App sollte mit einem Eingabefeld laden. Füge Aufgaben hinzu, teste die Filter und lade die Seite neu, um die Persistenz zu prüfen.

Warum diese Architektur funktioniert

  • Trennung der Verantwortlichkeiten: Zustandsübergänge berühren nicht das DOM; Rendering verändert nicht den Zustand.
  • Event-Delegation: Ein einziger Listener behandelt Aktionen vieler Items, was effizient und wartbar ist.
  • Zustandsbasiertes Rendering: Jede Aktualisierung zeichnet die sichtbare UI aus dem Zustand neu, wodurch Synchronisationsfehler vermieden werden.

Barrierefreiheitsdetails und Best Practices

  • Labels: Das Aufgaben-Eingabefeld hat ein Label (visuell verborgen) für Screenreader.
  • Buttons statt Anchors: Aktionen nutzen button-Elemente mit aria-label für Klarheit.
  • Tastaturunterstützung:
    • Enter im Eingabefeld zum Absenden
    • Tab zu einem Aufgabentext und Enter zum Bearbeiten, Escape zum Abbrechen
    • Space toggelt eine fokussierte Checkbox
  • aria-live-Regionen:
    • Die Liste und Statistiken melden Zustandsänderungen, ohne den Fokus zu übernehmen.
  • Fokusmanagement:
    • Nach dem Hinzufügen einer Aufgabe kehrt der Fokus zum Eingabefeld zurück.
  • Farbkontrast: Die Palette behält guten Kontrast; überprüfe sie mit den Accessibility-Tools der DevTools.

Zu vermeidende Fehler:

  • Keine interaktiven Handler auf nicht-interaktiven Elementen ohne role und Tastaturunterstützung setzen.
  • Die "Bearbeiten"-Aktion zugänglich machen; der Aufgabentext ist fokussierbar (tabindex="0").

UX mit kleinen Details verbessern

  • Eingabe nach dem Hinzufügen automatisch auswählen, um das schnelle Hinzufügen zu beschleunigen.
  • Leichte Einblendanimationen für neue Items (optional mit CSS-Transition auf transform/opacity).
  • title-Attribute auf Edit-/Delete-Buttons für Hover-Hinweise.
  • Filter im localStorage persistieren, falls gewünscht (zusätzliche Verbesserung).

Kleine Animationen hinzufügen (optional)

Um das Erscheinen von Listenelementen dezent zu animieren, füge hinzu:

.todo-item { opacity: 0; transform: translateY(4px); animation: fadeIn .18s ease-out forwards; }
@keyframes fadeIn { to { opacity: 1; transform: translateY(0); } }

Das hält die Performance hoch (nur opacity/transform, GPU-beschleunigt).

Testen und Debugging

Manuelle Tests:

  • Aufgaben hinzufügen, bearbeiten, toggeln, löschen
  • Filter wechseln; prüfen, ob die Liste korrekt aktualisiert wird
  • "Erledigte löschen"; verifizieren, dass verbleibende Aufgaben persistiert werden
  • Seite neu laden; prüfen, ob Aufgaben und Erledigt-Status erhalten bleiben
  • Nur Tastatur: Sind alle Aktionen möglich?

Konsolentests:

  • Temporär state in update() loggen, um Übergänge zu untersuchen
  • Große Aufgabenlisten simulieren: In DevTools-Konsole:
    • for (let i=0;i<200;i++) { state = addTask(state, "Task " + i); } render(state, els);

Häufige Bugs:

  • preventDefault() beim Formular nicht aufrufen → Seitenreload
  • data-id bei Listenelementen nicht setzen → Event-Routing bricht
  • contenteditable leert den Text; editTask verwendet trim(), um das zu vermeiden

Performance-Überlegungen

Für diese App ist Re-Rendering der sichtbaren Liste pro Update in Ordnung. Bei Skalierung:

  • Liste diffen (nach id) und nur geänderte Nodes aktualisieren
  • requestAnimationFrame nutzen, um DOM-Schreibvorgänge zu bündeln
  • Aufwändige Arbeit außerhalb des Hauptthreads verlagern (hier nicht erforderlich)
  • Layout-Thrashing vermeiden: DOM-Werte vor Schreibvorgängen lesen

LocalStorage-Performance:

  • Einmal pro Update serialisieren; akzeptabel für kleine Payloads (<1 MB)

Filter persistieren (Verbesserung)

Um den ausgewählten Filter über Sitzungen zu merken, erweitere storage oder verwende die URL-Hash:

  • Beim Start location.hash lesen (/#/active oder #/completed)
  • setFilter bei hashchange aufrufen
  • Links werden so teilbar, z. B. yourapp/#/completed

Beispielanpassung:

// in main.js (enhancement)
window.addEventListener("hashchange", () => {
  const val = location.hash.replace("#/", "") || "all";
  update(setFilter(state, val));
});

Responsivität und Layout-Anpassungen

  • Das Grid klappt auf schmalen Bildschirmen die Steuerung in eine Spalte zusammen.
  • Touch-Ziele sind mindestens 36px hoch für mobile Bedienung.
  • Eingaben und Buttons reagieren auf Hover- und Active-Zustände als Feedback.

Mobile-Optimierungen:

  • inputmode="text" und Autocomplete-Hinweise verwenden, falls du weitere Felder hinzufügst
  • Buttons nicht zu dicht platzieren, um Fehlklicks zu vermeiden

Deployment: App ausliefern

Statisches Hosting ist ideal für diese App.

GitHub Pages:

  1. Erstelle ein GitHub-Repository und pushe deine Dateien
  2. In den Repository-Einstellungen GitHub Pages für den main-Branch und Root-Ordner

Avalie este tutorial

Entrar para avaliar este tutorial

Comentários (0)

Entrar para participar da discussão

Role para baixo para carregar comentários e avaliações