Oberfläche zerlegen
Auftrag
Der Freiwillige hat sich App.vue angesehen und einen Vorschlag gemacht. «Wir zerlegen die App in Komponenten, dann muss niemand mehr 180 Zeilen verstehen, um das Formular zu ändern.» Martina Hostettler ist einverstanden. «Hauptsache, die Mitglieder merken nichts davon.»
Als Freiwilliger möchte ich die App in kleine Komponenten mit je einer Aufgabe zerlegen, damit ich eine Stelle ändern kann, ohne den Rest zu verstehen.
Die Akzeptanzkriterien lauten:
- Ein Baum zeigt alle Komponenten der App und die Aufgabe jeder Komponente.
- Jede Komponente hat einen Namen aus mehreren Wörtern und eine eigene Datei.
- Für jeden Zustand ist festgelegt, welche Komponente ihn besitzt.
- Kopf, Fuss und Öffnungszeiten sind schon eigene Komponenten, und die Seite sieht aus wie vorher.
Lernziele
Nach diesem Kapitel kannst du
- eine Oberfläche anhand eines Entwurfs in einen Baum von Komponenten zerlegen,
- jeder Komponente genau eine Aufgabe geben und sie passend benennen,
- Komponenten in Dateien und Ordnern ablegen,
- entscheiden, welche Komponente einen Zustand besitzt,
- Teile ohne Daten in eigene Komponenten auslagern.
Du brauchst dafür Single File Components aus dem Kapitel «Das erste Projekt».
Der Baum der Komponenten
Abschnitt betitelt „Der Baum der Komponenten“App.vue enthält am Ende von Teil 3 den Katalog, die Suche, den Filter, die Seite eines Dings und das Formular mit seinen Regeln. Wer das Formular ändert, scrollt an der Suche vorbei, und eine Änderung an einer Stelle kann eine andere treffen. Spätestens dann lohnt es sich, die App zu zerlegen.
Am einfachsten geht das mit einem Entwurf der Seite, einer Skizze auf Papier oder einem Bildschirmfoto. Du zeichnest Rahmen um die Bereiche, die zusammengehören. Jeder Rahmen wird eine Komponente, und Rahmen in einem anderen Rahmen werden ihre Kinder. So entsteht ein Baum mit App an der Spitze.
CatalogView und ItemView sind die beiden Seiten der App. Sie setzen die Bausteine darunter zusammen. In Teil 6 übernimmt Vue Router den Wechsel zwischen ihnen. BaseDialog kommt in Teil 4 erst später dazu, der Baum zeigt schon das Ziel.
Eine Aufgabe pro Komponente
Abschnitt betitelt „Eine Aufgabe pro Komponente“Eine gute Komponente lässt sich in einem Satz ohne «und» beschreiben. ItemCard zeigt ein Ding. SearchBar nimmt einen Suchtext entgegen. ReservationForm sammelt eine Anfrage und prüft sie. Brauchst du für eine Komponente ein «und», etwa «zeigt den Katalog und das Formular», steckt darin mehr als eine Aufgabe.
Zu klein ist auch nicht gut. Eine Komponente für einen einzelnen Absatz bringt eine Datei mehr, aber keinen Gewinn. Gute Kandidaten sind Teile, die sich wiederholen, wie die Karte eines Dings, und Teile mit eigener Logik, wie das Formular oder die Öffnungszeiten.
Namen und Dateien
Abschnitt betitelt „Namen und Dateien“Komponenten bekommen Namen aus mehreren Wörtern in PascalCase, damit sie sich nicht mit HTML-Elementen wie header oder footer verwechseln lassen. Der Styleguide von Vue empfiehlt dazu einige Konventionen.
- Komponenten, die es in der App nur einmal gibt, tragen ein gemeinsames Präfix. Der Styleguide nennt
The, wie beiTheWelcomeim Beispiel von create-vue. Der Kurs verwendetApp, wie beiAppHeaderundAppFooter. - Allgemeine Bausteine ohne Bezug zur App beginnen mit
Base, wieBaseDialog. - Namen gehen vom Allgemeinen zum Besonderen, darum heissen die Teile des Katalogs
ItemListundItemCardund nichtListOfItems.
Jede Komponente bekommt eine eigene Datei mit ihrem Namen. Bausteine liegen in src/components, ganze Seiten wie CatalogView in src/views. Diesen Ordner verwendet auch create-vue, sobald du den Router auswählst.
Der Ort für den Zustand
Abschnitt betitelt „Der Ort für den Zustand“Jeder Zustand gehört genau einer Komponente. Sie hält ihn, ändert ihn und gibt ihn an die Komponenten darunter weiter. Welche das ist, folgt aus einer Regel. Der Zustand gehört in die tiefste Komponente, die über allen Komponenten liegt, die ihn brauchen.
- Den Katalog brauchen
CatalogViewundItemView. Die tiefste Komponente über beiden istApp. - Den Suchtext braucht nur
CatalogView. Er soll aber erhalten bleiben, wenn jemand ein Ding öffnet und zurückkehrt. In dieser Zeit verschwindetCatalogView, darum gehört auch der Suchtext zuApp. - Die Favoriten brauchen die Karten und der Kopf, also wieder
App. - Die Felder der Anfrage braucht nur
ReservationForm, sie bleiben dort.
Wie Daten von App zu den Karten kommen, zeigt das Kapitel «Props». Wie eine Karte App meldet, dass jemand reservieren will, zeigt das Kapitel «Events und v-model».
Teile ohne Daten auslagern
Abschnitt betitelt „Teile ohne Daten auslagern“Am einfachsten lagerst du Teile aus, die nichts von aussen brauchen, den Kopf, den Fuss und die Öffnungszeiten. Schau dir an, wie das Beispiel die Seite zusammensetzt, und überleg dir, welche Komponenten es gibt, bevor du es öffnest.
App.vue
<script setup>import AppFooter from './AppFooter.vue'import AppHeader from './AppHeader.vue'</script>
<template> <AppHeader /> <main> <h1>Katalog</h1> <p>Hier steht bald wieder der Katalog.</p> </main> <AppFooter /></template>AppFooter.vue
<script setup>import OpeningHours from './OpeningHours.vue'</script>
<template> <footer> <OpeningHours /> <p>Bibliothek der Dinge im Lorraine-Quartier in Bern</p> </footer></template>AppHeader.vue
<template> <header class="site"> <a href="#/" class="brand">Lorraine Commons</a> </header></template>
<style scoped>.site { padding: 0.5rem 1rem; background: #1f6b4f;}.brand { font-weight: 700; color: #ffffff; text-decoration: none;}</style>OpeningHours.vue
<template> <section> <h2>Öffnungszeiten</h2> <p>Mittwoch 17 bis 19 Uhr</p> <p>Samstag 10 bis 13 Uhr</p> </section></template>App.vue ist kurz geworden und liest sich wie ein Inhaltsverzeichnis. AppFooter verwendet seinerseits OpeningHours, Komponenten lassen sich also beliebig tief verschachteln. Der Style wandert mit der Komponente in ihre Datei und gilt dank scoped nur dort.
Eine Karte für ein Ding
Abschnitt betitelt „Eine Karte für ein Ding“Die Karte eines Dings ist der nächste Kandidat, denn sie wiederholt sich für jedes Ding. Sie braucht aber Daten, den Namen, die Kategorie und die Verfügbarkeit. Ohne einen Weg, ihr diese Daten zu geben, steht ihr Inhalt fest im Template.
App.vue
<script setup>import ItemCard from './ItemCard.vue'</script>
<template> <h1>Katalog</h1> <div class="grid"> <ItemCard /> <ItemCard /> <ItemCard /> </div></template>
<style>.grid { display: grid; gap: 0.75rem; grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));}</style>ItemCard.vue
<template> <article class="card"> <h2>Bohrmaschine</h2> <p class="category">Werkzeug</p> </article></template>
<style scoped>.card { padding: 0.75rem 1rem; border: 1px solid #c8c8c8; border-radius: 8px;}h2 { margin: 0 0 0.25rem; font-size: 1.1rem;}.category { margin: 0; color: #5f5f5f;}</style>Alle drei Karten zeigen die Bohrmaschine. Die Komponente ist richtig ausgelagert, aber sie kann noch nichts Verschiedenes zeigen. Das ändert das nächste Kapitel.
Im Code
Abschnitt betitelt „Im Code“Arbeite mit dem Beispiel zum Kopf und zum Fuss. Lies zuerst die Dateien und schreib auf, was du erwartest, bevor du etwas änderst.
- Zeichne den Baum der Komponenten des Beispiels, bevor du den Playground öffnest. Stimmt er mit dem überein, was die Vorschau zeigt?
- Ändere die Öffnungszeit am Samstag. In wie vielen Dateien musst du dafür etwas ändern, und in welchen nicht?
- Lagere die Zeile mit der Adresse aus
AppFooterin eine neue KomponenteAppAddress.vueaus und binde sie im Fuss ein. - Überleg dir, warum
OpeningHoursinAppFootersteht und nicht direkt inApp. Wann wäreAppder bessere Ort?
Aufgaben
Abschnitt betitelt „Aufgaben“Die Aufgaben zerlegen eine verkürzte Fassung der App aus Teil 3. Du findest das Projekt im Kursrepo im Ordner exercises/04-components/breaking-down/de. Kopiere den Ordner, installiere die Pakete und starte die Tests.
npm installnpm testAufgabe Einstieg
Lagere den Kopf aus App.vue in die Komponente src/components/AppHeader.vue aus und binde sie in App.vue mit dem Alias @ ein. Die Seite sieht danach aus wie vorher.
Lösung
<template> <header class="site"> <a href="#/" class="brand">Lorraine Commons</a> <span class="tagline">Bibliothek der Dinge im Lorraine-Quartier</span> </header></template>
<style scoped>.site { display: flex; flex-wrap: wrap; gap: 0.25rem 1rem; align-items: baseline; margin: -1rem -1.5rem 1rem; padding: 0.75rem 1.5rem; background: #1f6b4f; color: #ffffff;}.brand { font-weight: 700; color: #ffffff; text-decoration: none;}</style>In App.vue stehen danach der Import import AppHeader from '@/components/AppHeader.vue' und an der Stelle des Kopfs <AppHeader />. Der Style des Kopfs gehört jetzt der Komponente und gilt dank scoped nur für sie.
Aufgabe Vertiefung
Lagere den Fuss in die Komponente AppFooter.vue aus. Die Öffnungszeiten darin werden zur eigenen Komponente OpeningHours.vue, die AppFooter verwendet. Danach enthält App.vue weder ein Element header noch ein Element footer.
Lösung
<script setup>import OpeningHours from './OpeningHours.vue'</script>
<template> <footer class="site-footer"> <OpeningHours /> <p>Lorraine Commons, Bern</p> </footer></template>
<style scoped>.site-footer { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #e2e2e2;}</style>OpeningHours.vue enthält die Überschrift und die beiden Zeilen mit den Zeiten aus App.vue. Das Template von App.vue besteht danach aus drei Teilen.
<template> <AppHeader /> <main> <h1>Katalog</h1> <label for="search">Suchen</label> <input id="search" v-model="query" type="search"> <ul class="catalog"> <li v-for="item in visibleItems" :key="item.id"> <span class="name">{{ item.name }}</span> <span class="category">{{ item.category }}</span> </li> </ul> </main> <AppFooter /></template>Aufgabe Knobelei
OpeningHours soll unter den Zeiten in einem Element mit der Klasse today sagen, ob heute offen ist. Am Mittwoch steht dort «Heute offen, 17 bis 19 Uhr», am Samstag «Heute offen, 10 bis 13 Uhr», an allen anderen Tagen «Heute geschlossen, nächste Öffnung am Mittwoch» oder «Heute geschlossen, nächste Öffnung am Samstag». new Date().getDay() liefert 0 für Sonntag bis 6 für Samstag.
Lösung
<script setup>// The opening days as numbers of getDay, 0 is Sunday, each with its hoursconst OPENING_DAYS = [ { day: 3, name: 'Mittwoch', hours: '17 bis 19 Uhr' }, { day: 6, name: 'Samstag', hours: '10 bis 13 Uhr' },]
const weekday = new Date().getDay()const today = OPENING_DAYS.find((entry) => entry.day === weekday)// After Saturday, Wednesday comes next againconst next = OPENING_DAYS.find((entry) => entry.day > weekday) ?? OPENING_DAYS[0]
const status = today ? `Heute offen, ${today.hours}` : `Heute geschlossen, nächste Öffnung am ${next.name}`</script>
<template> <section class="opening-hours"> <h2>Öffnungszeiten</h2> <p v-for="entry in OPENING_DAYS" :key="entry.day">{{ entry.name }} {{ entry.hours }}</p> <p class="today">{{ status }}</p> </section></template>Die Öffnungstage stehen einmal in einem Array, und die Zeilen mit den Zeiten entstehen mit v-for daraus. find sucht den heutigen Tag, und wenn keiner passt, den nächsten Öffnungstag in derselben Woche. Nach Samstag gibt es keinen solchen Tag mehr, darum folgt mit ?? wieder der Mittwoch. Hier gilt getDay mit der Zeitzone des Geräts, denn es geht um den heutigen Tag der Person, die die Seite öffnet. Die ganze Logik steckt in OpeningHours, und weder AppFooter noch App müssen davon wissen.
Typische Fehler
Abschnitt betitelt „Typische Fehler“| Fehler | Was hilft |
|---|---|
| Für jedes Element entsteht eine eigene Komponente. | Nur Teile mit einer eigenen Aufgabe auslagern, etwa Wiederholungen und Teile mit eigener Logik. |
Eine Komponente heisst Header oder Card. | Namen aus mehreren Wörtern wählen, etwa AppHeader und ItemCard. |
| Zwei Komponenten halten denselben Zustand, etwa den Suchtext, und laufen auseinander. | Jeden Zustand genau einer Komponente geben und von dort weiterreichen. |
| Der Suchtext geht verloren, wenn jemand ein Ding öffnet und zurückkehrt. | Den Zustand in eine Komponente legen, die beim Wechsel der Seite bestehen bleibt. |
Eine ausgelagerte Komponente importiert die Daten selbst, obwohl App sie schon lädt. | Daten von oben übergeben, das zeigt das Kapitel «Props». |
Ein KI-Assistent registriert jede Komponente mit app.component in main.js. | Komponenten dort importieren, wo sie gebraucht werden. So bleibt sichtbar, woher sie kommen, und der Build lässt Unbenutztes weg. |
Selbstcheck
Abschnitt betitelt „Selbstcheck“Selbstcheck
- Woran erkennst du, dass eine Komponente zu viel tut?
- Welche Präfixe verwendet der Kurs für Komponenten, die es nur einmal gibt, und für allgemeine Bausteine?
- Nach welcher Regel entscheidest du, welche Komponente einen Zustand besitzt?
- Warum besitzt
Appden Suchtext und nichtCatalogView?
Antworten
- Ihre Aufgabe lässt sich nicht in einem Satz ohne «und» beschreiben, ihr Template ist lang, und sie hält Zustand, der nichts miteinander zu tun hat.
Appfür Komponenten, die es nur einmal gibt, wieAppHeader, undBasefür allgemeine Bausteine, wieBaseDialog.- Der Zustand gehört in die tiefste Komponente, die über allen Komponenten liegt, die ihn brauchen.
CatalogViewverschwindet, wenn jemand ein Ding öffnet. Der Suchtext soll das überstehen, darum liegt er inApp, die immer da ist.
Auftrag
Die Seite sieht aus wie vorher, und der Freiwillige findet sich in den Dateien schneller zurecht. Die Karte eines Dings ist schon eine eigene Komponente, zeigt aber für jedes Ding dieselben Angaben. Im nächsten Kapitel bekommt sie ihre Daten.
Texte und Abbildungen stehen unter CC BY-SA 4.0, der Code unter der MIT-Lizenz und die Daten des Kurses unter CC0 1.0. Die Schrift der Abbildungen ist Kalam unter der SIL Open Font License 1.1. Die Quellen liegen auf GitHub.