Empfehlung:
allgemeine Komponentendefinition in einer .vue-Datei:
<template>
...
</template>
<script>
...
</script>
<style scoped>
...
</style>
<div>
<button @click="imgId = 0">start</button>
<button @click="prevImg()">previous</button>
<img :src="imgUrl" alt="slideshow" />
<button @click="imgId++">next</button>
</div>
export default {
name: 'MySlideshow',
data() {
return { imgId: 0 };
},
computed: {
imgUrl() {
return `https://picsum.photos/200?image=${this.imgId}`;
},
},
methods: {
prevImg() {
if (this.imgId > 0) {
this.imgId--;
}
},
},
};
angegebene Properties und Methoden in einer Komponentendefition:
Einträge in data, methods, computed, ... sind im Skript als this.entryname
und im Template als entryname
verfügbar
State wird mittels der data
-Methode initialisiert
State-Einträge sind reaktiv: Vue kann reagieren, wenn sie sich ändern (und das Rendering der Komponente entsprechend aktualiseren)
Methoden sind Funktionen, die mit einer Komponente assoziiert sind
computed
können abgeleitete Daten berechnencomputed
werden automatisch aufgerufen, wenn sich eine Abhängigkeit ändertWie weiß Vue, wann Werte in computed aktualisert werden müssen?
Während dem ersten Berechnen eines computed-Wertes überprüft Vue, auf welche State-Einträge zugegriffen wird - diese zählen später als Trigger für die Aktualisierung
<div>A year has {{365 * 24}} hours.</div>
Kurzform:
<a :href="'https://en.wikipedia.org/wiki/' + topic">
some article
</a>
Langform:
<a v-bind:href="'https://en.wikipedia.org/wiki/' + topic">
some article
</a>
besondere Syntax für die class-Property:
<div :class="{todo: true, completed: isCompleted}">...</div>
besondere Syntax für die style-Property:
<div :style="{padding: '8px', margin: '8px'}">...</div>
<div :style="{color: completed ? 'grey' : 'black'}">
...
</div>
Kurzform:
<button @click="alert('hello')">Say Hello</button>
Langform:
<button v-on:click="alert('hello')">Say Hello</button>
Event-Modifier:
Verhindern des "Default-Verhaltens":
<form @submit.prevent="handleSubmit()">...</form>
Zugriff auf das Event-Objekt mittes $event
:
<button @click="handleClick($event)">handle event</button>
<div v-if="request.loading">Loading...</div>
<div v-else-if="request.error">Error while loading</div>
<div v-else>
<h1>Results</h1>
...
</div>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{todo.title}}
</li>
</ul>
Jedes wiederholte Element sollte eine lokal eindeutige key-Property haben (für Effizienz)
Explizites Two-way Binding für Inputs:
<input :value="title" @input="title = $event.target.value" />
Kurzform:
<input v-model="firstName" />
Standardmäßig wird Whitespace zwischen Elementen nicht von Vue gerendert
<strong>no</strong> <em>space</em>
"Erzwingen" eines Leerzeichens:
<strong>with</strong>{{ " " }}<em>space</em>
Aufgaben:
Wir erstellen eine Todo-Anwendung mit der folgenden Funktionalität:
<div>
<h1>Todo</h1>
<form @submit.prevent="addTodo()">
<input v-model="newTitle" />
<button type="submit">add</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.title }}
</li>
</ul>
</div>
export default {
name: 'TodoList',
data() {
return {
newTitle: '',
todos: [
{ id: 1, title: 'groceries', completed: false },
{ id: 2, title: 'taxes', completed: true },
],
};
},
methods: {
onSubmit() {
this.addTodo();
this.newTitle = '';
},
addTodo() {
let maxId = 0;
for (let todo of this.todos) {
maxId = Math.max(maxId, todo.id);
}
this.todos.push({
id: maxId + 1,
title: this.newTitle,
completed: false,
});
},
},
};
Plugins für Chrome, Firefox und Edge
Funktionen:
mögliche Zugänge beim Suchen nach Problemen:
Open-Source-Entwicklungsumgebung
Unabhängig vom eigentlichen Visual Studio
F1 oder Ctrl + Shift + P: Befehlspalette
Beispiele für Befehle:
Via File - Preferences - Settings
Eingeteilt in User Settings und Workspace Settings
Empfehlungen:
\n
Weitere Möglichkeiten:
Extensions-Sidebar öffnen: fünftes Symbol auf der linken Seite
mögliche Extensions:
Linter mit mehr Funktionalität als der Standard-Linter von VS Code
Tool zum Initialisieren eines Vue-Projekts
Installieren und Ausführen:
npm install -g @vue/cli
vue create my-app
oder direktes Ausführen:
npx @vue/cli create my-app
Erstellt eine einfache Vue-App, die als Ausgangspunkt verwendet werden kann
viele Aspekte können automatisch eingerichtet werden:
Konfiguration:
damit der Build bei ESLint-Fehlern nicht fehlschlägt:
neue Datei vue.config.js:
module.exports = {
lintOnSave: 'warning',
};
Überprüfe Imports auf Fehler:
npm install eslint-plugin-import
in der eslint-Konfiguration unter "extends"
, füge "plugin:import/recommended"
hinzu
im Projektordner:
npm run serve
: Startet den lokalen Entwicklungsservernpm run build
: Erstellt einen Build (zum Deployment)Ein Vue-Projekt kann auf einem beliebigen statischen Hosting-Service gehostet werden
build mit Vue CLI:
npm run build
Minifizierter Build wird im dist-Ordner erstellt
einfache Möglichkeiten, um das Deployment zu testen (ohne Login):
Eigene Komponenten müssen im components-Eintrag der Elternkomponente aufgelistet werden:
import TodoItem from './TodoItem';
import AddTodo from './AddTodo';
export default {
components: { TodoItem, AddTodo },
// ...
};
Eigene Komponenten können im Template typischerweise auf zwei Arten geschrieben werden:
empfohlen:
<TodoItem />
<VBtn>foo</VBtn>
alternativ:
<todo-item />
<v-btn>foo</v-btn>
für Vue 2:
für Vue 3:
Hinzufügen von Vuetify 2 zu einem neuen Vue CLI-Projekt:
npx @vue/cli add vuetify
(Bemerkung: überschreibt App.vue und manche andere Dateien)
Stelle sicher, dass der ganze Komponentenbaum in App.vue von einer <v-app>
-Komponente umfasst wird:
<template>
<v-app>
...
</v-app>
</template>
Beispiel für die Verwendung von Komponenten:
<v-btn color="primary" type="submit">add</v-btn>
<v-text-field v-model="newTitle" label="new title" />
<v-app>
<v-app-bar app>
<v-toolbar-title>Todo</v-toolbar-title>
</v-app-bar>
<v-main>
...
</v-main>
<v-footer app>Todo App by Marko</v-footer>
</v-app>
v-container: responsive Container mit horizontalen Margins (Abständen)
<v-main>
<v-container>
...
</v-container>
</v-main>
Vuetify bietet ein Grid-System mit 12 Spalten (ähnlich wie bootstrap)
zwei Spalten mit gleicher Breite:
<v-row>
<v-col>foo</v-col>
<v-col>bar</v-col>
</v-row>
Konfigurieren der Breite:
<v-row>
<v-col :cols="12" :sm="6" :md="3">
<v-text-field v-model="newTitle" label="new title" />
</v-col>
<v-col>
<v-btn color="primary" type="submit">Add</v-btn>
</v-col>
</v-row>
:cols="12"
- nutzt auf den kleinsten Bildschirmen alle 12 Spalten:sm="6"
- nutzt auf Bildschirmen der Mindestgröße small 6 Spalten:md="3"
- nutzt auf Bildschirmen der Mindestgröße medium 3 Spaltenmögliche Dateinamen für Komponenten-Dateien:
Komponenten-Namen sollten immer mehrere Wörter sein (um sie von standard HTML-Elementen zu unterscheiden)
Wiederholung: wichtige Properties in Komponentendefinitionen:
eine Komponenten kann ein Interface definieren, mit dem sie mit ihrer Elternkomponente interagiert:
Beispiele:
<ProgressBar percentage={75} color="lightgreen" />
<Rating :stars="3" />
Props einer Komponente müssen in der Konfiguration aufgelistet werden:
export default {
props: ['value', 'color'],
};
Zugriff: gleich wie für data
, methods
, ...
Props aus mehreren Wörtern: üblicherweise mixedCase in JavaScript, kebab-case in Templates
props: ['greetingText']
<WelcomeMessage greeting-text="hi" />
events have a name and potentially contain some data ("payload(s)")
Beispiel: Verarbeiten von Komponenten-Events
<StarsRating
:value="rating1"
@update:value="rating1 = $event"
/>
<TodoItem
:todo="todo"
@delete="deleteTodo($event)"
@update:completed="changeTodoCompleted($event)"
/>
Auslösen eines Events aus einer Unterkomponente:
this.$emit('eventname', payload);
Beispiel: StarsRating
:
Wiederholung: Inputs:
explizites two-way-Binding für Inputs:
<input value="title" @input="title = $event.target.value" />
Kurzversion für two-way-Binding:
<input v-model="firstName" />
eigene Komponenten in Vue 3:
<StarsRating
:modelValue="rating1"
@update:modelValue="rating1 = $event"
/>
Kurzform:
<StarsRating v-model="rating1" />
eigene Komponenten in Vue 2:
<StarsRating :value="rating1" @input="rating1 = $event" />
Kurzform:
<StarsRating v-model="rating1" />
Aufgabe: Teile die Todo-Anwendung in kleinere Unterkomponenten auf (z.B. TodoList, TodoItem, AddTodo)
Aufgabe: Extrahiere wiederverwendbare Komponenten, die hauptsächlich für das Styling verwendet werden - z.B. eine Button
-Komponente oder eine TextInput
-Komponente
Wir können Komponenten erstellen, die wie folgt verwendet werden können:
<DialogModal type="error">
<p>Changes could not be saved</p>
<button>OK</button>
</DialogModal>
Template für DialogModal
:
<div :class="{dialog: true, dialogType: type}">
<slot></slot>
</div>
Composition API: Komponentenlogik wird in einer setup
-Methode definiert
Composition API verglichen mit dem Options API:
options API:
export default {
name: 'TodoApp',
data() {
return {
todos: [],
newTitle: '',
};
},
methods: {
/*...*/
},
computed: {
/*...*/
},
};
composition API:
import { ref, reactive, computed } from 'vue';
export default {
name: 'TodoApp',
setup() {
const todos = reactive([]);
const newTitle = ref('');
function addTodo() {
// ...
}
const numActive = computed(
() => todos.filter((t) => !t.completed).length
);
return { todos, newTitle, addTodo, numActive };
},
};
mögliche Umstrukturierung des Codes:
function useTodos() {
// manages todos
}
export default {
name: 'TodoApp',
setup() {
const newTitle = ref('');
const { todos, addTodo, numActive } = useTodos();
return { todos, addTodo, numActive, newTitle };
},
};
VueUse: Sammlung von vordefinierten Vue Composition-Funktionen:
https://github.com/vueuse/vueuse
Beispiele:
zwei Mechanismen:
ref
für primitive / unveränderbare Daten (string, number, ...)reactive
für Objekte, Arrays, ...Initialisierung eines refs:
const newTitle = ref('');
Lesen / Schreiben aus dem Skript:
const a = newTitle.value;
newTitle.value = 'foo';
Lesen / Schreiben aus dem Template:
<div>new title is {{newTitle}}</div>
<button @click="newTitle = 'foo'">set to foo</button>
<div>
<h1>Todo</h1>
<form @submit.prevent="addTodo()">
<input v-model="newTitle" />
<button type="submit">add</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.title }}
</li>
</ul>
</div>
import { ref, reactive } from 'vue';
export default {
name: 'TodoApp',
setup() {
const newTitle = ref('');
const todos = reactive([
{ id: 1, title: 'groceries', completed: false },
{ id: 2, title: 'taxes', completed: true },
]);
function onSubmit() {
addTodo(newTitle.value);
newTitle.value = '';
}
function addTodo(title) {
const maxId = Math.max(0, ...todos.map((t) => t.id));
todos.push({
id: newId,
title: newTitle.value,
completed: false,
});
}
return { newTitle, todos, onSubmit };
},
};
Erstelle eine Slideshow-Komponente / -Anwendung, die Bilder wie das folgende darstellt:
https://picsum.photos/300/200?image=0
Die setup
-Methode erhält zwei Argumente: props
und context
Beispiel: Rating-Komponente:
export default {
name: 'StarsRating',
props: ['value'],
setup(props, context) {
const ariaLabel = computed(
() => `${props.value} out of 5 stars`
);
const onStarClick = (id) => {
context.emit('change', id);
};
return { ariaLabel, onStarClick };
},
};
Promises: moderne Möglichkeit, asynchronen Code zu verwenden:
async
/ await
.then()
moderne Möglichkeiten, um Netzwerkanfragen zu senden:
fetch()
(in Browsern inkludiert)asynchrone Funktion, die Todos von einem API lädt:
// todosApi.ts
async function fetchTodos(): Promise<Array<Todo>> {
const url = 'https://jsonplaceholder.typicode.com/todos';
const res = await fetch(url);
const apiTodos = await res.json();
// convert data format (don't include userId)
const todos = apiTodos.map((todo) => ({
id: todo.id,
title: todo.title,
completed: todo.completed,
}));
return todos;
}
Asynchrone Funktion, die Wechselkursdaten von einem API lädt:
async function fetchExchangeRate(
from: string,
to: string
): Promise<number> {
const res = await fetch(
`https://api.exchangerate.host/convert?from=${from}&to=${to}`
);
const data = await res.json();
return data.result;
}
}
Wenn sich Komponenten-Props bzw State ändern:
"Main Effect": Komponente wir mit aktuellen Daten (neu) gerendert
mögliche "Side Effects": Auslösen von API-Abfragen, Speichern von Daten, Explizite Änderungen am DOM, ...
Typischerweise möchten wir Side Effects auslösen, wenn sich bestimmte Props oder State geändert haben oder wenn die Komponente zum ersten Mal eingebunden wurde
typische Fälle von Side Effects:
typische Auslöser für Side Effects:
Side Effects im Options API:
created
watch
-FunktionSide Effects im Composition API (Variante 1):
setup
watch
-FunktionSide Effects im Composition API (Variante 2):
watchEffect
-Funktion: Läuft beim ersten Rendering und bei Änderungen von überwachten WertenOptions API: Komponentenmethoden, die bei bestimmten Lifecycle-Events aufgerufen werden:
created
mounted
updated
destroyed
Composition API: äquivalente Funktionen:
created
→ aufnehmen in setup
mounted
→ onMounted
updated
→ onUpdated
destroyed
→ onUnmounted
Laden von Daten beim ersten Einbinden einer Komponente (Options API):
export default {
// ...
async created() {
const res = await fetch(
'https://jsonplaceholder.typicode.com/todos'
);
const todos = await res.json();
this.todos = todos;
},
};
Die watchEffect
-Funktion kann verwendet werden, um side effects auszulösen, wenn die Komponente zum ersten Mal eingebunden wird oder wenn sich Props oder State geändert haben
Beispiel: Laden von SpaceX-Startdaten beim ersten Einbinden, oder wenn sich launchNr
geändert hat
function setup() {
const launchNr = ref(1);
const loading = ref(true);
const launchData = reactive({ name: null, date: null });
watchEffect(async () => {
loading.value = true;
const res = await fetch(
`https://api.spacexdata.com/v3/launches/${launchNr.value}`
);
const data = await res.json();
loading.value = false;
launchData.name = data.mission_name;
launchData.date = data.launch_date_utc;
});
return { launchNr, launchData };
}
Ãœbungen:
vollständiger Code für eine SpaceX-Launch-Komponente:
<template>
<article>
<button @click="launchNr--">prev</button>
<button @click="launchNr++">next</button>
<div v-if="loading">loading...</div>
<div v-else>
<h1>{{ launchData.name }}</h1>
<p>{{ launchData.date }}</p>
<img :src="launchData.patch" :key="Math.random()" />
</div>
</article>
</template>
<script>
import { reactive, ref, watchEffect } from 'vue';
export default {
setup() {
const launchNr = ref(1);
const loading = ref(true);
const launchData = reactive({
name: null,
date: null,
patch: '',
});
watchEffect(async () => {
loading.value = true;
const res = await fetch(
`https://api.spacexdata.com/v3/launches/${launchNr.value}`
);
const data = await res.json();
loading.value = false;
launchData.name = data.mission_name;
launchData.date = data.launch_date_utc;
launchData.patch = data.links.mission_patch_small;
});
return { launchNr, launchData, loading };
},
};
</script>
Laden aus / Speichern in localStorage (Counter-Komponente):
import { ref, watchEffect } from 'vue';
export default {
setup() {
// try to load
const countStored = Number(
localStorage.getItem('count')
);
const count = ref(countStored || 0);
// persist to localStorage if count changes
watchEffect(() => {
localStorage.setItem('count', count.value);
});
return { count };
},
};