08: Vue.js Deep Dive
Vue kann alles und ist dabei auch noch so elegant – aber trotz seiner Einsteigerfreundlichkeit, ist Vue ein komplexes Framework mit vielen Funktionen und Eigenschaften. In dieser Session werden wir tief in die Welt von Vue eintauchen, aber selbst dann könnt ihr in eurer Freizeit noch viel weiter Tauchen, um wirklich alles zu lernen.
Inhalt
- Was ist npm und wie verwendet man das?
- Was bedeuten diese Versionsnummern?
- Was sind Webpack, Babel & Co?
- Was ist JS-Fatigue und wie beugt man dem vor?
- Wie sind Vue-Projekte aufgebaut?
- Exkurs: Was genau sind jetzt Progressive Web Apps?
- Vue-Instanzen und ihre Optionen
- Components im Detail
- Spezielle Attribute is, ref und key
- Wie Funktioniert v-bind mit class und style?
- Wie baut man coole Animationen?
- Wann braucht man Routing und State-Management?
- Wie benutzt man das?
- Bonus: Was sind CSS Pre-Prozessoren?
- Praxis
npm
und wie verwendet man das?
Was ist Je größer eure Projekte werden, desto mehr Libraries werdet ihr verwenden. Und
auch wenn ihr nur ein Framework und seine zugehörigen Module verwendet, wird es
schnell unübersichtlich und anstrengend, all eure <script>
-Importe in eurer
Index.html zu verwalten. Außerdem müsst ihr eure Frameworks und Libraries immer
erst suchen und auf eurem Computer speichern, oder einen CDN-Link verwenden, um
sie einzubinden, was alles nicht besonders ideal ist.
Deshalb existieren sogenannte Paketmanager und ganz besonders für unseren
Kontext, der npm
-Paketmanager. Das steht für node package mangager
und wurde
bei der Installation von Node.js mitinstalliert.
Ihr könnt euch einen Paketmanager wie einen AppStore vorstellen: ihr sucht, was
ihr braucht und der Paketmanager installiert euch das dann und hält es aktuell.
npm
ist in dieser Metapher also ein AppStore für JavaScript Libraries und
Frameworks, den ihr über euer Terminal verwenden könnt.
Aber npm
macht viel mehr als nur die benötigten Skripte, auch Pakete genannt,
herunterzuladen und abzuspeichern und dann stupide alle Updates zu installieren,
die herauskommen. npm
gibt euch die Möglichkeit, die Abhängigkeiten
(auch Dependencies genannt) eurer Anwendungen zu verwalten.
Erklärung: Was ist eine Abhängigkeit?
Eine Abhängigkeit ist ein Programm, oder eine Funktion, von der euer eigener Code abhängig ist, d.h. ohne das oder die er nicht ausgeführt werden kann. Allgemein beschreibt dieser Begriff externe Libraries und auch Frameworks, von denen euer eigenes Programm abhängig ist.
Dazu hat jedes Projekt eine besondere Datei namens package.json
, in der Meta-
Informationen wie die Version, der Name und mehr abgelegt sind. Zu diesen Meta-
Informationen gehören auch die Dependencies und die Dev-Dependencies, also alle
Abhängigkeiten, die euer Projekt benötigt, wenn es veröffentlicht wird und
welche es zur Entwicklung benötigt. Anhand dieser Datei weiß npm
immer, was es
herunterladen soll, auch wenn ihr euer Projekt einmal in einen neuen Ordner oder
auf einen neuen Computer verschiebt und dabei den node_modules
-Ordner vergesst,
in dem npm
alles abspeichert, was es herunterlädt.
Hier ein Auszug der package.json
dieser Website:
{
"dependencies": {
"gridsome": "^0.7.0",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"typeface-inter": "^3.12.0"
},
"devDependencies": {
"@gridsome/vue-remark": "^0.1.10",
"@vue/cli-plugin-eslint": "^4.3.1",
"@vue/cli-service": "^4.3.1",
"@vue/eslint-config-airbnb": "^5.0.2",
"babel-eslint": "^10.1.0",
"eslint": "^6.8.0",
"eslint-plugin-vue": "^6.2.2",
"gridsome-plugin-remark-shiki": "^0.3.1",
"remark-emoji": "^2.1.0",
"stylus": "^0.54.7",
"stylus-loader": "^3.0.2"
}
}
Wie ihr seht, hängt diese Website von einer Reihe von externen Paketen ab, nicht
nur Frameworks wie gridsome
und Libraries wie nprogress
, sondern auch CSS
Dateien und Schriften. Zur Entwicklung verwendet sie außerdem Pakete wie
eslint
und @vue/eslint-config-airbnb
, uvm.
Wie genau all das funktioniert sprengt den Rahmen dieses Kurses, aber im
Praxisteil werden wir sehen, wie man mit npm
arbeiten kann. Wenn ihr euch
einmal ansehen möchtet, welche Pakete es auf npm
so gibt, könnt ihr das auf
der offiziellen Webseite tun.
Achtung:
Npm lädt die Libraries und Frameworks nur herunter, fügt sie aber nicht in euer HTML ein, das heißt ihr könnt die Pakete nicht benutzen, ohne sie vorher zu importieren, wie man das macht sehen wir gleich.
An dieser Stelle noch ein nützlicher Hinweis: auf npm
gibt es viele Node.js
Programme, die man immer wieder in verschiedenen Projekten nutzen kann, z.B.
einen lokalen Webserver, oder Programme wie die Vue CLI. Oftmals wird empfohlen,
diese Programme global zu installieren, aber ihr könnt diese Programme auch
einfach ausführen indem ihr npx programmName
in eurem Terminal ausführt.
So könnt ihr zum Beispiel mit serve
schnell, und ohne etwas fest installieren zu müssen, einen lokalen Webserver
starten, um eure Projekte zu testen:
$ cd /pfad/zum/projekt
$ npx serve
Was bedeuten diese Versionsnummern?
Wenn ihr mit Paketen von npm
arbeitet, werdet ihr sehen, dass sie immer mit
einer Versionsnummer versehen sind. Diese gibt an, welche Version eines Pakets
ihr verwenden wollt. Ein @
bedeutet hierbei „genau diese Version“, während
das am häufigsten verwendete ^
für „alle Versionen mit dieser Versionsnummer,
oder einer höheren Patch-Version steht.
Diese Versionsnummern bestehen immer aus drei Zahlen und folgen so gut wie immer dem Semver-Standard. In diesem Standard bedeutet die erste Zahl die sogenannte Major-Version, die mittlere Zahl die Minor-Version und die letzte Zahl die Patch- Version.
Wenn sich die Major-Version ändert, gibt es sogenannte „breaking changes“, d.h.
Code, der mit einer vorherigen Version geschrieben wurde funktioniert unter
Umständen nicht mehr mit dieser neuen Version und muss von euch aktualisiert
werden. npm
wird niemals Pakete mit einer höheren Major-Version installieren,
wenn ihr eure Pakete mit npm upgrade
aktualisiert. Wollt ihr zu einer höheren
Major-Version wechseln, müsst ihr das Paket von Hand mit dem Zusatz @latest
neu installieren:
# Vue in Version 2.6.6 ist installiert
# Vue 3.0 wird veröffentlicht
$ npm outdated # gibt veraltete Pakete an
Package Installed Wanted Current
vue 2.6.6 2.6.6 3.0.0
$ npm upgrade # aktualisiert alle Pakete, aber nicht, wenn die Major-Version sich ändert
$ npm install vue@latest # zwingt npm vue auf die neueste Version zu aktualisieren
Achtung:
Der Wechsel auf eine neue Major Version zwingt euch so gut wie immer, Teile eures Codes zu verändern. Viele Pakete stellen hierzu sogenannte „Migration- Guides“ bereit, die ihr auf den jeweiligen Webseiten oder im Git-Repo des Pakets findet. Auch kleinere Versionswechsel werden für gewöhnlich in einem Changelog oder auf der „Releases“-Seite dokumentiert.
Updates auf neue Minor-Versionen zeigen hingegen an, dass neue Features verfügbar geworden sind, aber keine breaking changes existieren. Ihr könnt also problemlos neue Minor-Versionen installieren, um auch von etwaigen Sicherheitsaktualisierungen zu profitieren.
Die Patch-Versionsnummer wird immer dann erhöht, wenn eine neue Version eines Pakets veröffentlicht wird, das nur Probleme behebt.
Zwar ist es kein offizieller Teil des Semver-Standards, aber oftmals verwenden Entwickler eine 0 als Major-Version, um anzudeuten, dass ein Programm noch nicht fertig ist und jede neue Version breaking changes enthalten kann. Passt also mit solchen Paketen ganz besonders auf und lest die Changelogs bevor ihr sie aktualisiert!
Tipp:
Auch eure Apps haben eine Versionsnummer und ihr solltet diese immer aktuell
halten, wenn ihr eine neue Version veröffentlicht. Npm macht das einfach: ihr
könnt einfach npm version [patch|minor|major]
eingeben und eure
Versionsnummer wird sich automatisch am angegebenen Level erhöhen. Ihr könnt
das ganze aber natürlich auch manuell in der package.json
vornehmen.
Was sind Webpack, Babel & Co?
Wie bereits angemerkt, installiert npm
nur Pakete und ihre Abhängigkeiten und
aktualisiert sie, wenn ihr es dazu anweist, aber macht diese noch nicht
innerhalb eures Codes verfügbar. Außerdem werdet ihr feststellen, dass der
Inhalt eures node_modules
-Ordners sehr groß ist und es keinen Sinn machen
würde all diese Daten mit eurem Code zu veröffentlichen.
Deshalb gibt es Technologien wie Webpack, die diese ganzen Pakete zusammenfassen und komprimieren, damit am Ende alles in ein paar wenige JS-Dateien gesteckt werden kann. Webpack sorgt in einem Vue Projekt auch dafür, dass am Ende alle Skripte, die ihr verwendet, an der richtigen Stelle in der Index.html stehen. Es ist ein mächtiges Programm, das noch sehr viel mehr macht, aber für den Anfang braucht ihr das noch nicht zu wissen. Ihr werdet nur in den seltensten Fällen damit interagieren müssen, denn die Vue CLI nimmt euch hier die ganze Arbeit ab.
Babel auf der anderen Seite ist ein Tool, dass es euch erlaubt, modernes
JavaScript zu schreiben, ohne an ältere Browser denken zu müssen. Babel
übersetzt wann immer möglich neue Sprachfunktionen wie async/await
in
JavaScript, das auch von älteren Browsern verstanden wird, die mit der neuen
Syntax noch nicht umgehen können. Auch für Babel gilt: wisst einfach, dass es
existiert, aber ignoriert es für den Anfang einfach.
PostCSS ist etwas wie Babel, nur für CSS. Ihr erinnert euch vielleicht daran,
dass ihr für moderne CSS Features wie transform
immer auch die Browser-
Spezifischen Varianten wie -webkit-transform
etc. angeben müsst. PostCSS macht
das automatisch für euch. Ihr gebt in eurem Code transform
an und PostCSS
generiert dann all die Varianten für euch.
Aber wann passieren all diese Übersetzungen und Konvertierungen? Diese geschehen
im sogenannten „build step“ – also dann, wenn ihr eure App
veröffentlichungsfertig macht. In einem Vue-Projekt passiert das, indem ihr
npm run build
ausführt. Dann erhaltet ihr einen Ordner (standardmäßig dist/
),
den ihr so eins zu eins auf euren Webserver laden könnt. Wie genau das dann
aussieht sehen wir uns in Session 10 an.
Mir ist bewusst, dass all diese Werkzeuge am Anfang verwirrend erscheinen können, aber dank der Vue CLI müsst ihr euch noch nicht mit ihnen befassen. Ihr solltet nur wissen, dass sie existieren, damit ihr nicht vollkommen verloren seid, wenn ihr in irgendeiner Dokumentation über sie stolpert.
Was ist JS-Fatigue und wie beugt man dem vor?
Vielleicht seid ihr jetzt schon etwas überwältigt, aber es wird noch schlimmer: jeden Tag werden neue Versionen von Paketen, neue Libraries, neue Frameworks und neue JavaScript Funktionen veröffentlicht. Über Nacht kann sich die komplette JavaScript-Welt völlig auf den Kopf stellen. Jedes Mal wenn wir an unserer App arbeiten wollen und überprüfen, für welche Pakete es Updates gibt, werden wir wahrscheinlich sehen, dass sich etwas verändert hat und es eine neue Version, oder einen neuen Weg gibt, ein Problem zu lösen. Je mehr ihr zu diesen Themen online lest, desto mehr Artikel werdet ihr zu diesen Themen sehen.
Da ist es ganz normal, dass man sich schnell überwältigt und ausgelaugt fühlt. Das geht nicht nur euch als Anfängern so, sondern auch uns erfahreneren Entwicklern. Es gibt sogar einen eigenen Begriff dafür: „JS Fatigue“.
Ich persönlich gehe damit um, indem ich mich zwar über alles mögliche Informiert halte, aber voll und ganz auf das Vue.js-Ökosystem konzentriere. Ich versuche so wenige Libraries wie möglich zu verwenden und überlege immer zweimal, ob es sich lohnt eine neue Abhängigkeit zu einem Projekt hinzuzufügen.
Aber auch für Vue gibt es tausende verschiedene Tools und Wege. Deshalb bleibe ich bei den Offiziellen: vue, vue-cli, vue-router und vuex. Ich verwende keine externen Component-Bibliotheken, keine komplizierten Frameworks, die auf Vue aufbauen (von Gridsome mal abgesehen). Ich versuche so minimalistisch wie möglich zu arbeiten.
Ja, die Vue CLI arbeitet mit Webpack, Babel, PostCSS, etc., aber deswegen benutze ich ja die Vue CLI: damit ich mich nicht mit diesen komplexen Tools herumschlangen muss. Es ist also okay, wenn ich sie nicht 100%ig beherrsche und verstehe. Wenn sie einmal Probleme machen, kann ich immer noch nach spezifischen Fehlern googeln.
Ich habe es schon mehrmals gesagt, aber möchte es auch hier nochmal unterstreichen: Programmieren lernt man, indem man es macht. Also stürzt euch ins Getümmel, macht Fehler und lernt aus ihnen. Es sind die Erfahrungen, die am Ende zählen, nicht, ob ihr auswendig wisst, wie man eine Webpack-Konfiguration schreibt.
Wie sind Vue-Projekte aufgebaut?
Vue-Projekte, die mit der CLI generiert werden unterscheiden sich etwas von
Web-Projekten, wie ihr sie bisher kennengelernt habt. Wenn ihr die Vue CLI
aufruft, um ein Projekt zu starten, werdet ihr eine Reihe von Fragen beantworten,
anhand derer die CLI euren Projektordner aufsetzt und mit npm
Dependencies
installiert. Wie genau das abläuft ist im Praxisteil beschrieben.
Einer der wesentlichsten Unterschiede zu Web-Projekten, wie ihr sie bisher kennt,
ist, dass ihr den Ordner, in dem euer Projekt lebt nicht einfach veröffentlichen
könnt wie ihr es bisher getan habt. Wie weiter oben schon angesprochen, muss
eure Anwendung erst „verpackt“ werden, bevor sie veröffentlicht werden kann, und
um das einfacher zu machen, legt die Vue CLI euch ein Skript an, dass ihr mit
npm run build
ausführen könnt. Erst dann erhaltet ihr einen Ordner, den ihr in
eurem Webspace ablegen könnt (standardmäßig heißt dieser Ordner 'dist').
Da es aber umständlich wäre nach jeder Änderung die App neu zu verpacken und
dann in einem Browser zu öffnen, legt euch die CLI noch ein zweites Skript an,
das serve
-Skript. Wenn ihr das ausführt, wird eure App automatisch nach jeder
Änderung neu verpackt und gleich auch noch über einen Live-Server lokal auf
eurem Rechner gehostet. Konkret bedeutet das für euch, dass ihr einfach in eurem
Browser auf die ausgegebene Adresse gehen könnt (normalerweise localhost:8080
)
und eure App seht, aber nicht nur das, nach jeder Änderung aktualisiert sich
diese lokale Version automatisch und ihr erhaltet zudem eine Überlagerung mit
eventuellen Fehlern, die euer Linter erkennt.
Diejenigen von euch, die die Live-Preview von Brackets schon einmal benutzt haben, werden sich gleich wie Zuhause fühlen. Nur mit weniger Fehlern und seltsamen Aktualisierungsschwierigkeiten. 😉
Das bedeutet allerdings auch, dass ihr daran denken müsst, den Preview-Server zu
starten, indem ihr in eurem Terminal npm run serve
eingebt, bevor ihr anfangt
zu arbeiten!
Die Ordnerstruktur, die die Vue CLI für eure Projekte generiert sieht mehr oder weniger immer folgendermaßen aus:
name-eurer-app
├───node_modules
├───public
│ ├───img
│ │ └───icons (nur wenn PWA aktiv ist)
│ ├───favicon.ico
│ ├───index.html
│ └───robots.txt
├───src
│ ├───assets
│ │ └───logo.png
│ ├───components
│ │ └───HelloWorld.vue
│ ├───router (nur wenn vue-router aktiv ist)
│ │ └───index.js
│ ├───store (nur wenn vuex aktiv ist)
│ │ └───index.js
│ ├───views
│ │ ├───Home.vue
│ │ └───About.vue
│ ├───App.vue
│ ├───main.js
│ └───registerServiceWorker.js (nur wenn PWA aktiv ist)
├───package.json
├───README.md
└───…andere Dateien…
Ich werde im Praxisteil näher auf diese Struktur eingehen, aber allgemein gilt,
dass ihr hauptsächlich im „src“-Ordner arbeiten werdet, denn dort lebt euer Code,
alles andere sind nur Metadaten und Konfigurationsdateien. Die Dateien im Ordner
„public“ werden beim verpacken eurer App in den „dist/“-Ordner kopiert, weshalb
sich hier Dateien wie robots.txt
und index.html
befinden, die später auf der
obersten Ebene sein müssen.
In der main.js
-Datei des „src“ Ordners wird die Root-Instanz von Vue
initialisiert, es ist also ein guter Ort, um globale Daten wie Stylesheets,
Fonts, etc. zu importieren. Ganz ähnlich ist App.vue
das Hauptcomponent eurer
App, hier könnt ihr zum Beispiel globale Navigationselemente unterbringen, die
auf jedem „Screen“ eurer App zu sehen sein sollen.
Im „Components“-Ordner liegen alle eure Components ab, und im „Views“-Ordner alle Components, die einen „Screen“ darstellen – die Templates aus dem Atomic Design, wenn ihr euch noch daran erinnert. Diese Trennung ist natürlich nur eine Konvention, ihr könntet auch alles im Ordner „Components“ speichern, aber es wäre dann weniger übersichtlich.
Tipp:
Dank Webpack könnt ihr beim Import immer @
benutzen, um eine
Referenz auf euren „src“-Ordner zu erhalten. Wenn ihr also in einer View ein
Component importieren möchtet, müsst ihr keinen umständlichen relativen Pfad
schreiben, sondern könnt einfach import HelloWorld from '@/components/HelloWorld.vue'
benutzen.
Es steht euch natürlich frei, eigene Ordner anzulegen, wie ihr wollt, aber diese Grundstruktur ist schon recht solide und bietet euch Platz für fast alles, was ihr benötigt.
Exkurs: Was genau sind jetzt Progressive Web Apps?
Wir haben jetzt schon mehrfach den Begriff „PWA“ gesehen, aber nie eindeutig definiert, was genau denn nun eine PWA von einer normalen Website unterscheidet.
Eine PWA ist im Grunde genommen eine besondere Form einer Website, die bestimmte Kriterien erfüllt und sich dadurch eher wie eine native App verhält als eine herkömmliche Website. Im Wesentlichen sind diese Kriterien:
- Offline-Funktionalität durch Installation eines Service-Workers, d.h. eines Skripts, dass unter Anderem Netzwerkanfragen abfangen und von einem Cache ausliefern kann
- Installierbarkeit durch eine Manifest-Datei, die den Namen, das Icon, und mehr für die App festlegt
- Auslieferung über einen sicheren Kontext, d.h. über eine https-Verbindung
Durch die Aktivierung des PWA-Plugins beim Erstellen eines Projekts mit der Vue
CLI wird automatisch ein Skript erstellt, dass zwei der nötigen Kriterien für
die App erfüllt: beim Ausführen des build
-Skriptes wird automatisch eine
manifest.json
-Datei generiert und im Hauptverzeichnis unserer verpackten App
abgelegt, sowie alle nötigen Vorkehrungen getroffen, damit die App einen Service
Worker registrieren kann, wenn sie über HTTPS ausgeliefert wird.
Wir müssen uns also nur darum kümmern, dass für unsere Domain HTTPS aktiv ist
und schon ist unsere App eine installierbare PWA. Konfigurieren können wir das
genaue Verhalten dieser App über die vue.config.js
-Datei, sowie die
registerServiceWorker.js
-Datei – aber das wird erst dann wirklich nötig, wenn
ihr kurz vor der Veröffentlichung eurer Apps steht.
Weitere Kriterien, die eine PWA ausmachen:
- Sie sind progressiv, d.h. sie funktionieren auf jedem Gerät und in jedem Browser, können aber bestimmte Features bestimmten Browsern vorenthalten, die diese Features unterstützen, z.B. die Share-API
- Sie sind responsiv, d.h. sie passen sich der Größe des Displays an und bieten eine Nutzererfahrung, die für das verwendete Gerät passend ist
- Sie sind app-like, d.h. wie native Apps gestaltet
Es gibt noch einige weitere Kriterien, aber da sich die Definition ständig weiterentwickelt, sind dies die wichtigsten und die, auf die ihr euch konzentrieren solltet.
Vue-Instanzen und ihre Optionen
In der letzten Session haben wir bereits gesehen, dass jede Vue-Anwendung mit dem Erstellen einer Vue-Instanz beginnt. Alles was Vue betrifft spielt sich innerhalb dieser Instanz ab, und da sie der Startpunkt ist, wird sie auch Root- Instanz, also Wurzel-Instanz genannt und hat gegenüber weiteren Instanzen, die innerhalb angelegt werden einige Besonderheiten. Auf diese Unterschiede werden wir eingehen, wenn wir tiefer in Components einsteigen.
Die Root-Instanz
Die Vue CLI generiert den Code für die Root-Instanz in der main.js
-Datei:
// Hier werden alle Module importiert, die für die Instanz wichtig sind
// Ihr könnt hier auch andere Dinge wie ein globales CSS-Stylesheet importieren
import Vue from 'vue'; // das ist ein Modul-Import, d.h. es wird über npm von node_modules importiert
import App from './App.vue'; // das sind Importe von Dateien im src-Ordner, wie man am ./ sieht
import './registerServiceWorker'; // das sind Importe von Dateien im src-Ordner, wie man am ./ sieht
import router from './router'; // das sind Importe von Dateien im src-Ordner, wie man am ./ sieht
import store from './store'; // das sind Importe von Dateien im src-Ordner, wie man am ./ sieht
// Hier kann Vue global konfiguriert werden, standarmäßig wird nur eine Warnung
// in der Konsole deaktiviert, dass man sich im Development-Modus befindet, der
// nicht so optimiert ist wie der Production-Modus.
// Alle verfügbaren Optionen findet ihr hier: https://vuejs.org/v2/api/#Global-Config
Vue.config.productionTip = false;
// Hier wird unsere Root-Instanz erstellt. Da wir nicht von Außen darauf
// zugreifen wollen, speichern wir sie auch nicht in einer Variablen.
new Vue({
router, // wir übergeben unsere Router Instanz, die wir oben importiert haben
store, // wir übergeben unsere Vuex Instanz, die wir oben importiert haben
render: (h) => h(App), // hier sagen wir, dass wir App.vue als unser Root-Component verwenden wollen
}).$mount('#app'); // hier wird die Root-Instanz an das DOM gehängt (könnte auch über el: '#app' geschehen)
Components sind auch nur Instanzen
Ein wichtiger Punkt ist, dass Components auch nur Vue-Instanzen sind. Sie sind allerdings keine Root-Instanzen, da sie innerhalb der Root-Instanz existieren. Während es nur eine Root-Instanz geben kann, kann diese beliebig viele Sub- Instanzen, also Components enthalten, wobei sich die Components selbst ebenfalls beliebig oft wiederholen können.
Bildlich könnt ihr euch das so vorstellen: ihr habt nur eine App, das ist eure Root-Instanz, aber die App kann beliebig viele Todo-Components enthalten.
Da Components Vue-Instanzen sind, bedeutet das, dass sie (bis auf wenige Ausnahmen) die selben Optionen übergeben bekommen können wie die Root-Instanz. Wie ihr in den vorhergehenden Beispielen gesehen habt, werden diese Optionen immer als ein Objekt übergeben. Was genau in diesem Objekt stehen kann, werden wir uns jetzt ansehen.
Data
Schon in den Beispielen in der letzten Session haben wir die data
-Option
kennengelernt. In diesem Objekt definiert ihr alle Daten, die ihr später
innerhalb der Vue-Instanz verwenden wollt. Vue nimmt sich diese Daten und
präpariert sie so, dass sie die Ansicht (View) selbst aktualisieren, wenn sich
ihre Werte ändern, d.h. dass sie reactive werden.
Achtung:
Ihr könnt die Daten benennen, wie ihr wollt, aber es gibt zwei Ausnahmen: beginnt der Name mit einem `_` oder `$` wird er von Vue ignoriert, da Vue diese Präfixe benutzt, um interne Daten zu markieren. Ihr könnt so zum Beispiel mit `this.$el` auf das HTML-Element zugreifen, das die Instanz kontrolliert.
Die Daten, die ihr in das data
-Objekt schreibt, sollten reine Daten sein,
also keine Browser-API-Objekte wie document
, ö.Ä. Außerdem könnt ihr das Data-
Objekt im Nachhinein nicht mehr verändern, bzw. Änderungen sind nicht mehr
reactive, initialisiert also alle Daten, die ihr benötigen werdet, auch wenn
ihre Werte noch nicht feststehen. Falls ihr den Wert noch nicht kennt, übergebt
einfach null
, oder false
:
new Vue({
data: {
foo: 1,
bar: 2,
createdAt: null, // wir wissen noch nicht, wann die Instanz erstellt wurde
},
created() { // diese Funktion wird aufgerufen, sobald die Instanz erstellt wurde
// jetzt können wir 'createdAt' auf den richtigen Wert setzen
// hätten wir 'createdAt' nicht in data definiert, wäre die Eigenschaft nicht reaktiv
this.createdAt = Date.now();
}
})
Computed
Wir haben in den Beispielen in der vergangenen Session schon gesehen, dass man innerhalb von Vue-Kontrollierten DOM-Elementen JavaScript ausführen kann:
<div id="app">
<p>Mein voller Name ist: {{vorname + ' ' + nachname}}</p>
</div>
Für kleinere Expressions ist das in Ordnung, aber sobald ihr einen vollen Namen
an mehreren Stellen braucht, oder die Expressions komplizierter werden, wird
euer Code schnell unübersichtlich und ineffizient. Um das zu beheben, gibt es in
Vue sogenannte „Computed Properties“, die ihr in der computed
-Option
definieren könnt.
Computed Properties sind Funktionen, die einen Wert zurückgeben. Sie werden
immer dann ausgeführt, wenn sich einer ihrer reaktiven Bestandteile ändert.
Innerhalb der Funktion habt ihr wie immer über this
Zugriff auf die aktuelle
Vue-Instanz:
<div id="app">
<p>Mein voller Name ist: {{fullName}}</p>
</div>
<script>
new Vue({
el: '#app',
computed: {
fullName() {
// wenn vorname oder nachname sich ändern, wird diese Funktion erneut
// ausgeführt und der Wert von fullName ändert sich ebenfalls
return `${this.vorname} ${this.nachname}`
}
}
data: {
nachname: 'Stadler',
vorname: 'Amadeus'
},
});
</script>
Aber Amadeus, könnte ich nicht einfach eine Method-Funktion schreiben und dann ausführen, die genau das gleiche bewirkt?
Ja, natürlich würde das auch gehen und das gleiche Ergebnis herbeiführen, aber
Computed Properties werden gecached, das bedeutet die Funktion wird nur dann
ausgeführt, wenn sich einer der reaktiven Werte innerhalb der Funktion ändert.
Die Methode wird immer ausgeführt. Wäre fullName
eine Methode, und würde fünf
Mal im Template auftauchen, dann würde die Methode auch fünf Mal ausgeführt
werden. Als Computed Property wird sie nur einmal ausgeführt und danach der Wert
fünf Mal ins Template eingetragen.
Achtung:
Computed Properties aktualisieren sich nur, wenn sich ihre reaktiven Bestandteile ändern! Das bedeutet, dass ihr nicht `return new Date()` schreiben und erwarten könnt, dass der Wert immer das aktuelle Datum ist, denn das ist keine reaktive Eigenschaft.
Wenn ihr also Daten habt, die auf anderen Daten basieren, benutzt eine Computed Property. Zum Beispiel, wenn ihr eine Liste von Todos ausgeben wollt, die erledigt sind:
<div id="app">
<ul>
<!-- Hier werden alle Todos auftauchen -->
<li v-for="todo in todos">{{todo.text}}</li>
</ul>
<ul>
<!-- Hier wird nur 'Vorlesung anhören' auftauchen -->
<li v-for="todo in doneTodos">{{todo.text}}</li>
</ul>
</div>
<script>
new Vue({
el: '#app',
computed: {
doneTodos() {
// gibt ein Array von todos zurück, bei denen done === true
return this.todos.filter((todo) => todo.done);
}
}
data: {
todos: [
{ text: 'Vorlesung anhören', done: true },
{ text: 'Praxis üben', done: false },
{ text: 'Abgabe rocken', done: false },
]
},
});
</script>
Watch
Für die meisten Zwecke reichen Computed Properties vollkommen aus. Aber falls
ihr einmal Methoden aufrufen wollt, wenn sich ein Wert ändert, oder die Werte
innerhalb eurer data
-Option ändern wollt, wenn sich ein Wert ändert, könnt
ihr die watch
-Option benutzen.
Hier könnt ihr Funktionen definieren, die genau den Namen haben wie die
Eigenschaft im data
-Objekt, die ihr beobachten wollt. Diese Funktionen
erhalten den neuen und den alten Wert der Eigenschaft als erstes und zweites
Argument und erlauben euch mit this
Zugriff auf die Instanz.
Für ein erweitertes Beispiel, was mit Watchern möglich ist, klickt bitte hier. Es ist aber unwahrscheinlich, dass ihr in diesem Kurs einen Watcher brauchen werdet.
Methods
Der methods
-Option könnt ihr ein Objekt übergeben, das eine Reihe von
Funktionen enthält, die ihr dann später in anderen Funktionen, oder als Reaktion
auf Events verwenden könnt. Diese Funktionen nennt man auch Methoden.
In diesem Beispiel verwenden wir die Methode add
, um den Wert des Counters zu
erhöhen, wenn der Button geklickt wird:
<div id="app">
<p>{{counter}}</p>
<button type="button" @click="add">Add</button>
</div>
<script>
new Vue({
el: '#app',
data: {
counter: 0
},
methods: {
add() {
this.counter += 1;
}
}
});
</script>
Components
Es gibt zwei Möglichkeiten, Components zu registrieren, die wir im nächsten
Abschnitt näher kennenlernen werden. Für den Augenblick reicht es zu wissen,
dass ihr mit der components
-Option eine Reihe von Components definieren könnt,
die nur für die aktuelle Instanz gültig sind.
import ComponentA from './ComponentA.vue'
new Vue({
components: {
// jetzt könnt ihr ComponentA als <component-a> oder <ComponentA> in eurem Template verwenden
ComponentA
},
// ...
});
Lifecycle-Hooks
Jede Vue-Instanz durchläuft einen Prozess, wenn sie initialisiert wird. Wir
können an bestimmten Punkten in diesem Prozess Code ausführen, indem wir
sogenannte „Hooks“ als Optionen an unsere Instanz übergeben. Diese Hooks sind
immer Funktionen, innerhalb derer ihr mit this
Zugriff auf die Vue-Instanz
habt.
Es gibt folgende Hook-Funktionen, die ihr übergeben könnt:
beforeCreate()
wird aufgerufen sobald die Instanz initialisiert wurde, aber bevor Daten reaktiv gemacht wurden und Events aktiviert werdencreated()
wird aufgerufen nachdem die Instanz erstellt wurde. Wichtig: die Instanz existiert bereits und ihr habt Zugriff auf alle Optionen wie Methoden und Daten, aber die Instanz wurde noch nicht an das DOM gehängt, d.h.this.$el
ist noch undefiniert und ihr könnt nicht mit dem DOM interagieren!beforeMount()
wird aufgerufen bevor die Instanz an das DOM gehängt wirdmounted()
wird aufgerufen nachdem die Instanz an das DOM gehängt wurde. Das bedeutet, dassthis.$el
nun eine Referenz auf das Element ist, das diese Instanz kontrolliert und ihr Zugriff auf DOM-APIs habt. Aber: es gibt noch keine Garantie dafür, dass alle Kind-Instanzen schon im DOM sind. Falls ihr also auf diese Zugreifen wollt, solltet ihr mitthis.$nextTick(() => {})
darauf warten, dass die komplette View gerendert wurde!beforeUpdate()
wird aufgerufen wenn Daten sich ändern und bevor das DOM mit diesen Änderungen aktualisiert wird. Hier solltet ihr EventListener entfernen, die ihr händisch an euren Elementen angebracht habtupdated()
wird aufgerufen, nachdem Daten geändert wurden und das DOM aktualisiert wurde. Funktioniert wiemounted()
, nur eben nach UpdatesbeforeDestroy()
wird aufgerufen, bevor eine Vue-Instanz zerstört wird. Hier solltet ihr EventListener entfernen, die ihr zuvor händisch hinzugefügt habtdestroyed()
wird aufgerufen, nachdem eine Vue-Instanz zerstört wurde und alle Kind-Instanzen ebenfalls nicht mehr existieren
Falls ihr diesen Prozess in einem visuellen Diagramm ansehen möchtet, könnt ihr es euch hier anschauen.
Weitere Optionen
Es gibt noch eine Reihe weiterer Optionen, die allerdings alle recht spezifisch sind und meistens nur in seltenen Fällen verwendet werden. Falls ihr sie dennoch gerne sehen möchtet, schaut in diesem Teil der API-Dokumentation nach.
Components im Detail
Wie wir also jetzt wissen, sind Components auch nur Vue-Instanzen mit einem Namen, die innerhalb von anderen Instanzen wiederverwertet werden können. Konkret vorstellen könnt ihr euch diese Components wie eigene HTML-Elemente, die ihr definieren und dann benutzen könnt.
Jedes Component hat seine eigenen Daten (diese werden als „State“ bezeichnet) und kann ganz genau so funktionieren, wie ihr es euch vorstellt.
Erstellen und Registrieren
In kleinen Vue-Anwendungen, die ohne einen Build-Step auskommen (also solchen, die ihr ohne Vue CLI baut), könnt ihr Components folgendermaßen registrieren:
// Erstellt ein neues Component mit dem Namen 'button-counter' und registriert es global
Vue.component('button-counter', {
data() { // in Components muss data eine Funktion sein, die ein Objekt zurückgibt
return {
count: 0,
};
},
methods: {
handleClick() {
this.count += 1;
}
},
template: '<button @click="handleClick">Ich wurde Mal geklickt!',
});
// Erstellt ein neues Component mit dem Namen 'local-counter' und registriert es lokal
const LocalCounter = {
data() { // in Components muss data eine Funktion sein, die ein Objekt zurückgibt
return {
count: 0,
};
},
methods: {
handleClick() {
this.count += 1;
}
},
template: '<button @click="handleClick">Ich wurde Mal geklickt!',
};
new Vue({
components: {
'local-counter': LocalCounter,
},
//…
})
Wenn ihr aber wie in den meisten Fällen euer Projekt mit der Vue CLI aufsetzt,
legt ihr eure Components als *.vue
-Dateien im src/components
-Ordner an. Da
diese Dateien jeweils ein komplettes Component enthalten, nennt man sie auch
„Single File Components“.
Diese Single File Components haben immer den selben Aufbau:
<!-- HelloWorld.vue -->
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String,
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="css">
h1 {
margin 40px 0 0;
}
a {
color #42b983;
}
</style>
Im <template>
-Block legt ihr das HTML fest, das euer Component kontrollieren
soll, d.h. die Struktur eures Komponents.
Im <script>
-Block übergebt ihr die Optionen, die intern dann verwendet werden,
um eine neue Vue-Instanz zu initialisieren. Deshalb verwendet ihr hier auch
export default
– denn dieses Optionen-Objekt ist es, was ihr später wieder
über import HelloWorld from '@/components/HelloWorld.vue'
importiert und dann
in der components
-Option für die lokale Registrierung, oder innerhalb von
Vue.component()
für die globale Registrierung verwendet. Zusammenfassend lässt
sich also sagen, dass ihr im <script>
-Block die Funktionalität eures
Components definiert.
Im <style>
-Block schreibt ihr das CSS, das das Aussehen eures Components
definiert. Habt ihr einen Pre-Processor installiert, könnt ihr ihn innerhalb
des lang
-Attributs festlegen, also z.B. lang="stylus"
für Stylus, damit Vue
weiß, dass der Code erst in CSS transpiliert werden muss. Es gibt außerdem noch
das spezielle Attribut scoped
, was es euch erlaubt festzulegen, dass das CSS
nur auf das Component angewendet wird, aber nicht für den Rest der Anwendung
gültig ist. Konkret bedeutet das also, dass ihr in einem Component einfach
Styles an den allgemeinen button
-Selektor hängen könnt, diese Styles aber nur
für <button>
-Elemente innerhalb dieses Components gelten und nicht für alle
Buttons. Es macht also meistens Sinn, scoped
aktiv zu lassen, denn der Sinn
eurer Components besteht ja darin, vom Rest eures Codes unabhängig zu sein. Mehr
zu Scoped CSS findet ihr hier.
Nur der <script>
-Block ist verpflichtend. <template>
und <style>
sind
optional, aber ihr werdet sie in den meisten Fällen alle drei benutzen.
Tipp:
Wenn ihr Atom und das language-vue
-Plugin benutzt, könnt ihr in
einer leeren \*.vue
-Datei einfach template
eingeben
und dann Tab drücken, um das Skelett eines Single File Components
zu erhalten.
Namensgebung
Wie ihr von den bisherigen Beispielen vermutlich schon vermutet, ist der Name, den ihr euren Components gebt der Name, den ihr später auch im HTML verwendet, wenn ihr das Component benutzen möchtet.
Wenn ihr nicht Single File Components benutzt, solltet ihr euren Components
immer einen Namen geben, der nur aus Kleinbuchstaben besteht und
mindestens einen Bindestrich enthält, z.B. mein-button
. Im HTML verwendet
ihr das Component dann als <mein-button>
.
Wenn ihr Single File Components verwendet, dann solltet ihr euer Component mit
PascalCase benennen, aber auch hier darauf achten, dass es aus mindestens zwei
Worten besteht, z.B. MeinButton.vue
. Im HTML könnt ihr dann sowohl
<mein-button>
als auch <MeinButton>
verwenden.
Allgemein gilt, dass ihr eure Components so benennen könnt, wie ihr wollt, aber der Name, wie auch bei Variablen schon, ausdrücken sollte, um was für ein Component es sich handelt. Es würde einfach keinen Sinn machen ein Button- Component „MeinApfel“ zu nennen, oder?
Tipp:
Da Component-Namen immer aus mindestens zwei Wörtern bestehen sollten, habe
ich es mir angewöhnt, sie mit dem Namen des Projekts zu „prefixen“, für mein
Portfolio würde ich also z.B. PortfolioButton.vue
verwenden und
für meine ToDo-App DitButton.vue
(Dit === Done In Time).
Lokale vs. Globale Registrierung
Ihr solltet in allen Fällen stets die Lokale Registrierung der Globalen vorziehen, da das euren Code schlank und sauber hält. Verwendet globale Registrierung nur dann, wenn ihr einen speziellen Grund dafür habt (der nicht „ich bin zu faul meine Components jedes mal zu importieren, wenn ich sie brauche“ ist).
Wenn ihr eine Bibliothek von „Base-Components“ habt und diese global nutzen wollt und ihr schon Erfahrung mit JS und modularisiertem Arbeiten habt, könnt ihr hier lernen wie das geht.
Templates
Templates von Vue-Components bestehen aus HTML, das – wie ihr bereits gesehen habt – mit Hilfe spezieller Symbole mit den Daten der Vue-Instanz verbunden wird.
Dabei ist es wichtig, dass ihr immer nur ein einziges HTML-Element als „Wurzel“ für ein Template benutzen dürft:
<!-- Ist ungültig, da zwei Wurzelelemente!!! -->
<template>
<span>{{ message1 }}</span>
<span>{{ message2 }}</span>
</template>
<!-- Gültig: -->
<template>
<!-- es ist immer eine gute Idee, dem Wurzel-Element eine class mit dem Namen des Components zu geben -->
<div class="span-wrapper">
<span>{{ message1 }}</span>
<span>{{ message2 }}</span>
</div>
</template>
Innerhalb des Templates könnt ihr:
- Mit
{{ "\{\{datenName\}\}" }}
reaktive Daten direkt in das HTML einbinden - Mit
v-html="datenName"
HTML in ein Element schreiben (ACTHUNG: unsicher!) - Mit
v-bind:attribut="datenName"
reaktive Daten in ein Attribut schreiben ( kann auch als:attribut="datenName"
geschrieben werden) - Mit
v-on:event="methodenName"
einen Event-Listener für ein Event registrieren (kann auch als@event="methodenName"
geschrieben werden) - Mit
v-if="bedingung"
,v-show="bedingung"
,v-for="schleife"
, etc. die Anzeige und Wiederholung von Elementen und Components kontrollieren
Achtung:
Wenn ihr v-for
benutzt, müsst ihr ein key
-Attribut
für das Element setzen und dieser Key muss einzigartig sein.
Wenn ihr die Liste nicht animieren möchtet, könnt ihr den Index als Key
verwenden, aber es bietet sich immer an, eine andere Eigenschaft zu benutzen,
wenn ihr garantieren könnt, dass sie einzigartig ist, z.B. eine UUID, oder die
von eurer Datenbank zugewiesene ID.
Das erlaubt es euch, hochflexible und interaktive Components zu erstellen, die aus anderen Components und standard HTML aufgebaut sind – die ihr zudem noch mit großer Einfachheit wiederverwerten könnt.
Unterschiede zur Root-Instanz
Vue Components sind also Vue Instanzen, unterscheiden sich aber wie angemerkt in ein paar Punkten von der Root-Instanz. Das meiste davon ist nur intern relevant, weshalb wir uns nicht damit beschäftigen müssen, aber es gibt dennoch zwei Punkte, die wir beachten sollten:
data
muss eine Funktion sein, denn da ihr Components beliebig oft wiederholen könnt jedes ein eigenes Data-Objekt haben sollte und nicht nur eine Referenz auf das gleiche Data Objekt. Konkret heißt das einfach, dass ihr in den Optionen eures Componentsdata
wie folgt angeben müsst:{ //…andere Optionen… data() { return { message: 'Hello World!', }; }, //…andere Optionen… };
Components haben eine
props
Option, die ihr dazu verwendet, um anzugeben, welche Daten an das Component weitergegeben werden können. Wie genau das funktioniert, sehen wir im nächsten Abschnitt. Der Props-Option könnt ihr entweder ein Array aus Strings geben, die dann alle als Props im Component verfügbar werden, oder ein Objekt, dass den Namen der Prop als Schlüssel und den Typ der Prop als Wert enthält. Ihr solltet immer Letztere Syntax verwenden, da ihr damit nicht nur sicherstellt, dass die übergebenen Daten vom richtigen Typ sind, aber auch die Möglichkeit habt, Default-Werte und Validatoren zu definieren.
Außer diesen beiden Punkten funktionieren eure Components also wie Root-
Instanzen und können dementsprechend die gleichen Optionen, wie z.B. methods
,
computed
, watch
, etc. enthalten.
Kommunikation mit anderen Components
Alles schön modular abzuspeichern und wiederzuverwerten ist natürlich super, auch für die eigene Ordnung. Aber was, wenn man einmal Daten von einem Component ins Andere bringen muss? Schließlich sind die einzelnen Screens unserer Apps ja auch nur große Components.
Props
Props sehen aus wie HTML-Attribute, dienen aber dazu Daten von Außen in das Component hineinzugeben. So könnt ihr zum Beispiel ein ToDo-Component programmieren, das überhaupt nichts von eurer Datenbank wissen muss, es erwartet lediglich ein Todo-Objekt mit Informationen darüber, was der Text des Todos ist, und ob es erledigt ist oder nicht. Noch ein einfacheres Beispiel wäre ein Button- Component, das wahlweise einen Hintergrund, oder nur eine Outline anzeigen kann. Auch hier hättet ihr eine Prop mit der ihr die Daten übergeben könntet.
Wichtig ist hierbei, dass Props nur in eine Richtung funktionieren: vom
Eltern-Component zum Kind-Component, nicht andersherum. Versucht also nicht, die
Daten im Kind-Component zu verändern. Wenn die Daten im Eltern-Component sich
ändern, ändern sie sich durch die reactivity auch im Kind, versucht ihr aber die
Daten vom Kind aus zu ändern, wird Vue euch warnen, dass das nicht getan werden
sollte. Wenn ihr Daten verändern wollt, die in ein Kind gegeben wurden, kopiert
sie erst in eine lokale data
-Eigenschaft oder führt die Veränderungen in einer
Computed Property aus:
{
//…andere Optionen…
computed: {
cleanData() {
// dirtyData kann alles mögliche sein, soll aber intern 'gesäubert' werden
// dafür eignet sich eine computed property hervorragend
return this.dirtyData.trim().toLowerCase();
}
}
data() {
return {
actualData: this.initialData, // actualData enthält eine Kopie von initialData und kann verändert werden
};
},
props: {
initialData: String,
dirtyData: String,
},
//…andere Optionen…
};
Achtung:
Objekte (d.h. auch Arrays!) werden über Referenzen verteilt, wie ihr wisst. Deshalb müsst ihr diese erst mit den Methoden aus Session 06 klonen und könnt sie nicht einfach zuweisen wie ihr es mit Strings, Zahlen, etc. tun könnt.
Wie einleitend erwähnt, sehen Props aus wie HTML-Attribute:
<!-- In einem anderen Component -->
<MyButton :outline="false">Click Me!</MyButton>
<!-- MyButton.vue -->
<template>
<button class="my-button" :class="{outline}">
<slot />
</button>
</template>
<script>
export default {
//…andere Optionen…
props: {
outline: Boolean,
}
//…andere Optionen…
};
</script>
Welche Props euer Component annimmt, legt ihr in der props
-Option fest. Diese
kann entweder ein Array mit den Namen der Props sein, oder (wie fast immer) ein
Objekt, dass den Namen der Props auch gleich den Typ zuweist, den sie annehmen
sollen:
export default {
//…andere Optionen…
props: ['propA', 'propB'], // Prop-Namen als Array
// ODER
props: { // Props als Objekt mit propName: Typ
propA: String,
propB: Boolean,
propC: {
// Ihr könnt einem Prop-Namen auch ein Objekt übergeben, das z.B. Standardwerte enthält
// Mehr dazu im unten verlinkten Artikel
type: 'Number',
default: 10,
},
}
//…andere Optionen…
};
Props werden in der Props-Option immer im camelCase geschrieben, aber da HTML
keinen Unterschied zwischen Groß- und Kleinbuchstaben macht, müsst ihr in euren
Components zwischen die einzelnen Wörter Bindestriche setzen. Aus einer Prop,
die als todoName
definiert wird wird also im Template todo-name
.
Da Props in HTML wie Attribute funktionieren, ist ihr Wert prinzipiell immer ein
String, wenn ihr also Daten aus eurem Component, oder etwas anderes als einen
String übergeben wollt, müsst ihr v-bind
benutzen:
<!-- Funktioniert nicht, da todoName, done und key die Strings 'todo.name',
'todo.done' und 'index' beinhalten würden -->
<MyTodo v-for="(todo, index) in todos" todo-name="todo.name" done="todo.done" key="index" />
<!-- Mit v-bind (abkekürzt als ':') übergebt ihr die Werte der Variablen und
nicht einfache Strings -->
<MyTodo v-for="(todo, index) in todos" :todo-name="todo.name" :done="todo.done" :key="index" />
Alle weiteren Attribute, die ihr einem Component übergebt, auch standard HTML-
Attribute, die nicht als Prop im Component deklariert sind, werden einfach als
normale Attribute an das Wurzel-Element des Components angehängt. Ausnahme:
style
und class
Attribute werden zusammengefasst und überschreiben sich
nicht.
Fortgeschrittene Themen, wie die Zuweisung von Standardwerten, und weitere nicht-essentielle Details könnt ihr hier nachlesen.
Events
Wir wissen jetzt, dass wir über Props Daten in unsere Components hineingeben können, aber auch, dass das eine Einbahnstraße ist. Wie bekommen wir jetzt also Daten wieder hinaus?
Ganz einfach: mit Events. Ihr wisst bereits, dass in JavaScript vieles über
Events passiert und während die meisten HTML-Elemente nur Events aussenden, die
der Nutzer verursacht hat, wie zum Beispiel Klick-Events, können unsere
Components jedes nur erdenkliche Event senden und ihm auch beliebige Daten
anhängen. Die einzige Einschränkung: nur das Eltern-Component kann Event-
Listener für die Events seiner Kind-Elemente registrieren, Vue-Events steigen
also nicht wie DOM-Events den gesamten Baum bis zum window
-Objekt hinauf (kein
Bubbling).
Alles, was ihr braucht, um ein Custom-Event aussenden zu können ist ein Name.
Dieser kann alles Mögliche sein, aber er muss in HTML darstellbar sein,
also fallen Großbuchstaben raus. Benutzt also einzelne Wörter wie click
oder
kebap-case für Events aus mehreren Wörtern: mein-event
.
<!-- MyButton.vue -->
<template>
<button class="my-button" @click="emitEvent">{{buttonText}}</button>
</template>
<script>
export default {
methods: {
emitEvent() {
this.$emit('my-click', 'meine Daten!');
},
},
props: {
buttonText: String,
},
};
</script>
<!-- HomeScreen.vue -->
<template>
<main class="home">
<MyButton button-text="Click Me!" @my-click="handleClick" />
</main>
</template>
<script>
import MyButton from '@/components/MyButton.vue';
export default {
components: {
MyButton,
},
methods: {
handleClick(data) {
console.log(`Die Daten des Events waren: ${data}`);
},
}
};
Da Components völlig frei benannte Events aussenden können, geht Vue prinzipiell
davon aus, dass auch DOM-Events wie click
, oder keyup
vom Component
ausgesendet werden. Das bedeutet, dass wenn euer Component diese Events nicht
aussendet, auch die Listener dafür nie aufgerufen werden. Wollt ihr also auf das
DOM-Event click
hören, müsst ihr dem Listener einen sogenannten Modifier
anhängen, in diesem Fall: .native
:
<!-- MyButton sendet kein 'click'-Event, wenn wir also das DOM-Event meinen,
müssen wir '.native' anhängen. -->
<MyButton @click.native="handleClick" />
<!-- <button> ist kein Component, deshalb brauchen wir hier das '.native' nicht -->
<button @click="handleClick">Click me!</button>
Tipp:
Da es häufig vorkommt, dass für ein Button-Component der Click-Listener
gebraucht wird, habe ich in meinen Button-Components normalerweise ein Pass-
Through-Event definiert: <button @click="$emit('click', $event)">
.
Das sorgt dafür, dass das native click
-Event einfach als Custom-
Event emittiert wird und ich mir das .native
sparen kann.
Es gibt auch noch viele weitere nützliche Modifier, die ihr hier nachlesen könnt. Ich will euch an dieser Stelle nur die drei Nützlichsten vorstellen:
- Mit
.self
feuert das Event nur, wennevent.target === event.currentTarget
, d.h. nur dann, wenn z.B. ein Klick auf dem Element selbst und nicht einem seiner Kinder ausgeführt wurde - Mit
.prevent
wird automatisch die Browser-Eigene Funktionalität für dieses Event gestoppt (als hättet ihr manuellevent.preventDefault()
aufgerufen). Das ist vor allem dann nützlich, wenn ihr z.B. verhindern wollt, dass ein Druck auf Enter eine neue Zeile einfügt - Mit sogenannten „Key Modifiers“ könnt ihr bei Keyboard-Events angeben für
welche Taste das Event auslösen soll, also z.B.
@keyup.enter
um nurkeyup
für die Entertaste abzufangen. - Modifier können auch aneinandergereiht werden:
@keyup.native.ctrl.enter.prevent
Slots
Es kann vorkommen, dass ihr einem Component eigenen Content übergeben wollt, der
sehr variabel ist, so wie ihr zum Beispiel auch einem <div>
-Element alles
mögliche übergeben könnt. Das über Props zu lösen wäre sehr umständlich, deshalb
gibt es in Vue ein spezielles <slot>
-Element.
Benutzt dieses Element in euren Components und alles was ihr dann innerhalb
der Tags schreibt, wird an der Stelle auftauchen, an der <slot>
stand:
<!-- MyButton.vue -->
<template>
<button class="my-button">
<slot /> <!-- Hier taucht alles auf, was innerhalb von <MyButton></MyButton> steht -->
</button>
</template>
<!-- In einem anderen Component -->
<MyButton>
<!-- <slot /> wird durch dieses HTML ersetzt -->
<strong>Click</strong>
Me!
<!-- <slot /> wird durch dieses HTML ersetzt -->
</MyButton>
Slots sind ein sehr mächtiges Werkzeug, mit dem viel erreicht werden kann. Für den Anfang wird euch dieser grundlegende Nutzen vollkommen ausreichen, aber falls ihr mehr wissen wollt, könnt ihr hier nachlesen.
v-model
Emulation von Ihr werdet vielleicht an einen Punkt kommen, an dem ihr ein eigenes
Input-Component baut und auf diesem gerne v-model
verwenden würdet. Damit das
funktionert, muss euer Component zwei Voraussetzungen erfüllen:
- Es muss das
value
-Attribut an einevalue
-Prop binden - Es muss ein
input
-Event mit dem aktuellen Wert des Inputs emittieren
<template>
<div class="my-input">
<!-- Kondition 1: value-Attribut wird an die value-Prop gebunden -->
<input :value="value" @input="handleInput">
</div>
</template>
<script>
export default {
methods: {
handleInput(e) {
// Kondition 2: das Component sendet ein 'input'-Event mit dem neuen Wert aus
// e ist das Event-Objekt
this.$emit('input', e.target.value);
}
},
props: {
value: String,
},
};
</script>
Jetzt könnt ihr v-model
problemlos auf eurem <MyInput>
-Component verwenden.
Mir ist bewusst, dass ihr euch vermutlich von all diesen Informationen erschlagen fühlt, aber wenn ihr all dieses neue Wissen nach und nach selbst anwendet, werdet ihr es immer besser verstehen und einsetzen lernen.
is
, ref
und key
Spezielle Attribute Es gibt in Vue einige spezielle Attribute, die ihr euren Components (und auch normalen DOM-Elementen) geben könnt, um bestimmte Funktionalitäten zu verwenden.
Wir gehen hier auf drei der Wichtigsten ein, wenn ihr mehr erfahren möchtet, seht euch den Abschnitt über Spezielle Attribute in der Vue-Dokumentation an.
key
Einzigartigkeit Signalisieren
Mit Das key
-Attribut wird von Vue dafür verwendet, die Einzigartigkeit eines
Elements oder Components festzustellen. Aus Performance-Gründen versucht Vue
nämlich so wenige neue Elemente im DOM anzulegen wie möglich, weshalb es wann
immer möglich bereits bestehende Elemente wiederverwertet. Gebt ihr einem
Element oder Component aber einen key
teilt ihr Vue mit, dass es dieses
Element oder Component nicht mehr wiederverwerten darf, wenn es erst einmal aus
dem DOM entfernt wurde, oder an eine andere Stelle wandert.
Primär werdet ihr das key
-Attribut in v-for
-Schleifen verwenden, aber es
kann auch nützlich sein, wenn ihr zum Beispiel das <transition>
-Component
verwendet und sicherstellen wollt, dass die Animation zwischen zwei gleichen
Elementen / Components stattfindet:
<transition>
<!-- Wenn key nicht gesetzt wäre, würde Vue das <span>-Element wiederverwerten
und es gäbe keine Animation, wenn 'text' sich ändert. Da key aber immer den
Wert von text beinhaltet sind die Keys unterschiedlich und Vue verwertet das
Element nicht wieder -->
<span :key="text">{{ text }}</span>
</transition>
Tipp:
Für den Anfang ist es besser, wenn ihr das key
-Attribut nur
innerhalb von v-for
verwendet, denn dort ist es Pflicht und euer
Linter wird sich beschweren, wenn ihr es nicht benutzt.
is
und <component>
dynamische Components verwenden
Mit Manchmal kann es vorkommen, dass ihr gerne dynamisch zwischen zwei Components wechseln möchtet, z.B. wenn ihr zwei verschiedene Ansichten für eure Todos habt.
In einer Ansicht existieren die Todos als Listenelement-Components und in einer
anderen Ansicht existieren sie als Karten-Components. Nun könntet ihr natürlich
beide Components in eurem Template verwenden und mit v-if
zwischen ihnen
wechseln, aber es gibt auch eine bessere Methode.
Mit dem speziellen <component>
-Component könnt ihr ein „Platzhalter-Component“
in eurem Template verwenden und dann mit Hilfe des speziellen is
-Attributs
dynamisch bestimmen, welches Component an dieser Stelle angezeigt werden soll:
<!-- View.vue -->
<template>
<div class="todo-wrapper">
<button type="button" @click="handleClick">{{currentView}}</button>
<component v-for="(todo, index) in todos" :is="`${currentView}Item`" :key="index" :todo="todo" />
</div>
</template>
<script>
import ListItem from '@/components/ListItem.vue';
import CardItem from '@/components/CardItem.vue';
export default {
components: {
ListItem,
CardItem,
},
data() {
return {
currentView: 'List',
todos: ['Todo 1', 'Todo 2', 'Todo 3'],
};
},
methods: {
handleClick() {
if (this.currentView === 'List') this.currentView = 'Card';
else this.currentView = 'List';
},
},
};
</script>
Wie ihr sehen könnt, könnt ihr euch mit dieser Methode etwas an Tipp-Arbeit
sparen. Falls euch das für den Anfang aber noch zu kompliziert ist, könnt ihr
natürlich wie eingehend bereits erwähnt mit v-if
arbeiten und ein ähnliches
Ergebnis erreichen – auch wenn es weniger flexibel ist.
ref
Referenzen auf DOM-Elemente und Components erhalten
Mit Wenn ihr einem Element oder Component das ref
-Attribut übergebt, legt Vue
automatisch eine Referenz zu diesem Objekt unter dem übergebenen Namen im
Eltern-Component ab. Aufrufen könnt ihr sie dort dann unter der speziellen
$refs
-Eigenschaft.
Ist das Element ein DOM-Element, erhaltet ihr so eine Referenz, wie ihr sie auch
über document.querySelector
bekommen würdet – ist es aber ein Component,
erhaltet ihr eine Referenz auf die Component-Instanz! Behaltet das also im
Hinterkopf.
Ihr solltet auch beachten, dass $refs
nicht reactive und erst nach
dem mounted()
-Lifecycle-Hook verfügbar ist.
Das ref
-Attribut ist ein letzter Ausweg für bestimmte Randfälle. Normalerweise
werdet ihr es nie benutzen, um auf Component-Instanzen zuzugreifen. Ich benutze
es auch nur hin und wieder, um zum Beispiel eine Referenz auf ein
<input>
-Element oder ähnliches innerhalb eines Components zu erhalten:
<template>
<div class="my-input">
<input ref="input" type="text" :value="value" @input="$emit('input')">
</div>
</template>
<script>
export default {
mounted() {
console.log(this.$refs.myInput.type); // Ausgabe: "text"
},
props: {
value: String,
},
};
</script>
v-bind
mit class
und style
?
Wie Funktioniert Eine der häufigsten Aktionen, die ihr mit JavaScript ausführt, ist die
Manipulation von CSS Klassen und Styles auf euren HTML-Elementen. Aus diesem
Grund macht Vue euch diese Aufgabe leichter, indem es den Attributen class
und
style
neue Funktionen hinzufügt, wenn ihr diese mit v-bind
(oder :
)
kombiniert.
Konkret bedeutet das, dass ihr den class
und style
Attributen eurer Elemente
und Components nicht nur einen String übergeben könnt, sondern auch Arrays, oder
Objekte. Außerdem können :class
und :style
Attribute mit „normalen“ class
und style
Attributen co-existieren und werden dann automatisch zur Laufzeit
zusammengefasst. Kombiniert mit der Reactivity von Vue stehen euch so alle Türen
offen.
class
Objekt-Syntax für Wenn ihr einem :class
-Attribut ein Objekt übergebt, repräsentieren die
Schlüssel (was vor dem Doppelpunkt steht) immer die Klasse, die dem Element
hinzugefügt werden soll, wenn der Wert (was nach dem Doppelpunkt steht) true
entspricht. Hier bietet es sich also an, z.B. eine reaktive Data-Eigenschaft
zu verwenden:
<template>
<!-- Im Browser hat der Button die Klasse 'active', weil 'isActive' in data 'true' ist -->
<!-- Ändert sich der Wert von 'isActive' auf 'false', wird die Klasse 'active' automatisch entfernt -->
<button :class="{ active: isActive }">Click me!</button>
</template>
<script>
export default {
data() {
return {
isActive: true,
};
},
},
</script>
Tipp:
Da man aus Konvention heraus CSS-Klassen meistens mit kebap-case
schreibt, kann es vorkommen, dass ihr das auch so in euren Objekten schreiben
möchtet. Packt hierzu einfach den Namen in Anführungszeichen:
:class="{ 'meine-klasse': showMyClass }"
Da das übergebene Objekt einfach nur ein reaktives Objekt sein muss, könnt ihr
es für erweiterte Fälle auch in data
oder als Computed Property ablegen. Wie
das aussehen kann könnt ihr hier
nachlesen.
class
Array-Syntax für Übergebt ihr einem :class
-Attribut hingegen ein Array, werden die Werte
der Elemente des Arrays als Klassen übergeben:
<template>
<!-- Hat im Browser die Klasse 'active', weil das der Wert von 'activeClass' ist -->
<!-- Ändert sich dieser Wert, ändert sich auch die Klasse auf dem Element -->
<button :class="[activeClass]">Click me!</button>
</template>
<script>
export default {
data() {
return {
activeClass: 'active',
};
},
},
</script>
Auch hier gilt: wenn die Variablen innerhalb des Arrays reactive sind, wird sich die Klasse auf dem Element dynamisch ändern, wenn sich der Wert der Variablen ändert.
Beides Zusammen
Die Array-Syntax lässt sich auch mit der Objekt-Syntax kombinieren, um trotzdem anhand von Bedingungen Klassen hinzuzufügen, oder zu entfernen:
<template>
<!-- Hat die Klassen 'active' und 'no-error', weil 'isActive' === 'true' und 'errorClass' === 'no-error' -->
<button :class="[{ active: isActive }, errorClass]">Click me!</button>
</template>
<script>
export default {
data() {
return {
errorClass: 'no-error',
isActive: true,
};
},
},
</script>
style
Objekt-Syntax für Auch für das style
-Attribut gibt es eine Objekt-Syntax. Hier sind die
Schlüssel die jeweilige CSS-Eigenschaft, also z.B. fontSize
(statt font-size
)
für die Schriftgröße und der Wert ist der eigentliche Wert für diese Eigenschaft,
also z.B. 16px
.
<template>
<p :style="{ fontSize: `${fontSize}px`}">Text mit variabler Größe!</p>
</template>
<script>
export default {
data() {
return {
fontSize: 16,
};
},
},
</script>
Achtung:
Vergesst die Einheiten (px, %, etc.) nicht! Oft habt ihr die Daten in eurem
Component einfach als Zahlen liegen, aber damit sie im style
-Attribut
funktionieren brauchen sie eine Einheit.
Wie auch schon bei class
könnt ihr auch hier eure Styles in ein Objekt in data
packen und gleich das ganze Objekt anbinden. So könnt ihr sogar mehrere Objekte
mit Styles verwenden, wenn ihr sie in ein Array steckt. Allerdings kommt das
selten bis gar nicht vor.
Tipp:
Vue hängt automatisch an CSS-Eigenschaften, die Präfixe (-webkit, -moz, etc.)
benötigen diese Präfixe an, wenn ihr die Eigenschaften über ein
:style
-Attribut bindet.
Wie baut man coole Animationen?
Animationen sind ein sehr aktuelles Thema und tauchen im gesamten Internet immer
öfter und prominenter auf. Aus CSS kennt ihr sicher die transition
und animation
Eigenschaften und schon nur mit diesen kann man unglaubliche Animationen
erreichen. Wenn man dann noch JavaScript-Animationsbibliotheken wie Mo.js
und Anime.js sowie SVG in den Mix aufnimmt, gibt es
praktisch nichts mehr, dass sich nicht umsetzen lässt.
Ich persönlich bin sogar ein Fan davon, meine Animationen gleich in HTML, CSS und JS zu bauen, anstatt Werkzeuge wie AfterEffects zu benutzen, weil ich dann die volle Kontrolle über alles habe, auch wenn es etwas weniger interaktiv und visuell ist.
Aber ganz gleich, wie und wo ihr eure Animationen macht, das Thema ist so groß, dass man damit einen eigenen Kurs füllen kann. Besonders im Web müsst ihr natürlich darauf achten, dass eure Animationen das Gerät eures Nutzers nicht zu sehr beanspruchen und dass die Animationen die UX nicht stören.
Hier sind zwei Artikel zum Thema, die ihr euch gerne durchlesen könnt (es gibt noch viele, viele mehr):
Alles, wofür ich hier Zeit habe, ist euch zwei spezielle Components vorzustellen, die es euch vereinfachen in Vue.js Animationen zu erstellen.
<transition>
Component
Das Mit dem <transition>
-Component könnt ihr Elemente animieren, die über v-if
,
v-show
und das <component>
-Component ein und ausgeblendet werden. Das kann
entweder über JavaScript, oder CSS geschehen.
Da CSS die einfachere Variante ist und auch für die meisten Fälle ausreicht, werde ich euch hier nur diese Methode zeigen.
Wenn ihr ein Element oder Component mit einem <transition>…</transition>
-Component
umgebt, könnt ihr diesem Element oder Component sechs verschiedene Klassen zuweisen,
die Vue dann für euch zu den richtigen Zeitpunkten einfügt und wieder entfernt:
v-enter-active
undv-leave-active
sind über die gesamte Dauer der Transition auf dem Element vorhanden. In ihnen definiert ihr eure Werte für dietransition
-Eigenschaft im CSSv-enter
undv-leave
sind die „Startzustände“ eurer Animation und werden aktiviert, sobald die Animation beginnt und nach einem Frame wieder entferntv-enter-to
undv-leave-to
sind die „Endzustände“ eurer Animationen, sie werden aktiviert, sobaldv-enter
undv-leave
entfernt werden
Erklärung: Wann enter und wann leave?
Alle Klassen, die „enter“ beinhalten werden aktiviert, wenn ein Element oder Component sichtbar wird, d.h. den Browser betritt. Alle Klassen, die „leave“ beinhalten werden aktiviert, wenn ein Element oder Component unsichtbar wird, d.h. den Browser verlässt.
Das hört sich komplizierter an, als es ist. So könnt ihr ein Element weich ein- und ausblenden lassen:
<template lang="html">
<div class="transition-example">
<transition>
<p v-show="visible">Now you see me!</p>
</transition>
<button type="button" @click="visible = !visible">{{buttonText}}</button>
</div>
</template>
<script>
export default {
computed: {
buttonText() {
if (this.visible) return 'Hide me';
return 'Now you don’t';
},
},
data() {
return {
visible: true,
};
},
};
</script>
<style lang="css" scoped>
.transition-example {
max-width: 44rem;
margin: 4rem auto;
background-color: rgba(38, 50, 56, 1);
padding: 2rem;
border-radius: 1.5rem;
color: white;
}
.transition-example p {
margin-bottom: 1rem;
}
.transition-example p.v-enter-active,
.transition-example p.v-leave-active {
transition: opacity 500ms ease;
}
.transition-example p.v-enter,
.transition-example p.v-leave-to {
opacity: 0;
}
</style>
Now you see me!
Wie ihr sehen könnt, wird das Element erst von der Seite entfernt, wenn die Animation abgeschlossen ist – das ist in Vanilla JS nur sehr umständlich umzusetzen, aber Vue macht es uns einfach. 🎉
Achtung:
Es darf immer nur ein Element innerhalb des
<transition>
-Components liegen! Benutzt also nicht v-show
,
sondern v-if/v-else
, wenn ihr zwischen mehreren Elementen wechselt.
<transition-group>
Component
Das Da innerhalb eines <transition>
-Component immer nur ein Element oder
Component liegen darf, gibt es für Listen das <transition-group
-Component, das
ihr zum Beispiel einsetzen könnt, wenn ihr etwas mit v-for
wiederholt.
Es funktioniert im Endeffekt genauso wie das <transition>
-Component, hat aber
ein paar Extras:
- Im Gegensatz zum
transition
-Component fügt es ein Element in das DOM ein, ihr könnt mit demtag
-Attribut bestimmen, was für ein Element das sein soll. Standardmäßig ist es ein<span>
- Elemente und Components innerhalb des Components müssen ein
key
-Attribut haben - Wenn sich Positionen von Elementen innerhalb des Components ändern, können
diese animiert werden, indem sich eine
v-move
-Klasse auf ihnen befindet. Das geschieht vollautomatisch über die FLIP-Methode, die ziemlich komplex ist, aber mit der wir uns dank Vue nicht auseinander setzen müssen, yay 🎉
<template lang="html">
<div class="transition-group-example">
<transition-group tag="ul">
<li v-for="num in numbers" :key="num">
{{num}}
</li>
</transition-group>
<button type="button" @click="addNum">Add</button>
<button type="button" @click="removeNum">Remove</button>
<button type="button" @click="shuffle">Shuffle</button>
</div>
</template>
<script>
export default {
data() {
return {
counter: 4,
numbers: [0, 1, 2, 3],
};
},
methods: {
addNum() {
const newNum = this.counter;
const randomIndex = Math.floor(Math.random() * this.numbers.length);
this.counter += 1;
this.numbers.splice(randomIndex, 0, newNum);
},
removeNum() {
const randomIndex = Math.floor(Math.random() * this.numbers.length);
this.numbers.splice(randomIndex, 1);
},
shuffle() {
const numCopy = [...this.numbers];
// Fisher-Yates Shuffle
for (let currentIndex = numCopy.length - 1; currentIndex > 0; currentIndex -= 1) {
const randomIndex = Math.floor(Math.random() * (currentIndex + 1));
const currentNum = numCopy[currentIndex];
numCopy[currentIndex] = numCopy[randomIndex];
numCopy[randomIndex] = currentNum;
}
this.numbers = numCopy;
},
},
};
</script>
<style lang="css" scoped>
.transition-group-example {
max-width: 44rem;
margin: 4rem auto;
background-color: rgba(38, 50, 56, 1);
padding: 2rem;
border-radius: 1.5rem;
color: white;
}
.transition-group-example ul {
margin-bottom: 1rem;
position: relative;
}
.transition-group-example .v-enter-active,
.transition-group-example .v-leave-active {
transition: opacity 500ms ease, transform 500ms ease;
}
.transition-group-example .v-leave-active {
position: absolute;
}
.transition-group-example .v-move {
transition: transform 500ms ease;
}
.transition-group-example .v-enter {
opacity: 0;
}
.transition-group-example .v-leave-to {
opacity: 0;
transform: translateX(100%);
}
</style>
- 0
- 1
- 2
- 3
Das ist nur die Spitze des Eisbergs
Ihr könnt so viel mehr erreichen, wenn ihr euch genauer mit den Components und Vues Reaktivitätssystem befasst. Die Artikel zu den Components und State Transitions der Vue.js-Guide sind ein super Startpunkt dafür.
Wann braucht man Routing und State-Management?
Sobald eure Apps mehr als nur eine Ansicht (d.h. einen Screen) benötigen, wird
es kompliziert, das alles über v-if/v-else
zu managen. Auch die Möglichkeit,
einfach mehrere HTML-Seiten anzulegen ist nicht praktisch, weil es einerseits
zu unschönen Artefakten kommen kann, wenn man die Seiten wechselt (z.B. ein
Aufblitzen des leeren Browser-Fensters) und andererseits eure Skripte
auswechselt, was bedeuten würde, dass auf jeder neuen Seite eine neue
Root-Instanz von Vue aufgebaut werden müsste.
Deshalb gibt es in Apps wie wir sie hier bauen nur eine einzige Seite:
index.html
(daher wie in Session 01 erwähnt der Begriff „Single Page App“) und
wir müssen das Konzept mehrerer Seiten irgendwie über JavaScript lösen. Da es
sich natürlich nicht um echte „Seiten“ handelt, werden sie stattdessen „Routen“
genannt. Dementsprechend nennt man die Libraries, die sich für uns darum kümmern
auch man „Router“. Es gibt sogar einen offiziellen Router für Vue, den
Vue Router.
Auf der anderen Seite ist es auch wichtig, einen zentralen Ort für all eure Daten zu haben, die über mehrere Components verteilt sind, z.B. der Name eures Nutzers, oder seine Einstellungen, die einerseits alle gebündelt in einem Einstellungs-Screen modifiziert werden können sollten, aber andererseits Auswirkungen in den verschiedensten Bereichen eurer App haben.
Jedes eurer Components hat seine eigenen Daten in der data
-Option, das
bezeichnet man auch als „lokalen State“, oder „Component State“ – globale Daten,
also Daten, auf die man von jedem Component aus Zugriff haben sollte, sind ein
„globaler State“. Allerdings ist es immer schwierig mit globalen Daten umzugehen,
da man sich nie sicher sein kann, welcher Teil einer Anwendung diese Daten
benutzt und verändert und was für Auswirkungen das haben kann. Deshalb wird
stets davon abgeraten, z.B. globale Variablen zu verwenden.
Dennoch brauchen wir nun einmal einen globalen State in bestimmten Anwendungen und den Prozess, wie man damit umgehen kann, dass es nicht zu den typischen Problemen globaler Daten kommen kann, nennt man „State Management“ – und auch hierfür gibt es eine offizielle Vue-Library: Vuex.
Wie benutzt man das?
Vue-Router und Vuex sind Plugins für Vue, die ihr ebenfalls über npm
installieren, oder einfach bei der Erstellulng euer App mit der Vue CLI
auswählen könnt. Sie erweitern eure Vue-Instanzen um Routing und State
Management.
Beide Plugins haben sehr viele Funktionen, die für fortgeschrittene und große Apps sehr nützlich sind, aber wir werden uns hier auf die einfachste Verwendung konzentrieren, da ihr schon jetzt unglaublich viel neues Wissen auf euren Tellern habt.
Vue Router
Der Kern von Vue-Router ist eine Datei (router.js
oder router/index.js
), in
der ihr angebt, welche URL zu welchem Component gehört. Ihr sagt Vue also im
Endeffekt: wenn der Browser-Pfad (die URL) „meine-app.de/settings“ ist, dann
zeig das „Einstellungen.vue“-Component an.
Die router/index.js
-Datei, die euch die Vue CLI anlegt, wenn ihr bei der
Erstellung angebt, dass ihr Vue Router benutzen wollt, sieht folgendermaßen aus:
// src/router/index.js
import Vue from 'vue'; // importiere Vue, damit wir das Plugin registrieren können
import VueRouter from 'vue-router'; // importiere Vue Router
import Home from '../views/Home.vue'; // importiere Components
Vue.use(VueRouter); // Registriere VueRouter zur Verwendung in Vue
// Das ist die 'Landkarte' in der ihr angebt, welcher Pfad zu welcher Datei geöhrt
const routes = [
{
path: '/',
name: 'Home', // Ihr könnt Routen Namen geben, um leichter zu ihnen zu navigieren
component: Home, // Das Home Component wurde in Zeile 3 importiert
},
{
path: '/about',
name: 'About',
// ihr könnt aber auch Components so importieren, das hat den Vorteil, dass
// sie erst geladen werden, wenn ihr die Route zum ersten Mal besucht, das
// nennt man 'Lazy Loading'
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
},
];
// Hier wird der eigentliche router initialisiert
const router = new VueRouter({ // man kann ihm Optionen übergeben
// z.B. dass der History-Modus verwendet werden soll (ansonsten werden eure Routen
// mit einem '#' an den Pfad gehängt, z.B. meine-app.de/#/about)
mode: 'history',
// und welche URL die "Basis" ist, meistens ist das einfach meine-app.de, aber
// vielleicht habt ihr sie ja in einem Unterordner "apps", dann wäre die Basis
// meine-app.de/app/ (die Standardeinstellung ist aber meistens richtig)
base: process.env.BASE_URL,
routes, // eure Routen-Landkarte muss natürlich auch übergeben werden!
});
// Hier wird der initialisierte Router exportiert, damit ihr ihn eurer Root-Instanz übergeben könnt
export default router;
// src/main.js
import Vue from 'vue';
import App from './App.vue';
import './registerServiceWorker';
import router from './router'; // hier wird der exportierte Router importiert
import store from './store';
Vue.config.productionTip = false;
new Vue({
router, // und hier der Root-Instanz übergeben, damit ist er aktiv
store,
render: (h) => h(App),
}).$mount('#app');
Dadurch, dass ihr Vue Router aktiviert habt, bekommt ihr Zugriff auf zwei neue Instanz-Optionen in eueren Components:
this.$router
ist eine Referenz auf den Router selbst, um zum Beispiel über JavaScript zu einer neuen Route zu wechseln:this.$router.push('/about')
this.$route
ist eine Referenz auf die aktuelle Route, darüber könnt ihr bestimmte Metadaten auslesen, oder zum Beispiel schnell herausfinden, auf welcher Route ihr euch gerade befindet
Das <router-link>
Component müsst ihr statt normaler <a>
-Tags verwenden,
um zwischen mehreren Routen innerhalb eurer App zu verlinken – denn es sind ja
keine herkömmlichen Seiten wie in HTML, sondern nur „virtuelle“. Statt einem
href
-Attribut gebt ihr ein to
-Attribut an, das entweder den Pfad eurer Route,
oder ein spezielles Objekt mit Informationen enthält:
<template>
<nav class="main-nav">
<!-- "to" kann entwender ein String mit dem Pfad sein, wie "href" -->
<router-link to="/">Home</router-link>
<!-- oder ein Objekt mit dem Namen der Route (und anderen Optionen, für Fortgeschrittene) -->
<router-link :to="{ name: 'About' }">About</router-link>
</nav>
</template>
Beide Varianten rendern als ein <a>
-Element auf eurer Seite, haben aber den
Vorteil, dass sie z.B. automatisch eine router-link-active
-Klasse bekommen,
wenn die aktuelle Route die gleiche ist, wie die, die im to
angegeben wurde.
Dieses Component ist sehr flexibel und bietet haufenweise weiterer Optionen, die ihr hier nachlesen könnt.
Das <router-view>
Component verwendet ihr, um Vue mitzuteilen wo das
Component gerendert werden soll, das zu einer Route gehört. So könnt ihr zum
Beispiel Components wie eine Navigationsleiste, die auf jeder Route zu sehen
sein soll an einem zentralen Ort (meistens App.vue
) angeben:
<!-- src/App.vue -->
<template>
<div id="app">
<!-- Diese Div wird auf jeder Route zu sehen sein -->
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<!-- An dieser Stelle taucht das Component auf, das zur Route gehört -->
<router-view/>
</div>
</template>
Da <router-view>
auch nur ein Component ist, könnt ihr es zum Beispiel
innerhalb eines <transition>
-Components verwenden, um den Übergang zwischen
zwei Routen zu animieren:
<template>
<!-- Dieses Component wird nicht ausgeblendet -->
<MainNav />
<!-- So wird die aktuelle Route zuerst ausgeblendet, dann wird die neue Route wieder eingeblendet -->
<transition mode="out-in">
<router-view class="route" />
</transition>
</template>
<style lang="css">
.route.v-enter-active,
.route.v-leave-active {
transition: opacity 200ms ease;
}
.route.v-enter,
.route.v-leave-to {
opacity: 0;
}
</style>
Damit wisst ihr auch schon alles, was für den Anfang wichtig wäre und könnt Vue Router in eurer App verwenden. Für fortgeschrittene Themen, wie zum Beispiel das Abbrechen von Navigation, Weiterleitungen, laden von Daten, etc. könnt ihr euch natürlich die exzellente Dokumentation von Vue Router durchlesen.
Vuex
Vuex hilft euch dabei, einen globalen State zu haben und ihn auf eine Art und Weise zu verwalten, die Probleme verhindert und es einfacher macht nachzuvollziehen, wo Fehler entstehen.
Im Kern ist der sogenannte „Store“, ein großes JavaScript-Objekt in dem all eure globalen Daten liegen („State“), sowie die Funktionen, die diese Daten bearbeiten („Mutations“) könnt. Wie so ziemlich alles in Vue sind die Daten, die in eurem Store liegen reactive, aktualisieren sich bei Veränderung automatisch in euren Templates.
Wie auch schon der Router wird dieser Store in einer eigenen Datei angelegt
(meistens src/store.js
oder src/store/index.js
) und in src/main.js
dann
eurer Root-Instanz hinzugefügt.
Aber Amadeus, warum brauche ich Mutations, wenn ich auch einfach direkt die Daten im State ändern könnte?
Der Grund, weshalb ein globaler State in Libraries wie Vuex funktionieren kann, ist, dass jede Änderung an den globalen Daten explizit und zentral geschieht. So können Fehlerquellen minimiert werden. Deshalb braucht ihr Mutations, damit immer klar ist, welche Funktion schuld ist, wenn ein Fehler entsteht.
Der einfachste Vuex Store sieht folgendermaßen aus:
// src/store/index.js
import Vue from 'vue'; // importiere Vue damit wir das Plugin registrieren können
import Vuex from 'vuex'; // importiere das Plugin
Vue.use(Vuex); // registriere das Plugin
const store = new Vuex.Store({ // erstelle einen neuen Vuex Store mit diesen Optionen
// hier ist unser State mit einer Eigenschaft: count
state: {
count: 0,
},
// hier sind unsere Mutations
mutations: {
// diese Mutation erhöht den Wert von state.count um 1
increment(state) {
state.count += 1;
},
},
});
// exportiere den Store, damit wir ihn unserer Root-Instanz übergeben können
export default store;
// src/main.js
import Vue from 'vue';
import App from './App.vue';
import store from './store'; // hier wird der exportierte Store importiert
new Vue({
store, // und hier der Root-Instanz übergeben, damit er aktiv wird
render: (h) => h(App),
}).$mount('#app');
Wenn der Store so initialisiert wurde, haben wir in allen unseren Components
von nun an mit this.$store
Zugriff auf den Store. So können wir mit
this.$store.state
auf die globalen Daten zugreifen, und mit
this.$store.commit(mutationName, mutationPayload)
eine Mutation ausführen:
<template>
<div class="my-global-counter">
<p>The current global value is: {{$store.state.counter}}</p>
<button @click="$store.commit('increment')">Increment</button>
</div>
</template>
Hier ein konkreteres Beispiel: euer Nutzer kann während des Onboardings einen Namen angeben, der ihm später auf dem Home-Screen der App angezeigt wird und den er in den Einstellungen ändern kann.
In eurem Store habt ihr also z.B. ein Objekt, das all die Einstellungen eures Nutzers abspeichert, sowie eine Mutation, die diesen Namen anpasst:
//…
const store = new Vuex.Store({
state: {
preferences: {
name: '',
},
},
mutations: {
setName(state, value) {
state.preferences.name = value;
},
},
});
//…
Dann könnt ihr in eurem Onboarding den Namen setzen:
<template>
<div class="onboarding">
<!-- wir benutzen hier den lokalen State, damit wir ihn überprüfen können, bevor wir den Namen global setzen -->
<input v-model="name" type="text" @keyup.enter="setName">
<button type="button" @click="setName">Set Name</button>
</div>
</template>
<script>
export default {
// Lokaler State
data() {
return {
name: '',
},
},
methods: {
setName() {
// Fehler ausgeben, wenn der Name ungültig ist
if (this.name.length === 0) window.alert('Name is too short!');
else this.$store.commit('setName', this.name); // oder den Namen setzen, wenn er gültig ist
},
},
};
</script>
In eurem Home-Component könnt ihr euch den gesetzten Namen dann anzeigen lassen:
<template>
<div class="home">
<h1>Hi {{$store.state.preferences.name}}!</h1>
</div>
</template>
In eurem Einstellungs-Component könnt ihr den Namen dann wieder Ändern:
<template>
<div class="settings">
<input v-model="name" type="text" @keyup.enter="setName">
<button type="button" @click="setName">Set Name</button>
</div>
</template>
<script>
export default {
created() {
// Wenn das Component initialisiert wurde, holen wir uns den aktuellen Namen
// und kopieren ihn in unseren lokalen State
this.name = this.$store.state.preferences.name;
},
data() {
return {
name: '',
},
},
methods: {
setName() {
// Fehler ausgeben, wenn der Name ungültig ist
if (this.name.length === 0) window.alert('Name is too short!');
else this.$store.commit('setName', this.name); // oder den Namen setzen, wenn er gültig ist
},
},
};
</script>
Achtung:
Mutations müssen synchron sein. Falls ihr also asynchronen Code
benötigt, um zum Beispiel dies Daten des Nutzers aus einer Datenbank zu lesen,
benutzt stattdessen Actions. Diese funktionieren ähnlich wie Mutations,
werden aber mit this.$store.dispatch(actionName, actionPayload)
aufgerufen und können asynchron sein. Anstatt den State zu verändern, rufen
sie ihrerseits Mutations auf, um ihn zu verändern.
Wie auch Vue Router, bietet euch Vuex zahlreiche weitere Funktionen an, die State Management vereinfachen. Aber so lange ihr das Grundkonzept des States und der Mutations verstanden habt, könnt ihr schon fast alles damit erreichen. Für fortgeschrittene Themen und detailliertere Beschreibungen, lest euch die Dokumentation von Vuex durch.
Ich weiß, dass es am Anfang unintuitiv erscheinen mag, eine State Management Library wie Vuex zu verwenden, weil es umständlich ist, für alle möglichen Änderungen eine Mutation, oder sogar eine Action und zugehörige Mutations, zu schreiben, aber auf lange Sicht betrachtet wird euch dieser Workflow eine Menge Stress ersparen, also lasst euch darauf ein.
Bonus: Was sind CSS Pre-Prozessoren?
Wenn ihr ein neues Projekt mit der Vue CLI aufsetzt, diese euch fragen, ob ihr CSS Pre-Prozessoren verwenden möchtet. Aber was ist das überhaupt?
In euren Webdesign-Abenteuern seid ihr vielleicht schon einmal über den Begriff SASS oder LESS gestolpert, und ich habe inzwischen wahrscheinlich auch das ein oder andere Mal von Stylus gesprochen. All das sind CSS Pre-Prozessoren, also Sprachen mit ihrer eigenen Syntax und Funktionen, die dann im Build-Step zu nativem CSS übersetzt, bzw. transpiliert werden.
Der Hintergrund ist, das CSS eine sehr rudimentäre Sprache ist, die erst vor kurzem Features wie Variablen erhalten hat und noch immer keine Schleifen oder ähnliches unterstützt. Außerdem wird es sehr schnell sehr umständlich, CSS so zu schreiben, dass auch wirklich nur diejenigen Elemente ausgewählt werden, die man meint. CSS Pre-Prozessoren füllen diese Lücken und machen es angenehmer, CSS zu schreiben.
Ich persönlich benutze nun schon seit Jahren Stylus, aber auch nur, um
geschweifte Klammern und Semikolons wegzulassen, sowie Styles ineinander
zu schachteln und leicht Variablen benutzen zu können. Viele andere Features
wie die Funktionen (außer vielleicht alpha()
, darken()
und lighten()
und
Schleifen benutze ich gar nicht.
Wenn ihr CSS beherrscht, und euch das Leben leichter machen wollt, dann kann ich euch Stylus nur empfehlen (auch wenn SASS bekannter und weiter verbreitet ist). Falls ihr aber noch Anfänger seid und auch CSS nur in den Grundzügen beherrscht, empfehle ich euch, CSS erst einmal richtig zu lernen, bevor ihr einen Pre-Prozessor verwendet.
Hier noch zwei Artikel, die näher auf das Thema eingehen und SASS, LESS und Stylus miteinander vergleichen:
Wenn ihr mehr über Stylus speziell und die Funktionen lernen wollt, die es bereitstellt, schaut auf der Projektseite nach, dort gibt es auch ein interaktives Tutorial, um die Sprache zu lernen.
Praxis
Jetzt wisst ihr alles, was ihr wissen müsst, um mit Vue wirklich durchzustarten, aber natürlich erwartet noch niemand von euch, dass ihr alles verstanden habt. Viel, von dem was wir heute besprochen haben ist sehr abstrakt und in der Theorie viel komplizierter als in der Praxis.
Benutzt meine Beispiele und das Grundgerüst, das die Vue CLI euch generiert als Anhaltspunkte für eure eigenen Apps und probiert einfach herum, es gibt kein richtig oder falsch, so lang es am Ende funktioniert. Wenn ihr euch nicht sicher seid, warum etwas funktioniert (oder nicht funktioniert), lest im Skript oder in der Dokumentation nach – und natürlich stehe ich euch weiterhin für Fragen und Erklärungen zur Verfügung.
Ich sage es noch einmal: die einzige Möglichkeit, wirklich programmieren zu lernen ist, es zu machen.
Technische Konzeption
Trotzdem will ich euch noch ein paar Tipps mit auf den Weg gehen, bevor ich euch zeige, wie ihr mit der Vue CLI ein neues Projekt generiert.
Wenn ihr eure Screens in eurem Design-Tool bereits nach dem Prinzip von Atomic Design aufgebaut habt, habt ihr eigentlich die Struktur, wie ihr sie in der Technik umsetzen sollt, schon vor euch. Geht vom Kleinen ins Große und versucht die einzelnen Bestandteile so einfach wie möglich zu halten.
Ich gehe eigentlich immer so vor:
- Generieren des Projekts mit Vue CLI
- Aufräumen des generierten Codes
- Aufsetzen eines Base-Stylesheets (und einer Datei für die Farbvariablen, wenn ich Stylus benutze)
- Anlegen meiner Grund-Components (Buttons, etc.) anhand der in Figma angelegten Components
- Anlegen der Screens im
views
-Ordner und der dazugehörigen Routen im Router - Aufbauen der Screens aus den Grundkomponenten
- Entwickeln einer Datenstruktur für mein State Management und die Datenbank (mehr dazu in der nächsten Session)
- Verknüpfen der globalen Daten mit den Components
- Feinschliff
- Veröffentlichung
Aufteilen von Funktionalität in Komponenten
Es ist wirklich wichtig, dass ihr in Components denkt, wenn ihr mit Vue arbeitet, denn alles basiert darauf. Dazu müsst ihr die Funktionalitäten eurer Apps auch in Components aufteilen.
Wenn ihr etwas an mehr als einer Stelle in eurer App braucht, steckt es in ein
Component und versucht die einzelnen Components immer so einfach wie möglich
zu halten. Das bedeutet auch, dass ihr unter Umständen eher abstrakte Components
anlegt, die an sich noch nicht wirklich viel bringen und erst durch den Inhalt,
der ihnen z.B. über <slot/>
übergeben wird funktionieren.
Nehmen wir als Beispiel eine Dialogbox (Modal). Wir wissen:
- Dass es viele verschiedene Arten von Dialogboxen gibt (verschiedne Inhalte)
- Dass alle Dialogboxen über dem Rest der Anwendung stehen und alles darunter liegende abdunkeln
- Dass alle Dialogboxen sich schließen lassen wenn:
- Auf einen Punkt außerhalb der Dialogbox geklickt wird
- Ein Schließen-Button geklickt wird
- Ein Bestätigen-Button geklickt wird
Die erste Einsicht stellt klar, dass wir ein Dialogbox-Component brauchen, dass einen flexiblen Inhalt hat. Die anderen Einsichten geben uns einen Rahmen für die Funktionalität.
Hier ist, wie ich es in Vue umsetzen würde:
<!-- ModalExample.vue -->
<template lang="html">
<!-- Wir verwenden <transition> für die Animation -->
<transition>
<!-- Die Dialogbox soll nur angezeigt werden, wenn visible === true -->
<!-- Außerdem soll ein 'close'-Event gesendet werden, wenn irgendwo außerhalb des Containers geklickt wird -->
<div v-if="visible" class="modal" @click.self="$emit('close')">
<div class="container">
<header>
<!-- Wir zeigen den Titel nur an, wenn er auch übergeben wurde -->
<h2 v-if="title">{{title}}</h2>
<!-- Der Schließen-Button sendet ebenfalls ein 'close'-Event bei einem Klick -->
<button type="button" @click="$emit('close')">×</button>
</header>
<!-- Hier kommt der Content der Dialogbox hin, der vom Eltern-Component übergeben wird -->
<slot />
<!-- Der Bestätigen-Button sendet ein 'confirm'-Event bei einem Klick -->
<button type="button" @click="$emit('confirm')">Bestätigen</button>
</div>
</div>
</transition>
</template>
<script>
export default {
props: {
title: String,
visible: Boolean,
},
};
</script>
<style lang="css" scoped>
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
}
.modal.v-enter-active,
.modal.v-leave-active {
transition: opacity 350ms ease;
}
.modal.v-enter-active .container,
.modal.v-leave-active .container {
transition: transform 350ms ease;
}
.modal.v-enter,
.modal.v-leave-to {
opacity: 0;
}
.modal.v-enter .container,
.modal.v-leave-to .container {
transform: translateY(2rem);
}
.modal .container {
background-color: white;
width: 90%;
max-width: 640px;
padding: 2rem;
border-radius: 1.5rem;
font-family: sans-serif;
color: black;
}
.modal .container header {
display: flex;
}
.modal .container header h2 {
margin: 0
}
.modal .container header button {
margin-left: auto;
padding: 0;
width: 2rem;
height: 2rem;
background-color: lightgrey;
border: none;
border-radius: 50%;
cursor: pointer;
}
</style>
Wie ihr seht, nehmen die CSS Styles den größten Teil des Codes ein. Es ist also recht simpel ein solches Component aufzubauen. In der realen Welt würden die Buttons natürlich wahrscheinlich Funktionen aufrufen, die noch etwas anderes machen würden, also nur Events zu emittieren.
Um dieses Component nun einzusetzen, brauchen wir es nur noch innerhalb eines anderen Components zu verwenden, das die richtigen Daten übergibt und auf die Events des Modals reagiert:
<!-- ModalContainer.vue -->
<template lang="html">
<div class="modal-container">
<!-- Wir haben einen Button, der showModal auf 'true' stellt, wenn er geklickt wird -->
<button type="button" @click="showModal = true">Show Modal</button>
<!-- Hier verwenden wir unser Dialogbox-Component und übergeben unsere Daten -->
<!-- Wie ihr seht, wird die Visible-Prop an unsere showModal-Eigenschaft gebunden -->
<!-- Außerdem reagieren wir hier auf die Events, die unser Component ausgibt -->
<ModalExample title="Hello World" :visible="showModal" @close="showModal = false" @confirm="handleConfirm">
<!-- Alles was hier steht wird später innerhalb der Dialogbox angezeigt, wo dort im Template <slot /> stand -->
<h1>Heureka!</h1>
<p>Dieser Inhalt wird vom Eltern-Component über <code><slot/></code> an das Kind-Component weitergegeben!</p>
</ModalExample>
</div>
</template>
<script>
// Wir müssen unser Dialogbox-Component importieren
import ModalExample from './ModalExample.vue';
export default {
components: {
ModalExample, // und es registrieren
},
data() {
return {
showModal: false, // anhand dieser Eigenschaft wird unsere Dialogbox angezeigt oder versteckt
};
},
methods: {
handleConfirm() {
window.alert('Das Modal wurde bestätigt!'); // eslint-disable-line no-alert
this.showModal = false;
},
},
};
</script>
Nach diesem Modell lässt sich jeder Bereich eurer App in kleinere Teile herunterbrechen, die ihr dann immer wieder verwenden könnt. In der nächsten Session werden wir uns den Quellcode der Kurs-App ansehen, die ich geschrieben habe, dann wird dieser Zusammenhang euch hoffentlich noch klarer.
Aufsetzen des Kurs-App-Projekts mit Vue CLI
Im folgenden werden wir sehen, wie einfach sich ein neues Projekt mit Hilfe der Vue CLI aufsetzen lässt. Ihr könntet all dies natürlich auch von Hand machen, aber das wäre deutlich umständlicher und würde voraussetzen, dass ihr die einzelnen Bestandteile versteht. Dank der CLI müssen wir das nicht und bekommmen ein frisches Projekt, in dem wir sofort mit der Programmierung loslegen können.
1. Ordner erstellen und Features auswählen
Öffnet ein Terminal und navigiert in den Ordner, in dem ihr eure Coding-Projekte abspeichert. In meinem Fall ist das der „Git“-Ordner in meinem Home-Verzeichnis.
Das kann ein x-beliebiger Ordner sein, ihr solltet ihn nur wiederfinden können. 😉
# in den Über-Ordner des späteren Projekts wechseln (cd = "change directory")
cd ~/Git
# anstatt @vue/cli global zu installieren und dann zu verwenden, laden wir es
# mit npx für eine einmalige Ausführung herunter und führen es gleich aus
# -p @vue/cli beschreibt das Paket, das wir verwenden wollen
# vue create ist das Kommando, um ein neues Projekt zu erstellen
# projectName ist der Ordner, in dem wir unser Projekt aufsetzen wollen, er wird
# neu erstellt
npx -p @vue/cli vue create projectName
Sobald ihr dieses Kommando ausgeführt habt, wird npm
das Paket für einmalige
Benutzung herunterladen und ihr werdet einen Auswahldialog in eurem Terminal
angezeigt bekommen (je nach Internetverbindung kann es einen Moment dauern):
Im Auswahldialog navigieren
Wenn das alles gut abläuft, gibt es einen interaktiven Dialog, der uns dabei hilft die Features für unser Projekt auszuwählen. Wir entscheiden uns, diese manuell zu bestimmen, anstatt ein vorgefertigtes Template zu verwenden. Man navigiert mit Arrow Up und Arrow Down. Enter bestätigt und mit Space kann man in Mehrfachauswahlen einzelne Punkte an- und abwählen.
Wählt jetzt bitte die zweite Option: „Manually select features“ und bestätigt mit Enter. Dann solltet ihr folgenden Bildschirm sehen:
Features
Wir werden für unser Projekt folgende Features verwenden:
- Babel
- PWA Support
- Router
- Vuex
- CSS Pre-Processors (optional)
- Linter / Formatter
Demnach sollte euer Dialog so aussehen, bevor ihr wieder mit Enter bestätigt:
Danach werdet ihr einige spezielle Fragen zu den einzelnen Features beantworten müssen, die ihr Ausgewählt habt.
„Use history mode for router?“
Wenn auf diese Frage mit „y“ für „Yes“ geantwortet wird, werden die einzelnen Routen (Seiten) innerhalb der App direkt an die Domain angehängt (mehr Informationen). Das ist sauberer, erfordert aber eine angebrachte Konfiguration im Hosting. Auf GitLab ist es momentan nur über Workarounds möglich, weshalb ich empfehlen würde, die Frage für den Anfang mit „n“ für „No“ zu beantworten. Natürlich kann diese Einstellung auch im Nachhinein geändert werden.
Ein Beispiel zur Veranschaulichung:
Es existiert eine Route "Settings" mit dem Pfad /settings
.
Bei aktiviertem History-Mode würde diese Route im Browser so dargestellt werden:
https://meine-app.de/settings
.
Wenn der History-Mode nicht aktiv ist, würde sie so dargestellt werden:
https://meine-app.de/#/settings
.
„Pick a linter / formatter config“
Wir werden hier die ESLint + Airbnb config wählen. Im nächsten Schritt können wir auch die Voreinstellung für "Lint on save" aktiviert lassen, was Fehler nach dem Abspeichern direkt anzeigt.
Sonstiges
Ihr könnt die Konfiguration für die einzelnen Module entweder in einer einzigen oder in mehreren dedizierten Dateien anlegen lassen. Ich persönlich finde dedizierte Dateien übersichtlicher. Falls ihr möchtet könnt ihr euch diese Zusammenstellung von Optionen auch als ein Template für die Zukunft abspeichern lassen, aber ich mache es gerne für jedes Projekt neu, damit ich auch nur die Features einbinde, die ich schlussendlich benutzen werde.
Wichtig: diese Einstellungen sind nur eine „Starthilfe“, alles kann im Nachhinein noch verändert werden, z.B. können Module hinzugefügt und entfernt werden. Außerdem macht dieses Programm nichts, was man nicht auch von Hand machen könnte und wer Interesse hat, kann sich gerne damit auseinander setzen, wie man ein solches Projekt von Null auf aufsetzen würde.
Wenn ihr all diese Schritte befolgt habt und die CLI alle Pakete installiert und konfiguriert habt, solltet ihr in eurem Terminal die folgende Nachricht sehen:
🎉 Successfully created project done-in-time.
👉 Get started with the following commands:
$ cd "Euer Projektname"
$ npm run serve
Das bedeutet, dass alles geklappt hat und ihr nun in den Ordner wechseln könnt.
2. Ordnerstruktur
Euer Projektordner (auch Root oder Project-Root genannt) ist ein in sich geschlossenes System (Vue CLI hat ihn sogar schon als Git-Ordner vorkonfiguriert). Hier ein Überblick über die Struktur:
app-name
├── babel.config.js
├── .browserslistrc
├── .editorconfig
├── .eslintrc.js
├── .git
├── .gitignore
├── node_modules
├── package.json
├── package-lock.json
├── public
│ ├── favicon.ico
│ ├── img
│ │ └── icons
│ ├── index.html
│ └── robots.txt
├── README.md
└── src
├── App.vue
├── assets
│ └── logo.png
├── components
│ └── HelloWorld.vue
├── main.js
├── registerServiceWorker.js
├── router
│ └── index.js
├── store
│ └── index.js
└── views
├── About.vue
└── Home.vue
.git
: dieser Ordner beinhaltet alles, was Git benötigt, um diesen Ordner zu verwaltennode_modules
: dieser Ordner enthält alle installierten Module und Zusatzpakete, er wird riesengroß und unübersichtlich, aber ihr braucht euch nicht darum zu kümmern, das machtnpm
für euchpublic
: in diesem Ordner sind alle Dateien enthalten, die einen absoluten Pfad in eurer Anwendung benötigen, zum Beispiel euer Favicon (mehr Informationen)src
: dieser Ordner enthält den Quellcode eurer Anwendung und hier werdet ihr eure meiste Zeit verbringen. „src“ steht für „Source“..gitignore
: diese Datei enthält die Ordner und Dateien, die Git ignorieren sollpackage.json
: in dieser Datei befinden sich die Paketinformationen für euer Projekt,npm
verwaltet diese Datei für euch, aber ihr könnt sie auch von Hand anpassenREADME.md
: diese Datei dient als „About“-Seite für euer Projekt und sollte die wesentlichen Informationen enthalten. Sie wird bei eurem Git-Provider angezeigt, wenn ihr euer Repository im Browser anseht.- Andere Dateien dienen der Konfiguration einzelner Pakete
Wie bereits angemerkt ist der „src“-Ordner der Wichtigste in eurem Projekt. Er beinhaltet alle Dateien, die ihr aktiv „programmiert“ und gliedert sich in mehrere Unterordner:
assets
: hier könnt ihr Bilder ablegen, die ihr innerhalb eurer Anwendung verwenden möchtetcomponents
: in diesem Ordner werdet ihr eure Single-File-Components abspeichernrouter
: in diesem Ordner befindet sich die Konfiguration für Vue Routerstore
: in diesem Ordner befindet sich die Konfiguration für Vuexviews
: hier könnt ihr die „Seiten“ oder „Screens“ eurer App ablegenApp.vue
: die „Kerndatei“ eurer Anwendung, vergleichbar mitindex.html
in normalen Webseitenmain.js
: der Einstiegspunkt des JavaScript-Programms. Hier wird Vue gestartet und ihr könnt globale Skripte, Komponenten, Stylesheets etc. importierenregisterServiceWorker.js
: diese Datei erlaubt es uns leichter PWA-Funktionen zu unserer App hinzuzufügen und auf Events zu reagieren, die dabei entstehen
Ich empfehle euch noch einen weiteren Ordner und eine weitere Datei anzulegen:
# im Root-Folder
mkdir src/styles # einen Ordner für globale Stylesheets (mkdir = make directory)
touch vue.config.js # eine spezielle Konfigurationsdatei für Vue CLI
3. Konfiguration anpassen
vue.config.js
Mit dieser Datei könnt ihr die Standardeinstellungen beeinflussen, mit denen eure Anwendung „verpackt“ wird, wenn ihr bereit seid, sie zu veröffentlichen. Da wir eine Progressive Web App programmieren, können wir hier auch beeinflussen, mit welchem Titel und welcher Akzentfarbe sie angezeigt werden wird, wenn sie auf einem Gerät installiert wird (mehr Informationen zu vue.config.js und den Optionen für PWAs).
Hier die Datei, die ich für „Done in Time“ verwendet habe:
// vue.config.js
module.exports = {
chainWebpack: (config) => { // set the title injected into the HTML template to something other than what’s in package.json
config
.plugin('html')
.tap((args) => {
args[0].title = 'Done in Time'; // eslint-disable-line no-param-reassign
return args;
});
},
outputDir: 'dist', // final bundle will be in the "dist" folder at project root (default)
publicPath: '/', // set to sub-directory if app isn’t running at domain-root (default)
pwa: {
appleMobileWebAppCapable: 'yes', // we provide all UI necessary to not get stuck
appleMobileWebAppStatusBarStyle: 'default', // set to black-translucent' for "true" fullscreen
manifestOptions: {
background_color: '#344D86', // background color of the splash screen
description: 'Keep track of what needs to be done and how long it takes to do so',
orientation: 'portrait', // will lock the app to portrait-mode
start_url: '/', // app will always start at domain root, not a different path
},
msTileColor: '#344D86', // background color of the tile when installed on Windows
name: 'Done in Time', // app name shown under the icon and in the splash screen
themeColor: '#344D86', // color of the window decorations, status bar on Android etc.
workboxOptions: {
skipWaiting: true, // allow upgrading the service worker as soon as a new version is installed
},
},
};
Es gibt noch deutlich mehr Konfigurationsoptionen, aber da alles mit sinnvollen Standardwerten befüllt ist (die auch für alle Optionen verwendet werden, die in dieser Datei nicht vorkommen) könnt ihr einfach nur die Werte in eure Datei eintragen, die für euch relevant sind. Für den Anfang reichen folgende vollkommen aus:
pwa.themeColor: *eure farbe*
pwa.msTileColor: *eure farbe*
pwa.workboxOptions.skipWaiting: true
„package.json“
Die „package.json“-Datei ist Dreh- und Angelpunkt eures Projekts in den Augen
von npm
. In ihr befindet sich Metadaten wie der Name eures Projekts, die
aktuelle Versionsnummer, Skripte, die ausgeführt werden können, und ganz
wichtig: die Abhängigkeiten eures Projekts. Dabei wird zwischen Entwicklungs-
Abhängigkeiten („dev-dependencies“) und allgemeinen Abhängigkeiten
(„dependencies“) unterschieden. Erstere sind nur nötig, wenn ihr an eurem
Projekt programmiert und werden nicht mit der fertig verpackten Anwendung
ausgeliefert. Die gelisteten Abhängigkeiten sind auch immer mit der minimalen
Version versehen, mit der sie zu installieren sind.
Ihr könnt die Abhängigkeiten wie folgt über euer Terminal verwalten:
npm install # installiert alle dependencies und dev-dependencies in der package.json nach node_modules
npm install --production # installiert nur die dependencies und nicht die dev-dependencies in der package.json nach node_modules
npm install paketname [...paketname] # installiert alle gelisteten Pakete und speichert diese als dependencies
npm install -D paketname [...paketname] # installiert alle gelisteten Pakete und speichert diese als dev-dependencies
npm outdated # listet alle Pakete auf, die nicht mehr die aktuellste version sind
npm update # aktualisiert alle Pakete auf die neuste Feature- oder Patch-Version, aber niemals auf eine höhere Major-Version
npm uninstall paketname [...paketname] # entfernt alle gelisteten Pakete aus node_modules und den dependencies
npm uninstall -D paketname [...paketname] # entfernt alle gelisteten Pakete aus node_modules und den dev-dependencies
Die Skripte, die Vue CLI euch standardmäßig anlegt sind die folgenden:
serve
zum Starten des Entwicklungsserversbuild
zum „Verpacken“ der Anwendung in ein Paket, das auf einen Hosting-Server abgelegt werden kannlint
zum Überprüfen eurer Dateien auf Programmierfehler
Diese Skripte könnt ihr in eurem Terminal ausführen, indem ihr einfach npm run
gefolgt vom Namen des Skripts eingebt, z.B. npm run build
zum Verpacken eurer
Anwendung.
In meiner „package.json“-Datei nehme ich generell nur eine einzige Änderung vor,
ich ändere das Kommando für den Start des Entwicklungsservers von „serve“ auf
„dev“, weil ich es so gewohnt bin. Das ist natürlich rein optional, aber ich
finde npm run dev
ist schneller getippt als npm run serve
. 😉 Ihr müsst
das natürlich nicht ändern.
// package.json
{
//...
"scripts": {
"dev": "vue-cli-service serve" // war vorher "serve": "vue-cli-service serve"
//...
}
//...
}
ESLint-Regeln
Wenn ihr eure Modul-Konfigurationen in dedizierte Dateien ablegen habt lassen, habt ihr jetzt eine Datei mit dem Namen ".eslintrc.js" im Hauptverzeichnis (Root-Folder) eures Projekts (im Dateibrowser / Finder wird die Datei eventuell nicht angezeigt, weil sie mit einem Punkt (.) beginnt, aber in Atom könnt ihr sie sehen). In dieser Datei könnt ihr die Regeln für euren Linter anpassen, zum Beispiel, wenn euch eine bestimmte Regel ganz besonders stört.
Falls ihr keine dedizierten Konfigurationsdateien verwendet, könnt ihr diese Änderungen unter „eslintConfig“ in der „package.json“-Datei anwenden.
Was ich hier ganz gerne mache ist die max-len
Regel zu deaktivieren, weil ich
in Vue-Components lange Zeilen übersichtlicher finde als viele Zeilenumbrüche
hintereinander, obwohl alles in eine einzige Zeile gehört.
Das könnt ihr auch machen, indem ihr einfach am Ende des rules
-Blocks folgende
Zeile einfügt:
'max-len': 'off', // deaktiviert die Regel für maximale Zeilenlänge
Eure Konfiguration sollte also so aussehen:
// .eslintrc.js
module.exports = {
root: true,
env: {
node: true,
},
extends: [
'plugin:vue/essential',
'@vue/airbnb',
],
parserOptions: {
parser: 'babel-eslint',
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'max-len': 'off',
},
};
„README.md“
Die von Vue CLI generierte „README.md“-Datei ist generell etwas nichts-sagend. Schaut euch zum Beispiel einmal die README.md von Vue an und füllt die eures Projekts etwas mehr aus. Nicht nur für Andere, aber auch für euch selbst, wenn ihr nach Jahren wieder über das Projekt stolpert und euch wundert, was das eigentlich war, und wie es funktioniert.
4. Projekt-Icons
Im Ordner „public“ findet ihr eure „favicon.ico“-Datei, sowie den Unterordner „img“ und dort den Unterordner „icons“. Diese Dateien bestimmen, wie das Icon eurer Anwendung im Browser und später im installierten Zustand auf dem Endgerät aussieht. Die Dateien sind schon korrekt benannt und in ihren jeweiligen Größen abgelegt. Ihr müsst sie lediglich durch eure selbst gestalteten Icons in den richtigen Größen ersetzen. Über die Figma-Exportoptionen könnt ihr schnell aus einem oder zwei Icons die richtigen Größen abspeichern.
Während die Dateien in public/img/icons
allesamt PNGs sind (außer
safari-pinned-tab.svg
), ist public/favicon.ico
eine \*.ico
-Datei, also
eine Art Archiv aus mehreren Dateien. Sie enthält normalerweise PNGs in
folgenden Größen:
- 16x16
- 32x32
- 96x96
Generiert werden kann die Datei auf eurem Rechner mit dem Programm convert
aus
der "ImageMagick"-Suite, die bei Ubuntu über sudo apt install imagemagick
installierbar ist:
# generiere ein favicon aus image.png mit transparentem Hintergrund in 16, 32 und 96px
convert -background transparent image.png -define icon:auto-resize=16,32,96 favicon.ico
Es gibt aber auch zahlreiche Websiten, wie diese, mit denen ihr diese Konvertierung umsetzen könnt. (Photoshop kann es wahrscheinlich auch.)
5. Git Reporsitory
Da Vue CLI den Projekt-Ordner bereits als Git-Ordner eingerichtet hat (wie durch den „.git“-Ordner im Hauptverzeichnis wird), müssen wir nur noch ein neues Projekt in GitLab anlegen und unseren Ordner damit verknüpfen.
Nachdem wir das Projekt bei GitLab angelegt haben, können wir diese Verknüpfung in unserem Terminal vornehmen:
# im Hauptverzeichnis ("projektname.git" mit eurem Projektnamen auf GitLab austauschen!)
git remote add origin https://gitlab.com/hm-webdesign/projektname.git # fügt das remote-Repository als Origin hinzu
git push -u origin --all # "pusht" alle lokalen commits und branches an das remote-Repo
Falls ihr Schwierigkeiten habt, den richtigen Code für euer Repository zu finden, ihr bekommt ihn auch auf GitLab angezeigt, ganz unten auf der Seite eures Projekts unter “Add existing Git Repository“
Von nun an könnt ihr wahlweise über euer Terminal, oder über das Interface in Atom Dateien committen und euch mit eurem Remote-Repository synchronisieren.
Entdecken und Ausprobieren der generierten Boilerplate
Herzlichen Glückwunsch! Ihr habt jetzt eine voll funktionsfähige Entwicklungsumgebung und alle Dateien und Ordner, um mit der Umsetzung eurer App zu starten! 🎉
Wechselt mit eurem Terminal in euren Projektordner, falls ihr es noch nicht
getan habt, und führt npm run serve
aus, um den Entwicklungs-Server zu starten.
Dann könnt ihr einfach die ausgegebene URL (meistens localhost:8080
) in eurem
Browser öffnen. Diese Ansicht aktualisiert sich automatisch, wenn ihr Änderungen
an eurem Code vornehmt – ganz ähnlich wie die Live-Preview, die ihr vielleicht
noch von Brackets kennt.
Nehmt euch bitte den Rest der Zeit, um die Dateien, die die Vue CLI für euch
angelegt hat, anzusehen und zu verstehen, wie alles zusammenhängt.Ihr könnt
natürlich auch die vorgefertigten Dateien assets/logo.png
, components/HelloWorld.vue
,
views/About.vue
löschen (WICHTIG: dann natürlich auch alle Referenzen zu
diesen Dateien in eurem Code entfernen!) und damit anfangen, eure eigenen
Components anzulegen.
Das Schlimmste ist vorbei. 😉 Ihr könnt euch jetzt voll und ganz auf die Umsetzung eurer Apps konzentrieren, und solltet jetzt 90% des Wissens haben, das ihr dafür benötigt. In der nächsten Session werden wir uns ansehen, wie wir Daten permanent auf den Geräten unserer Nutzer abspeichern können (die sogenannte „Persistenz“) und uns damit befassen, welche Vorteile ein Backend hat und weshalb man es meistens nicht braucht. 😊