Fullstack - ASP.NET Entwicklung mit DotNet Core und Vue.js - Teil 6 - Erweiterung um Vuex und Axios

Einleitung

Dieser Artikel ist eine Serie von Artikeln, die den Aufbau einer Fullstack Applikation unter DotNet Core und Vue.js beschreiben.

Bisher sind folgende Artikel erschienen:

  • Fullstack - ASP.NET Entwicklung mit DotNet Core und Vue.js - Erste Schritte Link
  • Fullstack - ASP.NET Entwicklung mit DotNet Core und Vue.js - Teil 2 Link
  • Fullstack - ASP.NET Entwicklung mit DotNet Core und Vue.js - Teil 3 - Einbinden PostgreSQL als Datenbank Link
  • Fullstack - ASP.NET Entwicklung mit DotNet Core und Vue.js - Teil 4 - Erweiterung um ein Repository Pattern und Service Layer Link
  • Fullstack - ASP.NET Entwicklung mit DotNet Core und Vue.js - Teil 5 - Erste Schritte mit Vue.js und Typescript Link

In diesen Artikel versuche ich die Funktonsweise von Vuex und Axios näher zu bringen. Beides sind unverzichtliche Tools um mit Vue in größeren Umgebungen erfolgreich zu arbeiten.

Axios bietet deutlich mehr Funktionalitäten als der "fetch". In diesen Artikel werde ich aber nur ein rudimentäre Implementierung und die Erweiterung auf Services durchführen.

Mit Vuex bekommt die Front-End Applikation einen zentralen Datenstore, von dem alle Komponenten Lesen und Schreiben können. Vuex bringt allerdings auch einiges an zusätzlicher Komplexität mit sich, die ich versuche in diesen Artikel näher zu bringen.

Was ist Vuex?

Vuex ist ein sogenanntes State Management Pattern und ist für die Verwaltung eines zentralen Datenspeichers in denen der aktuelle Zustand der Front-End Applikation zentral gespeichert werden kann. Üblicherweise wird hier die aktuelle User-Kennung, Session-Keys, aber auch wichtige zentrale Daten zwischengespeichert.

Änderungen die in diesen Datenspeicher gemacht werden, werden allen Komponenten mitgeteilt. Häufig wird die Backend API in den States hinterlegt und ich werde es mit den Forecat Daten in diesen Beispiel demonstrieren.

Anbei das Schaubild von VUEX:

Das Bild stammt von https://vuex.vuejs.org/en/intro.html, dass auch eine sehr gute Einführung in das Thema bietet.

Wie ist dieses Diagramm zu verstehen?

Der Ablauf beginnt bei der Vue Componente und läuft durch folgende Schritte:

  • Die Vue Componente möchte ein Update oder ähnliches anschieben und Started (Dispatched) asynchron eine "Aktion".
  • Die Aktion kann falls notwendig auf Daten vom Backend zugreifen, da diese Asyncron läuft. Eine Aktion kann aber auch sein, dass nur direkt eine Mutation angestartet werden soll.
  • Nur eine Mutation hat das Recht einen State zu ändern. Das ist notwendig, damit anschließend das notwendige Renderings aller Komponenten gestartet, die diese States verwenden. (Die Getter werden ausgeführt)

Installation von Vuex und Axios

Die Installation von Vuex gestaltet sich einfach:

npm install vuex --save-dev  

und natürlich auch von Axios:

npm install axios --save-dev  

Einbinden von Vuex

Zunächst definieren wir einen "einfachen" Datenstore, der die aktuelle Version verwalten soll.

Hierzu wird ein neues Verzeichnis "types" angelegt und dort eine Datei "RootState". Diese wird aktuell mit nur einen Attribut "version" erstellt:

export interface RootState {  
    version: string;
}

Anschließend wird die Datei "boot.ts" um eine Verlinkung auf Vueex erweitert und ein Storage für unseren RootState hinterlegt:

import './css/site.css';  
import 'bootstrap';  
import Vue from 'vue';  
import VueRouter from 'vue-router';  
import Vuex, { StoreOptions } from 'vuex';  
import { RootState } from './types/RootState';

Vue.use(VueRouter);  
Vue.use(Vuex);

