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

  1. Was ist npm und wie verwendet man das?
  2. Was bedeuten diese Versionsnummern?
  3. Was sind Webpack, Babel & Co?
  4. Was ist JS-Fatigue und wie beugt man dem vor?
  5. Wie sind Vue-Projekte aufgebaut?
  6. Exkurs: Was genau sind jetzt Progressive Web Apps?
  7. Vue-Instanzen und ihre Optionen
  8. Components im Detail
  9. Spezielle Attribute is, ref und key
  10. Wie Funktioniert v-bind mit class und style?
  11. Wie baut man coole Animationen?
  12. Wann braucht man Routing und State-Management?
  13. Wie benutzt man das?
  14. Bonus: Was sind CSS Pre-Prozessoren?
  15. Praxis

Was ist npm und wie verwendet man das?

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 werden
  • created() 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 wird
  • mounted() wird aufgerufen nachdem die Instanz an das DOM gehängt wurde. Das bedeutet, dass this.$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 mit this.$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 habt
  • updated() wird aufgerufen, nachdem Daten geändert wurden und das DOM aktualisiert wurde. Funktioniert wie mounted(), nur eben nach Updates
  • beforeDestroy() wird aufgerufen, bevor eine Vue-Instanz zerstört wird. Hier solltet ihr EventListener entfernen, die ihr zuvor händisch hinzugefügt habt
  • destroyed() 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:

  1. 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 Components data wie folgt angeben müsst:

    {
      //…andere Optionen…
      data() {
        return {
          message: 'Hello World!',
        };
      },
      //…andere Optionen…
    };
  2. 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 keyupvom 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, wenn event.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 manuell event.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 nur keyup 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.

Emulation von v-model

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 eine value-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.

Spezielle Attribute is, ref und key

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.

Mit key Einzigartigkeit Signalisieren

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.

Mit is und <component> dynamische Components verwenden

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>
Todo 1
Todo 2
Todo 3

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.

Mit ref Referenzen auf DOM-Elemente und Components erhalten

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 $refsnicht 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>

Wie Funktioniert v-bind mit class und style?

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.

Objekt-Syntax für class

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.

Array-Syntax für class

Ü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>

Objekt-Syntax für style

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.

Das <transition> Component

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 und v-leave-active sind über die gesamte Dauer der Transition auf dem Element vorhanden. In ihnen definiert ihr eure Werte für die transition-Eigenschaft im CSS
  • v-enter und v-leave sind die „Startzustände“ eurer Animation und werden aktiviert, sobald die Animation beginnt und nach einem Frame wieder entfernt
  • v-enter-to und v-leave-to sind die „Endzustände“ eurer Animationen, sie werden aktiviert, sobald v-enter und v-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.

Das <transition-group> Component

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 dem tag-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:

  1. Generieren des Projekts mit Vue CLI
  2. Aufräumen des generierten Codes
  3. Aufsetzen eines Base-Stylesheets (und einer Datei für die Farbvariablen, wenn ich Stylus benutze)
  4. Anlegen meiner Grund-Components (Buttons, etc.) anhand der in Figma angelegten Components
  5. Anlegen der Screens im views-Ordner und der dazugehörigen Routen im Router
  6. Aufbauen der Screens aus den Grundkomponenten
  7. Entwickeln einer Datenstruktur für mein State Management und die Datenbank (mehr dazu in der nächsten Session)
  8. Verknüpfen der globalen Daten mit den Components
  9. Feinschliff
  10. 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:

  1. Dass es viele verschiedene Arten von Dialogboxen gibt (verschiedne Inhalte)
  2. Dass alle Dialogboxen über dem Rest der Anwendung stehen und alles darunter liegende abdunkeln
  3. 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>&lt;slot/&gt;</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):

Ein Screenshot der Vue CLI

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:

Ein Screenshot der Vue CLI

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:

Ein Screenshot der Vue CLI

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“

Ein Screenshot der Vue CLI

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

Ein Screenshot der Vue CLI

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 verwalten
  • node_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 macht npm für euch
  • public: 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 soll
  • package.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 anpassen
  • README.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öchtet
  • components: in diesem Ordner werdet ihr eure Single-File-Components abspeichern
  • router: in diesem Ordner befindet sich die Konfiguration für Vue Router
  • store: in diesem Ordner befindet sich die Konfiguration für Vuex
  • views: hier könnt ihr die „Seiten“ oder „Screens“ eurer App ablegen
  • App.vue: die „Kerndatei“ eurer Anwendung, vergleichbar mit index.html in normalen Webseiten
  • main.js: der Einstiegspunkt des JavaScript-Programms. Hier wird Vue gestartet und ihr könnt globale Skripte, Komponenten, Stylesheets etc. importieren
  • registerServiceWorker.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 Entwicklungsservers
  • build zum „Verpacken“ der Anwendung in ein Paket, das auf einen Hosting-Server abgelegt werden kann
  • lint 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. 😊