Zum Inhalt
wannacode

Die erste Komponente

Kurz gesagt: Eine Vue-Komponente ist ein Template mit Daten: Es beschreibt, wie der Bildschirm für diese Daten aussieht, und Vue ändert auf der Seite nur den Unterschied.

Problem

Das Feld verliert den Fokus

Das Modul „JavaScript in der Seite“ endete mit einem Warenkorb, den ein einziges render() aus den Daten zeichnet. Hier ruft jede Änderung des Mengenfelds render() auf. Der Kunde möchte 15 Flaschen Milch: Er klickt ins Feld, löscht die 2 und tippt 15. Probier es aus.

HTML, CSS, JS

Nach dem Löschen ist das Feld leer und hat keinen Fokus mehr: Die 1 und die 5 gehen ins Leere. render() hat das Feld schon beim ersten Tastendruck durch ein neues ersetzt. Das neue Feld sieht gleich aus, ist aber ein anderes Element, und der Fokus lag auf dem alten. Einzelne Stellen von Hand anzufassen, vergisst Stellen. Alles neu zu zeichnen, verliert den Fokus. Es gibt einen dritten Weg.

Die Idee

Die Oberfläche als Funktion der Daten

render() hatte eine gute Idee: Der Bildschirm wird nur aus den Daten gebaut. Bei denselben Daten ist der Bildschirm derselbe, egal, was vorher darauf stand. Der Fehler lag im letzten Schritt: render() entfernt und erzeugt die Elemente selbst.

Die Arbeit wird deshalb in zwei Teile geteilt:

  • Du schreibst eine Funktion, die sagt, wie der Bildschirm für diese Daten aussieht. Sie fasst die Seite nicht an; sie gibt eine Beschreibung zurück: Hier ist ein <main>, darin ein <h1> mit dem Text „Der Warenkorb“, dann ein Feld mit dem Wert 2.
  • Die Bibliothek hält das echte DOM. Beim ersten Mal baut sie es aus der Beschreibung. Bei jeder Änderung der Daten nimmt sie die neue Beschreibung, vergleicht sie mit der alten und ändert auf der Seite nur den Unterschied.

Eine solche Funktion heißt Komponente. Kurz gesagt: Der Bildschirm ist eine Funktion der Daten, und die Komponente ist diese Funktion. React und Vue machen das auf unterschiedliche Weise. Die Idee ist dieselbe, und die Wahl zwischen den beiden ist Sache des Teams, nicht einer „besseren“ Bibliothek.

Sieh es dir an

Die erste Komponente

In Vue ist eine Komponente eine .vue-Datei (Single File Component) mit bis zu drei Teilen: <script setup> mit den Daten und Funktionen, <template> mit der Beschreibung des Bildschirms und optional <style>.

<script setup>
const product = { name: 'Frischmilch', price: 1.6, stock: 35 };
</script>
<template>
<main>
<h1>{{ product.name }}</h1>
<p>Preis: {{ money(product.price) }}</p>
</main>
</template>
  • Alles, was <script setup> deklariert (Variablen, Funktionen, importierte Komponenten), sieht das Template.
  • {{ product.name }} ist ein JavaScript-Ausdruck in doppelten geschweiften Klammern. Sein Ergebnis wird zu Text auf der Seite.
  • Der Name der Komponente ist der Name der Datei: App.vue ist die Komponente App.
  • Die Sandbox setzt App selbst auf die Seite. In einem echten Projekt macht das eine Zeile in main.js: createApp(App).mount('#app').
Vue

Ändere den Preis oder den Lagerbestand im Objekt product und klick auf „Ausführen“. Die Beschreibung des Bildschirms fasst du nicht an: Der Bildschirm folgt den Daten.

Die Idee

Die Regeln des Templates