const store: StoreOptions<RootState> = {  
    state: {
        version: '1.0.0'
    }
}

const routes = [  
    { path: '/', component: require('./components/home/home.vue.html') },
    { path: '/counter', component: require('./components/counter/counter.vue.html') },
    { path: '/fetchdata', component: require('./components/fetchdata/fetchdata.vue.html') }
];

new Vue({  
    el: '#app-root',
    router: new VueRouter({ mode: 'history', routes: routes }),
    render: h => h(require('./components/app/app.vue.html'))
});

Diese Version möchten wir zum Test auf Home anzeigen.

Hierzu muss "home.vue.hmtl" zunächst einen Verweis auf ein neues Script "home.ts" hinzugefügt bekommen:

Anschließend wird die Datei "home.ts" angelegt:

import Vue from 'vue';  
import { Component, Prop } from 'vue-property-decorator';

@Component
export default class Home extends Vue {  
    version = "1.0.0";
}

Aktuell haben wir noch in der Klasse die Versionsnummer hinterlegt. Ein erster Test auf der Homepage zeigt uns nun dieses Wert an:

Einbinden des Vuex Stores in Home

Selbstverständlich wollen wir eigentlich den Wert aus den Vuex Store haben. Daher ist die Datei "home.ts" wie folgt anzupassen:

import Vue from 'vue';  
import { Component } from 'vue-property-decorator';  
import { mapGetters } from 'vuex';

@Component({
    computed: mapGetters(['version'])
})
export default class Home extends Vue {  
    version: string;
}

Anschließend ist der Store noch in "boot.ts" anzulegen:

import './css/site.css';  
import 'bootstrap';  
import Vue from 'vue';  
import VueRouter from 'vue-router';  
import Vuex, { StoreOptions } from 'vuex';  
import { RootState } from './types/RootState';

Vue.use(VueRouter);  
Vue.use(Vuex);

const store: StoreOptions<RootState> = {  
    state: {
        version: '2.0.0'
    },
    getters: {
        version: state => state.version
    }
}

const routes = [  
    { path: '/', component: require('./components/home/home.vue.html') },
    { path: '/counter', component: require('./components/counter/counter.vue.html') },
    { path: '/fetchdata', component: require('./components/fetchdata/fetchdata.vue.html') }
];

new Vue({  
    el: '#app-root',
    router: new VueRouter({ mode: 'history', routes: routes }),
    render: h => h(require('./components/app/app.vue.html')),
    store: new Vuex.Store<RootState>(store)
});

Hierbei wird nun Vuex mit "Vue.use" zunächst eingebunden. Anschließend ein Store mit nur einen aktuellen Wert Version 2.0.0 vorbelegt.

Der Store wird im "new Vue" entsprechend zugewiesen.

Ist alles richtig gelaufen, dann erscheint nun eine Veränderte Versionsnummer auf den Bildschirm:

Implementierung von Axios in fetchdata

Zunächst wird fetchdata.ts umgeschrieben, so dass es statt mit "fetch" die Daten über "Axios" einliest:

import Vue from 'vue';  
import { Component, Inject } from 'vue-property-decorator';  
import { AxiosResponse } from "axios";  
import Axios from "axios";

interface WeatherForecast {  
    dateFormatted: string;
    temperatureC: number;
    temperatureF: number;
    summary: string;
}

@Component
export default class FetchDataComponent extends Vue {  
    forecasts: WeatherForecast[] = [];

    mounted() {
            Axios.get('api/SampleData/WeatherForecasts?ort=Munich')
            .then((res: AxiosResponse) => {
                this.forecasts = res.data;
            })
    }
}

Mit dieser Anpassung sollten die Daten weiterhin abgeholt werden:

Als nächstes werden die "WeatherForecast" in Types verschoben:

Und befinden sich nun in der Datei "WeatherForecat.ts" im Verzeichnis "types":

export interface WeatherForecast {  
    dateFormatted: string;
    temperatureC: number;
    temperatureF: number;
    summary: string;
}

Aufräumen der Store Struktur

Damit der Code besser lesbar und wartbar bleibt, wird der "Store" und die "Vuex" Logik in ein eigenes Verzeichnis "Store" verschoben in die Datei "store.ts":

