Übersicht
Repository
https://github.com/Thomas-A-Reinert/nuxtjs-starwars-tutorial/tree/teil_3
Was lerne ich in diesem Tutorial?
- Anlegen und Einbinden eigener NUXT-Plugins
- Asynchrone Requests auf REST-APIs mit Axios
- Javascript Promises vs. Async/Await
- Loops mit
v-for
- Sortierfunktion mit Lodash
- Einbinden der Inhalte von „The Star Wars API“ mit Template Literals in Vue
Voraussetzungen
- Grundlegende Kenntnisse in
- HTML
- CSS bzw. SCSS
- Javascript ES6 / ECMAScript 2015
- Eingaben in der Kommandozeile („Konsole“)
- JSON (JavaScript Object Notation)
- Ein Code-Editor (Empfehlung: Visual Studio Code)
Quellen
Schwierigkeitsgrad
Mittel
Vorherige Teile
Update vom 27.09.2019:
- Kleine Text-Korrekturen
- Kleine inhaltliche Korrekturen
- Vereinfachungen
- Hinweis und möglicher Fix zu Fehlermeldung bei Lodash-Installations hinzugefügt
Rückblick
Im den letzten Kapiteln haben wir NUXT.JS eingerichtet und uns angesehen, wie wir eigene Komponenten für die Navigation und den Footer einrichten und diese in die Template-Struktur einbinden.
Im letzten Kapitel haben wir uns ausschließlich um die Kosmetik gekümmert, indem wir SCSS aktiviert und einige Anpassungen an das „Star Wars Look & Feel“ vorgenommen haben.
Den letzten Stand könnt ihr euch wie immer unter dem oben angegebenen GitHub-Repository herunter laden.
Neuen Branch mit Git einrichten
Wie immer sollten wir einen neuen Branch mit Git einrichten – da ich das aber nun bereits zweimal wiederholt habe, schau im Zweifel einmal in die zurückliegenden Kapitel…
Axios und Lodash installieren
Axios haben wir bereits bei der Einrichtung von NUXT.JS aktiviert, deshalb brauchen wir uns darum nicht mehr zu kümmern.
Natürlich könnten wir auch selbst Funktionen schreiben, um unsere via REST-API erhaltenen Ergebnisse zu filtern und sortieren. Als Programmierer bin ich aber per Definition faul, möchte für euch Leser außerdem den Einstiegslevel möglichst gering halten und nutze deshalb lieber eines der beliebtesten NPM-Pakete zu diesem Zweck: Lodash.
Lodash können wir mit folgendem Konsolenbefehl installieren:
$ npm i --save lodash
Axios Plugin anlegen
Wenn wir uns die SWAPI etwas genauer anschauen, sehen wir dass alle Queries eine gemeinsame Base-URL (https://swapi.co/api/) und unterschiedliche Endpoints (films, people, planets, species, starships, vehicles) haben.
Deshalb können wir die Base-URL auch in unser Plugin hardcoden und lediglich bei der Query unterscheiden wir ggf.
import axios from 'axios' export default axios.create({ baseURL: 'https://swapi.co/api/' })
Datei ./plugins/axios.js
Das Plugin speichern wir im Ordner ./plugins
und müssen es nun nur noch dort per Import einbinden, wo wir es verwenden wollen. Grundsätzlich könnten wir in dieser Datei auch einen größeren Teil der Programmlogik ablegen, für unsere Übung soll das aber reichen.
Javascript Promises vs. Async/Await
Die herkömmliche Methode Rest-APIs einzubinden, ist die native Javascript-Methode mit Promises.
Ich bevorzuge allerdings die „modernere“ Methode via Async/Await, die – wie auch Promises – „non-blocking“ (das Script kann im Hintergrund weiter ausgeführt werden) ist, aber verschiedene Vorteile bietet:
- Subjektive Meinung: Besser/einfacher lesbarer Code
- Kürzerer Code
- Bessere Möglichkeiten beim Error-Reporting
Das sollten für uns an dieser Stelle erst einmal die wichtigsten Vorteile sein.
Wer gerne mehr darüber erfahren möchte, sollte sich den Artikel „From JavaScript Promises to Async/Await: why bother?“ auf Pusher durchlesen.
Wir werden uns dennoch beiden Möglichkeiten widmen und schauen uns deshalb erst einmal die Möglichkeit mittels herkömmlicher Promises an.
Daten der SWAPI REST-API mit Promises einbinden und darstellen
Hinweis: Was wir im Folgenden machen, ist nicht wirklich elegant – nämlich die Komponente zum Abruf und Darstellung der Daten direkt in eine Seite zu integrieren. Der Einfachheit halber machen wir es aber erst einmal genau so und überlegen uns später, wie wir stattdessen sinnvoll eine Komponente anlegen.
<template> <section class="container"> <div> <h1 class="title"> Star Wars Films </h1> <h2 class="subtitle text-primary mb-5"> – A Nuxt.js project – </h2> </div> <b-card-group deck> <b-card v-for="item in films" :key="item.episode_id" class="mb-5" > <b-list-group> <b-list-group-item> <b-card-title class="text-primary"> {{ item.title }} </b-card-title> </b-list-group-item> <b-list-group-item> <div class="opening-crawl text-primary"> {{ item.opening_crawl }} </div> </b-list-group-item> <b-list-group-item> <strong>Episode:</strong> {{ item.episode_id }} </b-list-group-item> <b-list-group-item> <strong>Director:</strong> {{ item.director }} </b-list-group-item> <b-list-group-item> <strong>Producer:</strong> {{ item.producer }} </b-list-group-item> <b-list-group-item> <strong>Release Date:</strong> {{ item.release_date }} </b-list-group-item> </b-list-group> </b-card> </b-card-group> </section> </template> <script> import axios from '~/plugins/axios' export default { components: {}, asyncData() { return axios.get('films/').then(response => ({ films: response.data.results })) } } </script> <style></style>
Datei ./pages/index.vue
Import von Axios und asynchrones Abrufen der Daten von SWAPI
Im Script-Bereich müssen wir zunächst unser Plugin importieren. Dadurch dass wir den export default
als „axios
“ importieren, können wir auch über diesen Namen auf die Funktion zugreifen, die uns alle Funktionen von Axios bereit stellt. Siehe dazu auch die Dokumentation von Axios.
Dazu nutzen wir in Zeile 51 die Promise-Funktion von NUXT.JS und übergeben der Axios „get
„-Funktion in Zeile 52 unseren SWAPI REST-Endpoint „films“.
Mit .then
warten wir die Antwort ab und übergeben das empfangene JSON-Object dem Daten-Parameter „films
„. Grundsätzlich sollten wir an dieser Stelle auch Fehler abfangen, kneifen uns das aber der Einfachheit halber und integrieren diese Anforderung später bei Async/Await.
Hinweis: Was wir empfangen, steht uns lediglich als response.data
zur Verfügung. Wir greifen direkt auf ein Unterobjekt, nämlich results
zu. Sieh dir dazu einmal das empfangene JSON an – die Filme selbst stecken alle im Unterobjekt „results“.
Integration der REST-Daten in die „Star Wars Films“-Anwendung
Die Idee: Ich möchte die „Card Deck Groups“ aus BootstrapVue nutzen und für jeden Film eine eigene Card generieren.
In jeder Card sollen
- der Titel des Films
- der „Opening Crawl“, also die Laufschrift am Anfang jedes Films
- die Nummer der Episode
- der Film-Director
- der/die Produzent/en des Films
- das Erscheinungsdatum des Films
angezeigt werden.
Grundsätzlich könnten wir weitere Informationen einbinden bzw. verknüpfen, denn im JSON-Objekt schlummern weitere Informationen.
So könnten wir zum Beispiel später auch im Kontext die im Film auftauchenden Charaktere, Planeten uvm. filtern und anzeigen.
Mal schauen 😉
In Code übersetzt bedeutet das: Wir müssen wie in einer for-Schleife durch alle Einträge loopen und für jeden Eintrag dann die entsprechenden Infos abrufen.
v-for
Schleifen
In den Zeilen 12-16 machen wir genau das.
Das Element <b-card>
erhält den v-for
Parameter, der zwei Argumente erwartet:
Zum einen den Namen für den Iterator, den wir verwenden möchten, in unserem Fall nennen wir es „item
“ . Zum Zweiten welches Objekt/Array wir durchlaufen möchten, in unserem Fall haben wir über Axios das Objekt als „films
“ deklariert. Damit steht uns jedes einzelne Element innerhalb der Schleife als „item
“ zur Verfügung.
Außerdem erwartet NUXT.JS einen eindeutigen Key in v-for
– wir nutzen dafür einen numerischen Key aus jedem einzelnen Unterobjekt bzw. Film, nämlich die episode_id
.
Innerhalb des Templates können wir die Texte nun durch Template-Literals wie zum Beispiel {{ item.title }}
darstellen, die du wie Platzhalter verstehen kannst.
Styling
Das funktioniert soweit, die Console schmeißt auch keine Fehler, allerdings sieht man noch nicht viel wegen der Farben, die wir vergeben haben.
Deshalb müssen wir noch ein wenig an den Styles arbeiten.
Ich ersetze einfach den kompletten <style>-Bereich in der default.vue
wie folgt ohne näher darauf einzugehen:
@import '~assets/scss/bootstrap-variables.scss'; @import '~bootstrap/scss/bootstrap.scss'; @import '~bootstrap-vue/src/index.scss'; body { padding-top: 100px; padding-bottom: 100px; background-color: $black; color: $light; } /* Navigation Styling */ .navbar-brand { #header-logo { max-height: 2rem; margin-right: 1.5rem; } h1 { font-family: "Star Wars", Arial, Helvetica, sans-serif; font-size: 1.25rem; } } /* Containers Styling */ .container { min-height: calc(100vh - 3rem - (1.25rem + 3.25rem + 1rem) -200px; text-align: center; } /* Headline Styling */ .title { font-family: "Star Wars", Arial, Helvetica, sans-serif; font-weight: normal; font-size: 3rem; color: $dark; text-shadow: 0px 0px 3px transparentize($primary, 0.5), -1px 1px 3px transparentize($primary, 0.5), 1px -1px 3px transparentize($primary, 0.5), -1px -1px 3px transparentize($primary, 0.5); } .subtitle { font-weight: 300; font-size: 2.5rem; } /* Card Styling */ .card { /* Default width for mobile-first */ min-width: 100%; background-color: $gray-900; .card-title { font-family: "Star Wars", Arial, Helvetica, sans-serif; } .opening-crawl { transform-origin: 50% 100%; transform: perspective(250px) rotateX(20deg); } } @media (min-width: 768px) { /* Width for large Smartphones and higher */ .card { min-width: calc(50% - 30px); } } .list-group { border: 1px solid transparentize($white, 0.5); box-shadow: 8px 8px 5px transparentize($black, 0.5); } .list-group-item { background-color: $black; border: none !important; border-bottom: 2px solid transparentize($white, 0.5) !important; &:last-child { border: none !important; } } /* Link Styling */ .nuxt-link-exact-active { color: $primary !important; } /* Transition Styling */ .page-enter-active, .page-leave-active { transition: opacity 0.5s; } .page-enter, .page-leave-to { opacity: 0; }
Datei: ./layouts/default.vue
Damit sollte das Ergebnis nun so aussehen:
Großartig! Die Daten werden erfolgreich empfangen und dargestellt, das Styling passt auch zum Thema und in der Console sollten wir auch keine Fehler erhalten.
Ich habe nur drei kleine Probleme mit der Lösung:
- Die Filme werde nicht nach der Episode sortiert – sondern einfach so, wie sie im JSON notiert sind. Das ist unschön.
- Die Lösung mit Async/Await ist eleganter.
- Wir haben keine Reaktion auf mögliche Fehler – Daten können aus welchem Grund auch immer nicht empfangen werden – eingebaut.
Sortieren der Daten mit Lodash
Lodash haben wir bereits installiert, wiederum müssen wir das Modul in den Dateien, in denen wir es nutzen wollen, importieren.
Zur Definition von Funktionen stellt Vue.js/NUXT.JS verschiedene Möglichkeiten wie methods
, computed
, created
, watchers
und mehr zur Verfügung. Was davon wozu genau taugt und wann/wo greift, schauen wir uns vielleicht später einmal an. Wenn du magst, solltest du schon einmal einen Blick auf einen recht guten Artikel bei Sitepoint zu den Unterschieden und Verwendungen lesen.
Wichtig ist für uns an dieser Stelle erst einmal nur computed
. das für uns aus folgenden Gründen entscheidende ist:
computed
greift nur beim ersten Aufruf der Seite- wir haben die Möglichkeit vor dem ersten Rendern eine Funktion zu übergeben, die die empfangenen Daten wunschgemäß sortiert, filtert oder, oder, oder..
- solang die Daten nicht irgendwie modifiziert werden, werden sie im Cache abgelegt
- das erspart uns teilweisem Neurendern der Anwendung aufwändige Rechenoperationen
Dazu ersetzen wir den <script>
-Bereich in index.vue
wie folgt:
<script> import _ from 'lodash' import axios from '~/plugins/axios' export default { components: {}, computed: { filmsOrderedByID: function() { const filmsOrdered = _.orderBy(this.films, 'episode_id') return filmsOrdered } }, asyncData() { return axios.get('films/').then(response => ({ films: response.data.results })) } } </script>
Datei: ./pages/index.vue
In Zeile 2 importieren wir zunächst Lodash als „_
„, in den Zeilen 7-12 fügen wir eine neue computed-Property namens „filmsOrderedByID
“ ein.
Darin steckt eine Funktion, die für den Wert „filmsOrderedByID
“ nun den Rückgabewert der Funktion beinhaltet.
Für die Sortierung ist der String _.orderBy(this.films, 'episode_id')
verantwortlich, der die orderBy
-Funktion von Lodash nutzt. Diese erwartet (minimal) zwei Parameter:
- was sortiert werden soll:
this.films
- den Wert nach dem sortiert werden soll:
episode_id
Damit stehen uns die sortierten Daten in einem neuen Objekt namens filmsOrderedByID
zur Verfügung, deshalb müssen wir auch unseren v-for
Loop anpassen, da wir auf das neue Objekt zugreifen wollen:
<b-card v-for="item in filmsOrderedByID" :key="item.episode_id" class="mb-5" >
Datei ./pages/index.vue
Und damit klappt nun endlich auch die Sortierung. Natürlich kannst Du auch nach jedem weiteren Feld sortieren und das sogar auf- oder absteigend. Wirf dazu einen Blick in die orderBy-Funktion von Lodash.
Optimierung: Async/Await statt Javascript Promises
Unsere Anwendung funktioniert zwar einwandfrei, dennoch will ich dir die elegantere Lösung mit Async/Await nicht vorenthalten.
Wir ersetzen den asyncData() { .. }
Bereich durch folgenden Code:
// Axios with Async/Await async asyncData({ error }) { const films = await axios .get('filmsx/') .then(response => ({ // Handle Success films: response.data.results })) .catch(e => { // Handle Errors, generate 404 Status with Message error({ statusCode: 404, message: 'Endpoint could not be resolved' }) }) return films }
Datei: ./pages/index.vue
Wenn du jetzt speicherst und die Seite neu aufrufst, siehst du die Fehlermeldung und in der Console den Statuscode, die wir im Errorhandling mit eingebaut haben.
Meiner ganz persönlichen Meinung nach ist dieser Code erheblich besser lesbar als das vorherige Promise.
Und warum erhalte ich die Fehlermeldung? Schau in Zeile 4: dort steht „filmsx
“ – entferne einfach das x, speichere und lade die Seite neu. Sie wird dann wie erwartet dargestellt.
Ich wollte den Fehler und die Meldung nur provozieren um zu veranschaulichen, dass man mit Async/Await differenzierte Fehlermeldungen besser ausgeben kann 😉
Fazit
Du hast gelernt, wie einfach es ist, erste Erfolge mit Vue.js und Nuxt.js zu erzielen und einen Einblick in die Funktionsweise und Struktur der Frameworks erhalten.
Dabei hast du hoffentlich auch festgestellt, wie schnell die ganze Anwendung funktioniert – und das, obwohl sie sich die Daten aus einer externen Quelle abholen muss.
Grundsätzlich könntest du mit einer leicht abgewandelten Variante nun auch eine Lösung entwickeln, die sich aus jeder möglichen Quelle, die dir eine REST-Api bietet, andockt.
Beispiele dafür wären:
- die PokéAPI, wenn du mit lieber mit Pokémon statt Star Wars arbeiten möchtest..
- TheCocktailDB, wenn du Cocktails liebst..
- aber natürlich auch dein eigenes WordPress als Headless CMS..
Die Möglichkeiten sind schier unendlich – viel Spaß beim Ausprobieren 🙂