Das Template ist HTML, aber Vue kompiliert es zu JavaScript und liest darin eigene Zeichen. Deshalb hat es eigene Regeln.

  • {{ Ausdruck }} im Text: <h1>{{ product.name }}</h1>.
  • : vor einem Attribut bedeutet „der Wert ist ein Ausdruck“: <img :src="product.image" :alt="product.name" />. :alt ist die Kurzform von v-bind:alt. Ohne : ist der Wert der Text „product.name“, Buchstabe für Buchstabe. {{ }} funktioniert in einem Attribut nicht: alt="{{ product.name }}" ist der Text {{ product.name }}.
  • @ vor einem Ereignis: @click="add" (Kurzform von v-on:click). Ein kurzer Ausdruck geht auch: @click="count++" oder @click="add(product)". Vue packt ihn in eine Funktion, die beim Klick läuft.
  • class und for bleiben class und for, wie in HTML.
  • Mehrere Wurzeln sind erlaubt: Das Template kann ein <header> und ein <main> nebeneinander haben.
  • Tags ohne Inhalt dürfen mit /> geschlossen werden: <input />, <ProductCard />.

Probier es aus

Finde den Fehler

Ein Klick auf „Hinzufügen“ schreibt nichts in die Konsole.

Wähle die Zeile mit dem Fehler.

Probier es aus

Ergänze

Der Screenreader soll den Produktnamen als Beschreibung des Bildes vorlesen.

<img :src="product.image" ="product.name" />

Die Idee

Komponenten aus Komponenten

Die Shop-Seite hat Teile: den oberen Bereich mit dem Menü, die Produktkarte, den Warenkorb. Jeder Teil wird zu einer Komponente in einer eigenen Datei, und eine größere Komponente nutzt sie als Tags. Dann ist App keine lange Funktion, sondern ein paar Zeilen, die sagen, was auf der Seite steht.

ShopHeader.vue
<template>
<header>
<a href="/">Der Shop</a>
<nav>…</nav>
</header>
</template>
App.vue
<script setup>
import ShopHeader from './ShopHeader.vue';
import ProductCard from './ProductCard.vue';
</script>
<template>
<ShopHeader />
<main>
<ProductCard />
</main>
</template>

Das Template sieht eine importierte Komponente direkt. Ohne den import weiß Vue nicht, was ShopHeader ist: Die Konsole meldet „[Vue warn]: Failed to resolve component: ShopHeader“, und ein unbekanntes Tag bleibt auf der Seite. Der Name beginnt per Konvention mit einem Großbuchstaben (PascalCase), damit er sich von HTML-Tags unterscheidet. Vue akzeptiert auch <shop-header />.

Probier es aus

Setz die Seite zusammen

ShopHeader und ProductCard sind in ihren Dateien fertig. Setz App zusammen: ShopHeader oben und die Produktkarte in main, unter der Überschrift „Milchprodukte“.

Vue

Die Idee

Nur der Unterschied

Hier ist der Warenkorb aus dem Problem als Komponente.

ref('2') ist das Gedächtnis der Komponente: ein Wert, den Vue beobachtet. Wenn er sich ändert, zeichnet Vue die Komponenten neu, die ihn lesen. Das ist das Thema des Moduls „Zustand“. Hier ist es nur dafür da, dass sich etwas ändern kann.

Vue

Mach dasselbe wie im Problem: Klick ins Milchfeld, lösch die 2 und tipp 15. Das Feld behält den Fokus und das Getippte, und die Summe wird 25,20 €. Die Komponente beschreibt bei jedem Tastendruck den ganzen Bildschirm, wie render(). Der Unterschied ist, dass die Seite nicht neu gebaut wird. Die Bibliothek sieht, dass das Feld in der neuen Beschreibung an derselben Stelle mit demselben Tag steht, und ändert nur seinen Wert und den Text der Summe.

Sieh es dir an

Sieh dir den Vergleich an