import Vue from 'vue';  
import Vuex, { StoreOptions } from 'vuex';  
import { RootState } from '../types/RootState';

Vue.use(Vuex);

const storevalue: StoreOptions<RootState> = {  
    state: {
        version: '2.0.0'
    },
    getters: {
        version: state => state.version
    }
}

const store = new Vuex.Store<RootState>(storevalue)

export default store;

Nun kann die Datei "boot.ts" wieder ein wenig aufgräumt werden.

import './css/site.css';  
import 'bootstrap';  
import Vue from 'vue';  
import VueRouter from 'vue-router';  
import Store from './store/store';

Vue.use(VueRouter);


const routes = [  
    { path: '/', component: require('./components/home/home.vue.html') },
    { path: '/counter', component: require('./components/counter/counter.vue.html') },
    { path: '/fetchdata', component: require('./components/fetchdata/fetchdata.vue.html') }
];

new Vue({  
    el: '#app-root',
    router: new VueRouter({ mode: 'history', routes: routes }),
    render: h => h(require('./components/app/app.vue.html')),
    store: Store
});

Separieren der Axios Logik in einen Service

Damit die Axios Logik nicht mehr bestandteil des Darstellungscodes ist und weil man diese ja möglicherweise für weitere Logiken verwenden möchte, wird ein neus Verzeichnis Services angelegt und dort eine WheatherforcastService Klasse angelegt.

import Axios from "axios";  
import { WeatherForecast } from "../types/WeatherForecast";  
import { AxiosResponse } from "axios";

class WeatherForecastServices {  
    async retrieveData(city: string): Promise<any> {
        return Axios.get('api/SampleData/WeatherForecasts?ort=Munich').then(res => {
            return res.data;
        })
    }
}

export const weatherForecastServices = new WeatherForecastServices();  

Achtung: Folgende Einstellung ist allerdings vorher vorzunehmen, damit die Browser-Kompatibilität mit IE11 gewährtleistet ist, da dieser keine Promises kann. Die Datei tsconfig.json ist um folgende "lib" Parameter anzupassen:

Nun kann das Typescript von "fetchdata.ts" weiter verschlankt werden und enthält nun keine direkte Logik, wie genau der Service bereitstellt:

import Vue from 'vue';  
import { Component } from 'vue-property-decorator';  
import { WeatherForecast } from "../../types/WeatherForecast";  
import { weatherForecastServices } from '../../services/WeatherForecastServices';


@Component
export default class FetchDataComponent extends Vue {  
    forecasts: WeatherForecast[] = [];

    mounted() {
        weatherForecastServices.retrieveData("Munich").then(res => {
            this.forecasts = res;
        })
    }
}

Übertragen der Logik nach Vuex

Nun ist es endlich soweit, dass die Logik komplett aus "fetchdata.ts" entfernt werden kann.

In "Store.ts" wird zunächst die benötigte Action deklariert:

import Vue from 'vue';  
import Vuex, { StoreOptions } from 'vuex';  
import { RootState } from '../types/RootState';  
import { weatherForecastServices } from '../services/WeatherForecastServices';

Vue.use(Vuex);

const storevalue: StoreOptions<RootState> = {  
    state: {
        version: '2.0.0'
    },
    getters: {
        version: state => state.version
    },
    actions: {
        fetchWeatherForecast() {
            return weatherForecastServices.retrieveData("Munich");
        }       
    }
}

const store = new Vuex.Store<RootState>(storevalue)

export default store;  

Anschließend kann "fetchdata.ts" weiter gekürzt werden:

import Vue from 'vue';  
import { Component } from 'vue-property-decorator';  
import { WeatherForecast } from "../../types/WeatherForecast";  
import { weatherForecastServices } from '../../services/WeatherForecastServices';  
import { mapActions } from 'vuex';


@Component({
    methods: mapActions({fetchWeatherForecast: 'fetchWeatherForecast'})
  })
export default class FetchDataComponent extends Vue {  
    fetchWeatherForecast?(): Promise<any>;
    forecasts: WeatherForecast[] = [];

