KI-generierten Code prüfen
Auftrag
Das Team am Tresen braucht eine Übersicht der Ausleihen, die überfällig sind.
Als Team am Tresen möchte ich die überfälligen Ausleihen sehen und Mitglieder daran erinnern, damit die Dinge rechtzeitig zurückkommen.
Die neue Freiwillige hat einen KI-Assistenten um die Komponenten gebeten. Der Code sieht vernünftig aus, die Listen bleiben aber leer, und die Konsole meldet Fehler. Die Akzeptanzkriterien lauten:
- Der Code folgt Vue 3 mit
<script setup>, ohne Muster aus Vue 2. - Keine Komponente ändert eine Prop, und keine verliert die Verbindung zu ihrem Zustand.
- Eine Checkliste und Tests fangen solche Fehler ab, bevor der Code auf
mainkommt.
Lernziele
Nach diesem Kapitel kannst du
- typische Fehler in vorgeschlagenem Vue-Code erkennen und ihre Quelle in der Dokumentation finden,
- Muster aus Vue 2 in Vue 3 übersetzen,
- verlorene Reaktivität und geänderte Props finden und beheben,
- Listen mit Schlüssel und ohne
v-ifnebenv-forschreiben, - eine Checkliste, Tests und einen Linter als Sicherheitsnetz einsetzen.
Code prüfen, den du nicht geschrieben hast
Abschnitt betitelt „Code prüfen, den du nicht geschrieben hast“Kapitel 0.1 hat festgehalten, dass Assistenten bei Vue oft Muster aus älteren Versionen schreiben. Was du committest, verantwortest du, egal wer es geschrieben hat. Lies vorgeschlagenen Code darum wie in einem Review, Zeile für Zeile, und such für jedes Muster, das dir fremd ist, die Stelle in der Dokumentation. Im Projekt hältst du in der KI-Deklaration fest, was du geprüft und geändert hast, und im Screencast zeigst du eine Stelle, an der ein Vorschlag falsch war.
Muster aus Vue 2
Abschnitt betitelt „Muster aus Vue 2“Vue 2 hat laut vuejs.org am 31. Dezember 2023 sein Lebensende erreicht. Der Migrationsleitfaden nennt, was sich in Vue 3 geändert hat. Statt new Vue startet createApp eine App, und globale Funktionen wie Vue.component hängen an der App. Filter gibt es nicht mehr, eine Funktion oder ein berechneter Wert übernimmt ihre Arbeit. $on, $off und $once sind entfernt, ebenso der Modifier .native und $listeners, und Vue.set braucht es nicht mehr.
Manches davon fällt nicht sofort auf. Ein Filter wie {{ loan.until | date }} kompiliert in Vue 3 ohne Fehler, denn Vue liest den Ausdruck als JavaScript, und | ist dort ein bitweises Oder. Auf der Seite steht dann eine Zahl statt eines Datums.
Die Options API in script setup
Abschnitt betitelt „Die Options API in script setup“Code in <script setup> läuft wie der Inhalt von setup(), und dort gibt es laut Vue-Dokumentation kein this. Ein vorgeschlagenes this.loans in einem berechneten Wert scheitert darum zur Laufzeit. Felder wie data, methods oder computed in einem zweiten <script> gehören zur Options API und passen nicht zu <script setup>. defineProps und defineEmits muss niemand importieren. Wer es trotzdem tut, bekommt beim Kompilieren die Warnung, dass defineProps ein Makro des Compilers sei. Props ohne Typen wie defineProps(['loans']) taugen laut Styleguide nur für Prototypen.
Vuex und Vue CLI
Abschnitt betitelt „Vuex und Vue CLI“Für geteilten Zustand empfiehlt die Vue-Dokumentation Pinia, Vuex ist im Wartungsmodus. Ein Vorschlag mit new Vuex.Store, commit und mapState gehört darum in einen Store mit defineStore, wie in Kapitel 7.2. Ebenso ist Vue CLI im Wartungsmodus, ein neues Projekt entsteht mit create-vue und Vite. Anleitungen mit vue create oder npm run serve stammen aus dieser Zeit.
Verlorene Reaktivität
Abschnitt betitelt „Verlorene Reaktivität“Wer ein reaktives Objekt oder einen Store direkt zerlegt, bekommt Kopien der Werte. Die Seite zeigt dann den Stand vom Zerlegen und folgt keiner Änderung mehr, wie Kapitel 5.1 und 7.2 gezeigt haben.
<script setup>import { reactive } from 'vue'
const state = reactive({ count: 0 })// Taking the object apart copies the number, the copy no longer follows the stateconst { count } = state
function add() { state.count++}</script>
<template> <button type="button" @click="add">Dazuzählen</button> <p>state.count: {{ state.count }}</p> <p>count: {{ count }}</p></template>toRefs hält die Verbindung für ein reaktives Objekt, storeToRefs für einen Store. Props darf man seit Vue 3.5 zerlegen, laut Vue-Dokumentation braucht watch eine zerlegte Prop aber als Funktion, etwa watch(() => loans, …).
Geänderte Props
Abschnitt betitelt „Geänderte Props“Daten fliessen laut Vue-Dokumentation nur von der Elternkomponente zum Kind. Ändert ein Kind seine Prop, warnt Vue in der Konsole, und die Änderung wirkt nicht.
App.vue
<script setup>import { ref } from 'vue'
import Counter from './Counter.vue'
const count = ref(0)</script>
<template> <p>Gezählt: {{ count }}</p> <Counter :count="count" /></template>Counter.vue
<script setup>const props = defineProps({ count: { type: Number, required: true } })
function add() { props.count++}</script>
<template> <button type="button" @click="add">Dazuzählen</button></template>Das Kind meldet stattdessen ein Event, und die Elternkomponente ändert ihren Zustand. Braucht das Kind nur einen Startwert, legt es mit ref eine eigene Kopie an.
Listen mit Schlüssel
Abschnitt betitelt „Listen mit Schlüssel“Der Styleguide verlangt zu jedem v-for einen key. Ohne Schlüssel oder mit dem Index als Schlüssel verwendet Vue die Elemente nach ihrer Position wieder. Verschwindet ein Eintrag, wandert der Zustand in den Elementen, etwa ein getippter Text, zum nächsten Eintrag.
<script setup>import { ref } from 'vue'
const notes = ref([ { id: 1, title: 'Pfand zurückgeben' }, { id: 2, title: 'Zelt trocknen' }, { id: 3, title: 'Bänke reservieren' },])
function remove(id) { notes.value = notes.value.filter((note) => note.id !== id)}</script>
<template> <ul> <li v-for="(note, index) in notes" :key="index"> <label :for="`note-${note.id}`">{{ note.title }}</label> <input :id="`note-${note.id}`" type="text"> <button type="button" @click="remove(note.id)">Entfernen</button> </li> </ul></template>v-if und v-for gehören laut Styleguide nie an dasselbe Element. In Vue 3 wertet Vue v-if zuerst aus, die Variable der Schleife gibt es dann noch nicht. Ein berechneter Wert filtert die Liste vorher, oder ein <template v-for> umschliesst das Element mit v-if.
Eine Checkliste
Abschnitt betitelt „Eine Checkliste“Diese Fragen helfen bei jedem vorgeschlagenen Stück Vue-Code.
- Läuft der Code mit Vue 3 und
<script setup>, ohnethis, Filter,$onundnew Vue? - Haben Props Typen, und ändert keine Komponente ihre Props?
- Bleiben Werte aus Stores und reaktiven Objekten verbunden?
- Hat jedes
v-foreinen eindeutigenkey, und steht keinv-ifam selben Element? - Verwendet der Code Pinia, Vue Router und Vite statt Vuex, Vue CLI und älterer Funktionen?
- Zeigt die Seite Laden, Fehler und leere Listen, wenn Daten von einem Server kommen?
- Gibt es einen Test für das, was der Code tun soll, und scheitert er, wenn der Code falsch ist?
Tests und Linter als Sicherheitsnetz
Abschnitt betitelt „Tests und Linter als Sicherheitsnetz“Tests prüfen, was der Code tun soll, und Kapitel 8.4 zeigt, wie du prüfst, ob sie einen Fehler auch bemerken. Ein Linter findet manche Fehler schon beim Tippen. create-vue richtet mit der Option «Linter» ESLint mit eslint-plugin-vue ein, wie Kapitel 3.2 gezeigt hat. Seine Regeln für Vue 3 melden unter anderem geänderte Props mit vue/no-mutating-props, fehlende Schlüssel mit vue/require-v-for-key, v-if neben v-for mit vue/no-use-v-if-with-v-for und Muster aus Vue 2 mit Regeln wie vue/no-deprecated-filter. Verlorene Reaktivität erkennt er nur mit Regeln, die nicht in den Vorgaben stehen, und ob eine Liste richtig sortiert, weiss nur ein Test.
Dem Assistenten Kontext geben
Abschnitt betitelt „Dem Assistenten Kontext geben“Ein Assistent kennt dein Projekt nicht. Nenn ihm die Versionen, etwa Vue 3 mit <script setup>, Pinia und Vue Router, und die Ordnung deines Projekts, und lass dir erklären, was sein Vorschlag tut. Was du nicht erklären kannst, übernimmst du nicht. Passwörter, Schlüssel und Personendaten gehören nie in eine Anfrage an einen Assistenten.
In deinem Projekt
Abschnitt betitelt „In deinem Projekt“In der KI-Deklaration deines README steht für jedes Werkzeug, wofür du es eingesetzt hast, welche Teile betroffen sind und was du am Ergebnis geprüft oder geändert hast. Notier dir während der Arbeit Stellen, an denen ein Vorschlag falsch war, mit dem Commit, der ihn korrigiert. Der Screencast fragt nach einer solchen Stelle.
Im Code
Abschnitt betitelt „Im Code“Arbeite mit den Beispielen dieses Kapitels. Lies den Code und schreib auf, was du erwartest, bevor du etwas änderst.
- Klick im Beispiel mit der geänderten Prop auf «Dazuzählen». Was meldet die Konsole, und was zeigt die Zahl? Schreib das Kind so um, dass es ein Event meldet.
- Tipp im Beispiel ohne Schlüssel einen Text in die zweite Zeile und entferne die erste. Wo steht dein Text jetzt? Ändere den Schlüssel auf
note.idund wiederhole es. - Klick im Beispiel mit der verlorenen Reaktivität mehrmals. Welche Zahl folgt? Ersetze das Zerlegen durch
toRefs(state). - Starte im Übungsprojekt dieses Kapitels die Tests, bevor du etwas änderst. Welche Meldungen deuten auf Muster aus Vue 2?
Aufgaben
Abschnitt betitelt „Aufgaben“Die Aufgaben korrigieren den Code, den der Assistent für das Team vorgeschlagen hat. Du findest das Projekt im Kursrepo im Ordner exercises/08-architecture/ai-review/de. Kopiere den Ordner, installiere die Pakete und starte die Tests.
npm installnpm testAufgabe Einstieg
OverdueList.vue mischt <script setup> mit this und einem Filter aus Vue 2, und ihre Props haben keine Typen. Schreib die Komponente in Vue 3 neu. Sie zeigt die Ausleihen, deren Tag vor today liegt, die älteste zuerst, mit einem Datum wie in der Sprache der Seite üblich.
Lösung
<script setup>import { computed } from 'vue'
const props = defineProps({ loans: { type: Array, required: true }, today: { type: String, required: true },})
// The loans whose day has passed, the oldest first. There is no this in <script setup>.const overdue = computed(() => props.loans.filter((loan) => loan.until < props.today).toSorted((a, b) => a.until.localeCompare(b.until)),)
// Vue 3 has no filters, a function does the same jobfunction formatDate(value) { return new Date(value).toLocaleDateString('de-CH', { timeZone: 'UTC' })}</script>
<template> <h2>Überfällige Ausleihen</h2> <ul class="overdue"> <li v-for="loan in overdue" :key="loan.id">{{ loan.name }}, fällig seit {{ formatDate(loan.until) }}</li> </ul></template>Aufgabe Vertiefung
Die Seite des Teams zerlegt den Store direkt und zeigt darum keine Ausleihen. LoanRow.vue ändert ihre Prop reminded, statt den Klick auf «Erinnern» zu melden. Behebe beides, sodass der Store festhält, wer erinnert wurde.
Lösung
const loansStore = useLoansStore()// storeToRefs keeps the connection to the store, actions come straight from the storeconst { loans, reminded } = storeToRefs(loansStore)const today = new Date().toISOString().slice(0, 10)
loansStore.load()<script setup>defineProps({ loan: { type: Object, required: true }, reminded: { type: Boolean, default: false },})
// A prop belongs to the parent, the row only reports the clickconst emit = defineEmits(['remind'])</script>
<template> <li> {{ loan.name }} <span v-if="reminded" class="reminded">erinnert</span> <button type="button" @click="emit('remind', loan.id)">Erinnern</button> </li></template>Aufgabe Knobelei
NoteList.vue hat v-if und v-for am selben Element, keinen Schlüssel und ändert beim Entfernen die Prop notes. Zeig nur die offenen Notizen, gib jeder Zeile einen eindeutigen Schlüssel und lass die Prop unverändert.
Lösung
<script setup>import { computed, ref } from 'vue'
const props = defineProps({ notes: { type: Array, required: true } })
// A copy of the first state, the list changes here and leaves the prop aloneconst list = ref(props.notes.map((note) => ({ ...note })))
// v-if and v-for never on the same element, a computed value filters firstconst openNotes = computed(() => list.value.filter((note) => !note.done))
function remove(id) { list.value = list.value.filter((note) => note.id !== id)}</script>
<template> <h2>Notizen</h2> <ul class="notes"> <!-- The id as key keeps every field with its note when another one disappears --> <li v-for="note in openNotes" :key="note.id"> <label :for="`note-${note.id}`">{{ note.title }}</label> <input :id="`note-${note.id}`" type="text"> <button type="button" @click="remove(note.id)">Entfernen</button> </li> </ul></template>Typische Fehler
Abschnitt betitelt „Typische Fehler“| Fehler | Was hilft |
|---|---|
Ein berechneter Wert in <script setup> liest this und scheitert. | Props mit defineProps übernehmen und props.loans lesen. |
Eine Interpolation mit | zeigt eine Zahl statt Text. | Filter gibt es in Vue 3 nicht mehr. Eine Funktion aufrufen. |
| Eine Liste aus einem Store bleibt leer, obwohl der Store geladen hat. | Den Store mit storeToRefs zerlegen oder über den Store lesen. |
| Die Konsole meldet, dass ein Ziel nur lesbar ist. | Eine Komponente ändert ihre Prop. Ein Event melden. |
| Ein getippter Text springt nach dem Entfernen in eine andere Zeile. | Einen eindeutigen key verwenden, nicht den Index. |
Eine Anleitung beginnt mit vue create oder installiert Vuex. | create-vue und Pinia verwenden. |
Selbstcheck
Abschnitt betitelt „Selbstcheck“Selbstcheck
- Woran erkennst du Code, der aus Vue 2 stammt?
- Warum bleibt eine Liste aus einem direkt zerlegten Store leer?
- Was passiert, wenn eine Liste den Index als Schlüssel hat und ein Eintrag verschwindet?
- Welche Fehler findet ein Linter, und welche nur ein Test?
Antworten
- An
new Vue,thisin<script setup>, Filter,$on,.native,Vue.setoder an Vuex und Vue CLI. - Das Zerlegen kopiert die Werte. Ersetzt der Store später seine Liste, zeigt die Seite noch die alte.
- Vue verwendet die Elemente nach ihrer Position wieder, und ihr Zustand landet bei einem anderen Eintrag.
- Der Linter findet Muster wie geänderte Props, fehlende Schlüssel und Funktionen aus Vue 2. Ob die App das Richtige tut, zeigt nur ein Test.
Auftrag
Das Team sieht die überfälligen Ausleihen und erinnert die Mitglieder daran. Damit ist die App für den Kurs fertig. Das Kompetenzraster zeigt dir, wo du nach Teil 8 stehst, und dein eigenes Projekt wendet die Kapitel dieses Teils an.
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.