3. Nuxt¶
3.1. Einleitung¶
Was ist Nuxt?
Nuxt [NuxtDokumentation] ist ein leistungsstarkes Web-Framework, das auf Vue.js basiert und es Entwicklerinnen und Entwicklern ermöglicht, einfach und schnell ansprechende Single-Page-Applikationen zu erstellen. Im Vergleich zu anderen Frameworks wie Angular oder React zeichnet sich Nuxt durch seine einfache Handhabung und die hohe Flexibilität aus.
Eine der Stärken von Nuxt liegt in seinem routing system, das automatisch generiert wird und eine SEO-freundliche URL-Struktur bietet. Darüber hinaus bietet Nuxt zahlreiche Erweiterungen und Plugins, die die Entwicklung von Webanwendungen noch weiter vereinfachen.
Nuxt eignet sich besonders gut für kleinere bis mittlere Webprojekte, bei denen es darum geht, schnell eine benutzerfreundliche und performante Website zu erstellen. Auch für Entwickler, die bereits mit Vue.js vertraut sind, bietet sich Nuxt als Framework an, da es die Entwicklung von Webanwendungen deutlich beschleunigt und Code-Wiederholungen minimiert.
In dieser Dokumentation werden wir einen Einblick in die Entwicklung eines Software-Projekts geben, das mit Hilfe von Nuxt umgesetzt wurde. Wir werden auf die Vor- und Nachteile von Nuxt im Vergleich zu anderen Sprachen eingehen und aufzeigen, für welche Art von Projekten Nuxt besonders gut geeignet ist.
Unser Projekt:
Ziel unseres Projektes war die Erstellung einer kleinen Applikation, mithilfe derer eine Fahrradtour geplant werden kann. Dabei sollten NutzerInnen eine Liste an Städten angeben können, die sie im Laufe der Tour besuchen möchten. Die App berechnet ihnen dann, in welcher Reihenfolge diese Städte abgefahren werden können, sodass optimale Wetterverhältnisse in der jeweiligen Stadt am Tag des Besuchs herrschen.
Dabei stellte es sich als eine Herausforderung dar, diese Anforderung zu operationalisieren. Zunächst wurde die Möglichkeit untersucht, das Problem im Sinne eines Travelling Salesman Problems (TSP) abzubilden, d.h. die optimale Rundreise zwischen den Städte-Knoten zu planen. Theoretisch wäre es möglich, die Kantengewichte mit Distanzen zwischen Städten und einem „Gut-Wetter-Faktor“ zu berechnen, jedoch wurde schnell klar, dass die zusätzliche zeitliche Komponente hier problematisch sein würde. Der TSP Graph würde sich ständig verändern, da es tägliche Wetterveränderungen geben wird.
Daher haben wir uns für dafür entschieden, das Problem mit einem selbst entwickelten „greedy“ Algorithmus zu lösen. Dieser läuft nach zwei einfachen Schritten ab:
Ausgehend von der Start-Stadt bzw. der aktuellen Stadt wird ermittelt, welche Städte in einer Tagesetappe erreichbar sind.
Unter den erreichbaren Städten wird nach festgelegten Bedingungen ermittelt, wo am nächsten Tag das beste Wetter sein wird.
Die resultierende Stadt wird als nächstes angefahren und dient dann wieder als Startpunkt für die nächste Berechnung. Im Folgenden soll beschrieben werden, wie die Anwendung genau umgesetzt wurde.
3.2. APIs¶
Um zu bestimmen, ob eine Stadt in einer Tagesetappe erreichbar ist, muss eine Route berechnet werden. Hierfür benötigt man eine API, die in der Lage ist, Routen für Fahrräder zu liefern. Dabei haben wir uns nach etwas Recherche für die API von OpenRouteService [OPENROUTESERVICE_API] entschieden.
Zudem brauchten wir eine API, die (kostenlos) mindestens für ein paar Tage Wettervorhersagen liefern kann. Unsere Recherche führte uns hier zur Weatherbit API [WEATHERBIT_API]. Da beide APIs gut dokumentiert sind, konnten die gewünschten Daten relativ einfach abgefragt werden.
3.3. Frontend¶
In der Softwareentwicklung wird häufig zwischen Frontend und Backend unterschieden, um die unterschiedlichen Funktionalitäten und Aufgaben klar zu trennen. Während das Backend die serverseitige Logik und Datenverarbeitung übernimmt, ist das Frontend für die Darstellung und Interaktion mit dem Benutzer verantwortlich.
Die Verwendung eines Frontends bietet eine Vielzahl von Vorteilen gegenüber einer reinen Kommandozeile. Durch die visuelle Benutzeroberfläche können Benutzer intuitiver und effizienter mit der Software interagieren. Dies ermöglicht eine benutzerfreundlichere Erfahrung und erhöht die Usability der Anwendung erheblich.
Benutzer können mit Frontends auf verschiedene Arten interagieren, darunter Mausklicks, Tastatureingaben, Drag-and-Drop-Operationen und Touchgesten. Diese vielfältigen Interaktionsmöglichkeiten erleichtern es Benutzern, schnell und einfach mit der Software zu arbeiten und komplexe Aufgaben auszuführen.
Im folgenden gehen wir weiter darauf ein, wie das Frontend in diesem Projekt im Detail umgesetzt wurde:
Die Datei „Background.vue“ definiert eine Vue.js-Komponente, die einen Container mit einer bestimmten Stilierung erstellt. Der Container hat eine Breite von 85%, einen blauen Hintergrund mit einer Transparenz von 0,94 und einen Innenabstand von 40px. Außerdem ist der Container abgerundet mit einem Radius von 3px. In dem Container wird der Slot platziert, sodass der Inhalt, der innerhalb dieses Containers eingefügt wird, angezeigt wird. Durch die Verwendung des Slots kann der Inhalt dynamisch eingesetzt werden, wenn die Komponente verwendet wird.
Die Datei „City.vue“ stellt eine Wetterkachel auf der Seite dar. Die Komponente hat drei Props: city (Stadtname als String), temp (Temperatur als Zahl) und icon (Icon als String). Im Template wird ein Container mit einem Bild (Icon), dem Stadtnamen und der Temperatur erstellt. Das Bild wird dynamisch mit dem Icon-URL von Weatherbit.io basierend auf dem Wert von icon geladen. Der Container und die darin enthaltenen Elemente werden mit CSS gestylt. Der Container ist ein flexibles Element mit vertikaler Ausrichtung in der Mitte. Das Bild hat eine festgelegte Höhe und Breite und die Absatzelemente haben keinen oberen oder unteren Abstand. Der JS <script setup>-Block definiert die Props und ermöglicht das Verwenden des Props-Objekts in der Komponente.
Die Datei „TourGraph.vue“ erstellt eine Vue-Komponente, die eine Schaltfläche „Create Route“ enthält. Beim Klick auf die Schaltfläche wird die Methode onClicked aufgerufen. Innerhalb der onClicked-Funktion werden Instanzen der Klassen WeatherService und RouteService erstellt.
async function onClicked() {
console.log("onClicked");
let weatherService = new WeatherService();
city_data.value = await weatherService.fetchWeatherData(props.cities);
let routeService = new RouteService(city_data.value, props.dailyRange, props.days);
route.value = await routeService.createRoute();
displayRoute.value = true;
Mit der Methode fetchWeatherData des WeatherService werden die Wetterdaten für die übergebenen Städte abgerufen und in der Variable city_data gespeichert. Dann wird die Methode createRoute des RouteService aufgerufen, um eine Route basierend auf den Wetterdaten, der täglichen Entfernung und der Anzahl der Tage zu erstellen. Diese Route wird in der Variable route gespeichert und die Variable displayRoute wird auf true gesetzt. In der Template-Sektion wird eine Schleife verwendet, um jede Stadt der erstellten Route anzuzeigen. Jede Stadt wird mit der City-Komponente dargestellt, die den Namen der Stadt, das Wetter-Symbol und die Temperatur zeigt. Die Styling-Regeln sind einfach und definieren das Aussehen der Schaltfläche und die Anordnung der Elemente auf der Seite.
Im Quellcode der Datei „UserInput.vue“ wird eine Vue.js-Komponente erstellt, die eine Multiselect-Auswahl für Städte, Input-Nummernfelder für das tägliche Budget und die Anzahl der Tage sowie ein benutzerdefiniertes Diagramm anzeigt.
Es werden die erforderlichen Komponenten und Referenzen importiert, darunter MultiSelect, InputNumber und ref aus Vue.
Eine Liste von möglichen Städten wird geladen und als Referenzvariable possibleCities gespeichert.
Die ausgewählten Städte werden als Referenzvariable selectedCities gespeichert.
Eine Funktion findCities wird definiert, um die ausgewählten Städte zu extrahieren und in einer separaten Liste cities zu speichern.
Im Template werden die MultiSelect-, InputNumber- und TourGraph-Komponenten gerendert, wobei die entsprechenden Daten und Einstellungen übergeben werden.
Im Style-Bereich wird das Styling für die Input-Felder und das zentrierte Layout des Diagramms definiert.
Der Benutzer kann also Städte auswählen, das tägliche Budget seiner Fahrleistungen und die Anzahl der Tage eingeben, und das Diagramm wird entsprechend aktualisiert, um die Reiseroute basierend auf den ausgewählten Parametern anzuzeigen.
3.4. Services (Logik)¶
Zur Berechnung der wetterabhängig optimalsten Fahrradroute setzen wir zwei Services ein, die sich unterteilen lassen in „WeatherService“ und „RouteService“. Beide Services werden in der Datei „TourGraph.vue“ aufgerufen.
WeatherService:
Der „WeatherService“ dient dazu, die aktuellen Wetterdaten der jeweiligen Stadt mithilfe der [WEATHERBIT_API] abzurufen. Durch die API sind wir limitiert nur das Wetter nächsten 7 Tagen betrachten zu können. Was auch unserer Anwendung ein Limit aufsetzt. Wir schicken eine Anfrage an „https://api.weatherbit.io/v2.0/forecast/daily“ mit den Parameter der Stadt, sowie der 7 Tage eben und unserem API-Key, um Zugriff zu erhalten. Für den jeweiligen Tag setzen wir einen Eintrag in einer Map mit Datum, Temperatur, Regenwahrscheinlichkeit, Windgeschwindigkeit und dem Wettericon. Die Daten legen wir in einer Liste an, in dem jeder Eintrag den jeweiligen Städtenamen mit Längen- und Breitengrad sowie aller Wetterdaten der 7 Tagen darstellt. Zuletzt wird die vollständige Liste noch sortiert, um unserer ursprünglich eingebenen Städtereihenfolge zu entsprechen, da anhand dieser die Startstadt herausgelesen wird. Beschriebene Funktionalität haben wir über folgende Promise gelöst:
const promises = cities.map(async (city) => {
let response = await axios
.get("https://api.weatherbit.io/v2.0/forecast/daily", {
params: {
city: city,
lang: "de",
days: 7,
key: this.apiKey,
},
})
.then(function (response) {
let days = response.data.data.map((day) => ({
date: day.valid_date,
avg_temp: day.temp,
probability_rain: day.pop,
wind_speed: day.wind_spd,
icon: day.weather.icon,
}));
city_data.push({
name: response.data.city_name,
lat: response.data.lat,
lon: response.data.lon,
days: days,
distances: [],
});
})
.catch(function (error) {});
await new Promise((r) => setTimeout(r, 1500));
});
await Promise.all(promises);
Erhaltende Daten können dann im folgenden verwendet werden, um die Fahrradroute zu erstellen.
RouteService:
Im „RouteService“ nutzen wir die [OPENROUTESERVICE_API], um die Route zwischen den gewünschten Städten zu berechnen unter Berücksichtigung der Wetterdaten. Hierfür übergeben wir dem Service unsere Liste der Daten der gewählten Städte, die maximale Anzahl an zu fahren möglichen Kilometern und wie viel Tage wir unterwegs sein wollen. Um eine Route zu kreieren, lassen wir zunächst über die Funktion generateCityCombinations() die Kombinationen der verschiedenen Städten zurückgeben.
generateCityCombinations() {
const cityCombinations = [];
for (let i = 0; i < this.cities.length - 1; i++) {
for (let j = i + 1; j < this.cities.length; j++) {
const city1 = this.cities[i];
const city2 = this.cities[j];
cityCombinations.push([city1.lon, city1.lat], [city2.lon, city2.lat]);
}
}
...
Diesen Rückgabewert verwenden wir im folgenden um uns über die Funktion getCityDistances() und die [OPENROUTESERVICE_API] die Entfernung der tatsächlichen Routen zwischen den Städten berechnen zu lassen. Hierfür nutzen wir den Service Directions von der API sowie die Funktion calculate() dabei geben wir untenstehenden Parameter an um die gewünschten Routendistanzen zu erhalten:
let response = await orsDirections.calculate({
coordinates: cityCoordinates,
profile: "cycling-regular",
format: "json",
units: "km",
});
...
Nun können wir die Distanzen und die Städte zusammenführen, indem wir die zusammengehörigen Distanzen und Städte heraussuchen und unserer Liste an Städten die jeweilige Distanz folglich zuweisen. Dies haben wir über jenen Code gelöst:
async addDistancesToCities(distances) {
distances = distances.filter((_, index) => index % 2 === 0);
let index = 0;
for (let i = 0; i < this.cities.length - 1; i++) {
for (let j = i + 1; j < this.cities.length; j++) {
this.cities[i].distances.push({
from: this.cities[j],
distance: distances[index],
});
this.cities[j].distances.push({
from: this.cities[i],
distance: distances[index],
});
index += 1;
}
}
}
Jetzt haben wir alle nötigen Daten um unsere gesamte Fahrradroute erstellen zu können. Beginnend in der Startstadt (Stadt, welche zuerst ausgewählt wurde) suchen wir nun die Stadt heraus, die in unserer täglichen Maximalreichweite liegt und das beste Wetter am nächsten verglichen mit den anderen erreichbaren Städten bietet. Diese setzen wir als nächste Stadt. Eine Stadt wird mit den Daten zu Längen-, und Breitengrad sowie den Wetterdaten der Route hinzugefügt. Die Wetterdaten umfassen die durchschnittliche Temperatur, die Regenwahrscheinlichkeit und das Wettericon des entsprechenden Tages, an welchem die radfahrende Person, in der Stadt losfährt. Jeden Tag fahren wir nur eine Stadt an. Liegt keine Stadt innerhalb unserer Maximalreichweite wird keine weitere Stadt zur Route hinzugefügt, was heißt, dass die Route in der Startstadt endet. Der Algorithmus läuft so lange bis alle Tage durchgelaufen sind oder keine weitere Stadt mehr befahren werden kann, weil alle Städte bereits in der Route zu finden sind oder eben zu weit entfernt liegen. Unser Algorithmus sieht also wie folgt aus:
let next = this.startCity;
for (let day = 0; day < this.days; day++) {
if (next) {
route.push({
city: next.name,
lat: next.lat,
lon: next.lon,
weather: {
temperature: next.days[day].avg_temp,
prob_rain: next.days[day].probability_rain,
icon: next.days[day].icon,
},
});
if (this.cities && this.cities.length > 0) {
this.cities = this.cities.map((city) => {
city.distances = city.distances.filter(
(d) => d.from.name !== next.name
);
return city;
});
let inRange = await this.getCitiesWithinRange(next);
next = await this.pickBestWeatherCity(inRange, day + 1);
} else {
break;
}
}
}
Nach welchen Kriterien aber entscheiden wir was das beste Wetter ist? Hierfür dient die Funktion pickBestWeatherCity(). Dieser übergeben wir alle Städte, die in unserer Reichweite liegen und den folgenden Tag. Innerhalb dieser Städte werden nun jene herausgesucht, in welchen es recht wahrscheinlich nicht regnen wird. Unser Grenzwert dabei ist eine Regenwahrscheinlichkeit von 30%. Gibt es nun nur eine regenfreie Stadt ist diese automatisch, die Stadt mit dem besten Wetter und wird als nächste Stadt unserer Route hinzugefügt. Gibt es aber mehr regenfreie Städte oder gar keine muss ein anderer Wert betrachtet werden. In diesen Fällen vergleichen wir die Durschnittstemperatur der regenfreien Städte oder eben aller Städte, wenn es in jeder Tag regnet würde, und wählen die wärmste Stadt aus.
Beispiel:
Wir möchten eine Route durch die Städte Augsburg, München, Nürnberg und Hof erstellen lassen. Dabei können wir maximal 200 Kilometer an einem Tag fahren und planen uns 4 Tage Zeit ein für die Tour. Unsere Startstadt ist Augsburg.
Innerhalb unserer Maximalreichweite liegen Nürnberg und München. Wir vergleichen das Wetter am nächsten Tag der beiden Städte. In Nürnberg hat es durchschnittlich 15°C und eine Regenwahrscheinlichkeit von 50%. In München wird es durchschnittlich 10°C werden mit einer Regenwahrscheinlichkeit von 76%. Anhand von unserem BestWeather-Algorithmus wird nun also Nürnberg als nächste Stadt unserer Route hinzugefügt.
Beide übrigen Städte (München und Hof) sind von Nürnberg aus mit maximal 200 km erreichbar, also werden sie nun für Tag 3 unserer Route betrachten. In München wird es mit 10% Regenwahrscheinlichkeit eher trocken bleiben, wobei es in Hof mit 90% Regenwahrscheinlichkeit sehr ungemütlich wird. Die nächste Stadt unserer Route ist also nun München.
Von München sehen wir das die letzte Stadt, Hof, nicht innerhalb unserer Maximalreichtweite von 200km liegt und schließen somit Hof von unserer Radreise aus.
Unser Ergebnis umfasst eine Fahrradtour von 3 Tagen. Wir starten in Augsburg, fahren nach Nürnberg und enden in München, wo wir dann an der Isar unsere Beine entspannt hochlegen können mit einem kühlen Spaten in der Hand.
3.5. Schlusswort¶
Im Allgemeinen ist Nuxt.js ein sehr komfortables Frontend-Framework, das alle Vorteile von Vue.s als leichtgewichtiges und sehr flexibles Tool nutzt, darüber hinaus aber noch einige weitere Vorteile bietet.
Der wohl bemerkenswerteste Unterschied, der uns in der Arbeit mit Nuxt begegnet ist, ist die Möglichkeit des Server-Site-Rendering. Dabei mussten wir feststellen, dass gerade in der Arbeit mit asynchronen Funktionen, darauf geachtet werden muss, dass zwischen server-seitigem und der client-seitigem Rendering konsistente Daten vorliegen, da es sonst zu sogenannten Hydration-Fehlern kommen kann.
Zudem ist es besonders wichtig, sich Gedanken darüber zu machen, an welchem Punkt im Komponenten-Lifecycle Funktionen aufgerufen werden sollten, um doppelte Aufrufe zu verhindern und so unnötigen Overhead zu verhindern.
3.6. Literatur¶
Nuxt: (abgerufen am 01.05.2024): https://nuxt.com/docs/getting-started/introduction
Weatherbit: (abgerufen am 27.04.2024): https://www.weatherbit.io
OpenRouteService (abgerufen am 27.04.2024): https://openrouteservice.org