    mounted() {
        if (this.fetchWeatherForecast) {
            const fetch: Promise<any> = this.fetchWeatherForecast();
            fetch.then(res => {
                this.forecasts = res;
            })
        }
    }
}

Verwenden des Vuex Datastore

Damit der ganze Aufwand sich tatsächlich gelohnt hat, muss nun noch implementiert werden, dass die "Wetterdaten" nur noch einmal geladen werden und anschließend nur noch aus dem Vuex Store gelesen werden.

Hierzu muss der RootState.ts zunächst erstmal diese Daten zum speichern definiert bekommen:

import { WeatherForecast } from "./WeatherForecast";

export interface RootState {  
    version: string;
    forecasts: WeatherForecast[];
}

Der Store bekommt weitere Methoden:

  • Eine Methode um die Daten zu lesen über den "getter", ähnlich wie bereits bei Version
  • Nur über Mutations dürfen Daten geändert werden im Store, damit die Getter auch den gültigen Update Trigger bekommen
  • Unter Action wird die Methode so geändert, dass diese nur noch den WeatherForecastService aufruft, wenn noch keine Daten im Store vorhanden sind. Anschliessent wird mit "commit" die Mutation aufgerufen, um die Daten zu ändern.

Insgesamt sieht der Store nun so aus:

import Vue from 'vue';  
import Vuex, { StoreOptions, Dispatch } from 'vuex';  
import { RootState } from '../types/RootState';  
import { WeatherForecast } from "../types/WeatherForecast";  
import { weatherForecastServices } from '../services/WeatherForecastServices';

Vue.use(Vuex);

const storevalue: StoreOptions<RootState> = {  
    state: {
        version: '2.0.0',
        forecasts: []
    },
    getters: {
        version: state => state.version,
        forecasts: state => {
            return state.forecasts;
        }
    },
    // Mur Mutatins können Daten ändern
    mutations: {
        UPDATE_WEATHER_FORECAST(state, data) {
            state.forecasts = data;
        }
    },
    // Nur Actions können asyncron sein
    actions: {
        fetchWeatherForecast(context) {
            if (context.state.forecasts === null || context.state.forecasts.length == 0) {
                weatherForecastServices.retrieveData("Munich").then(result => {
                    context.commit('UPDATE_WEATHER_FORECAST', result);
                })
            }
        }       
    }
}

const store = new Vuex.Store<RootState>(storevalue)

export default store;  

Der Code von FetchData sieht nun kaum noch, woher die Daten wirklich kommen:

import Vue from 'vue';  
import { Component } from 'vue-property-decorator';  
import { WeatherForecast } from "../../types/WeatherForecast";  
import { weatherForecastServices } from '../../services/WeatherForecastServices';  
import { mapActions, mapGetters } from 'vuex';


@Component({
    computed: mapGetters(['forecasts']),
    methods: mapActions({fetchWeatherForecast: 'fetchWeatherForecast'})
  })
export default class FetchDataComponent extends Vue {  
    fetchWeatherForecast?(): Promise<any>;
    forecasts: WeatherForecast[];

    mounted() {
        if (this.fetchWeatherForecast) {
            this.fetchWeatherForecast();
        }
        if (this.forecasts == null) {
            this.forecasts = [];
        }
    }
}

Startet man nun ist die Webseite beim Forecast Load beim zweiten Aufrufen deutlich schneller.

Es ist empfehlenswert nun auch spätestens die Vue Devtools zu installieren: https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd

Mit den Devtools ist schön zu sehen, dass der Store einmalig mit den Daten beladen wird und anschließend diese nur noch anzeigt:

Zusammenfassung

Diese Artikel hat gezeigt, wie Vuex und Axios verwendet werden können, um zentrale Daten nicht innerhalb von Componenten zu halten. Insbesondere wenn mehrere Komponenten auf die gleichen Daten zugreifen kann es sehr schnell zu Inkonsitenzen und Querabhängigkeiten kommen, die die Pflege der Applikation sehr erschweren würden.

Diesen Vorteil erkauft man allerdings mit einen etwas höhren Aufwand beim Setup den Vuex leider mit sich bringt.

Den aktuellen Stand finden Sie unter: https://github.com/smoki99/DotNetVueBlog/releases/tag/v0.0.6