<script setup>
import { ref } from 'vue';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const milk = ref('2');
const bread = ref('1');
</script>
<template>
<main>
<h1>Der Warenkorb</h1>
<label>
Frischmilch
<input type="number" :value="milk" @input="milk = $event.target.value" />
</label>
<label>
Weißbrot
<input type="number" :value="bread" @input="bread = $event.target.value" />
</label>
<p>Gesamt: {{ money(Number(milk) * 1.6 + Number(bread) * 1.2) }}</p>
</main>
</template>
Schritt für Schritt, in Worten
  1. Der Kunde hat die 2 gelöscht und 1 getippt: Die Daten sagen milk = "1". In der Mitte steht die letzte Beschreibung, die die Komponente zurückgegeben hat; rechts steht das DOM, mit dem Fokus im Milchfeld.
  2. Der Kunde tippt 5. @input schreibt "15" in milk: Die Daten ändern sich, und Vue weiß, dass das Template milk liest.
  3. Vue hat das Template zu einer Render-Funktion kompiliert. Weil sich milk geändert hat, führt Vue sie noch einmal aus, und sie gibt eine neue Beschreibung zurück: einen Baum aus einfachen Objekten.
  4. Die Bibliothek vergleicht die neue Beschreibung mit der vorigen. Dieselben Tags in derselben Reihenfolge und zwei Unterschiede: der Wert des Feldes und der Text der Summe.
  5. Nur die beiden Unterschiede erreichen das DOM. Das <input> ist dasselbe Element wie vorher, deshalb bleiben Fokus und Cursor darin.
  6. Du beschreibst den ganzen Bildschirm für die gegebenen Daten, wie bei render(). Die Bibliothek findet, was sich geändert hat, und fasst nur das an. Beide Bibliotheken tun das, jede auf ihre Weise.

Die Beschreibung ist billig: Es ist ein Baum aus einfachen Objekten, und die Komponente macht ihn jedes Mal neu. Das DOM ist der teure Teil, deshalb fasst die Bibliothek dort nur die Unterschiede an. Du schreibst wie bei render(): den ganzen Bildschirm aus den Daten. Der Preis von render() (verlorener Fokus, tausende neue Elemente) fällt nicht an.

Genauer, für Neugierige

Virtuelles DOM. Die Beschreibung heißt oft virtuelles DOM, und ihr Vergleich mit der alten heißt Abgleich (Reconciliation in React, Patching in Vue). Ein Knoten der Beschreibung ist ein Objekt wie { type: 'h1', props: { children: 'Der Warenkorb' } }.

Die beiden Wege. Das Template wird zu einer Render-Funktion kompiliert, die die Knoten der Beschreibung zurückgibt. Der Compiler markiert, welche Teile sich nie ändern, und der Vergleich überspringt sie. Vue merkt sich, welche Daten jede Komponente liest (Reaktivität), und führt nur die Komponenten erneut aus, deren Daten sich geändert haben. In beiden Fällen entstehen am Ende eine neue Beschreibung, ein Vergleich und ein paar Änderungen im DOM.

Text ist Text. {{ product.name }} setzt Text ein, wie textContent. Ein Name wie <img src=x onerror=alert(1)> erscheint als Zeichen und wird nicht zu einem Tag. HTML aus einem String kommt nur mit v-html hinein, und der Name mahnt zur Vorsicht.

Options API. In älterem Code ist eine Komponente ein Objekt: export default { data() { … }, methods: { … } }. Es funktioniert noch, aber neuer Code nutzt <script setup> (Composition API).

v-model. Für das Paar :value und @input gibt es die Kurzform v-model. Sie gehört in die Lektion über Formulare.

Übung

Die Produktkarte

In ProductCard steht das Objekt product (Weißkäse). Bau die Karte: den Namen als Überschrift der Ebene zwei, „Preis: 7,90 €“ (mit money), „Auf Lager: 12 Stk.“ und einen Button „Hinzufügen“, dessen Name für den Screenreader „Hinzufügen Weißkäse“ ist. Ein Klick auf den Button schreibt „Hinzugefügt: Weißkäse“ in die Konsole (die Funktion add ist fertig). Setz dann App zusammen: ShopHeader oben und die Karte in main, unter der Überschrift „Milchprodukte“.

Vue

Test

Teste dich

1. Eine Komponente wird ein zweites Mal mit denselben Daten gezeichnet. Was zeigt sie?
2. Welche Zeile im Template zeigt den Produktnamen als alt des Bildes?
3. Warum bleibt der Fokus in der Komponente im Feld, während render() aus dem vorigen Modul ihn verliert?
4. App.vue nutzt <ShopHeader />, und die Konsole meldet Failed to resolve component: ShopHeader. Was fehlt?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·