4. MVVM-Frameworks: Vue und Svelte

_images/vue.png _images/svelte.png

4.1. Model-View-ViewModel

4.1.1. Anforderungen

Model-View-ViewModel (MVVM) ist heutzutage ein weit verbreitetes Architekturmuster im Bereich JavaScript. Aufgrund seiner Beliebtheit sind dazu innerhalb der letzten Jahre zahlreiche Webframeworks entstanden, darunter auch Vue.js und Svelte.

Zu den Auslösern gehören u.a. die gestiegenen Anforderungen der Anwendungen an einen Webbrowser. Heutzutage sollte ein Browser in der Lage sein, asynchrone Netzwerkabfragen (AJAX-Requests) durchführen zu können, oder auch JavaScript-Module dynamisch nachladen zu können. Häufig müssen Inhalte basierend auf einem Datenmodell in der Oberfläche aktualisiert werden. Das ist dann der Fall, wenn durch die Interaktion des Nutzers mit der Seite eine Datenänderung nötig wird. Außerdem werden in Echtzeitszenarien Fälle unterstützt, bei denen Daten zwischen Client und Server beidseitig ausgetauscht werden müssen.

Durch den Ausbau dieser Funktionen wurde die Möglichkeit geschaffen, größere Teile einer Webanwendung vollständig in den Client auszulagern. Zur Motivation der Entwickler gehören etwa die Reduzierung der Serverlast, die Realisierung von Offline-Szenarien oder die Umsetzung von Seitenbestandteilen mit hoher Interaktivität (wie etwa bei Multimedia-Anwendungen).

4.1.2. Entstehung

MVVM wurde erstmals im Jahre 2005 vom Microsoft-Architekten John Gossman beschrieben und schon damals kam es in deren GUI-Sprache XAML zum Einsatz. Im Jahr 2010 wurde das MVVM-Muster dann von Steve Sanderson nach HTML und JavaScript portiert. Die so entstandene Bibliothek erhielt den Namen Knockout.js.

Nur drei Monate später veröffentlichte Google die erste Version von AngularJS, ein umfangreiches Framework, mit dem sich Single-Page-Webanwendungen erstellen lassen. Diese bestehen aus nur einem HTML-Dokument und lassen ihre Inhalte dynamisch nachladen. Knockout und AngularJS legten damit den Grundstein für viele weitere Bibliotheken, die auf sehr ähnlichen Technologien aufbauen, zum Teil aber mit stark abweichenden Philosophien, darunter mehr Flexibilität und Leichtgewichtigkeit.

4.1.3. Architekturkonzept

_images/mvvm.png
  • Modell:

    Das Modell steuert sowohl das Verhalten der Anwendung als auch ihre Daten. Es repräsentiert diejenige Zugriffsschicht, die für den App-Inhalt steht, oder kommt stellvertretend für den echten Zustandsinhalt zum Einsatz. Eine Aktualisierung des Modells kann nur über eine Datenänderung herbeigeführt werden.

  • View:

    Die View ist der Teil, den der Benutzer tatsächlich auf dem Bildschirm zu sehen bekommt. Sie beinhaltet die Struktur, das Layout und die visuelle Darstellung der Inhalte im Browserfenster. Umgekehrt nimmt die View die Interaktionen des Nutzers wahr und ist dafür zuständig, diese an die darunterliegenden Schichten weiterzuleiten.

  • ViewModel:

    Das ViewModel verknüpft die View mit dem Modell. Zum einen ruft es Funktionen oder Dienste auf, die durch das Modell bereitgestellt werden, und zum anderen stellt es der View Dateneigenschaften und Befehle zur Verfügung, auf die bei einer Nutzerinteraktion zugegriffen wird. Das ViewModel darf jedoch keinerlei Kenntnisse über die View haben, da sonst die so gewonnene Abstraktion aufgeweicht würde.

4.2. Vue.js

4.2.1. Allgemeines

Vue ist ein progressives JavaScript-Framework, mit dem sich Web-Oberflächen nach MVVM gestalten lassen. Es wurde 2013 mit folgendem Hintergedanken von einem ehemaligen Google-Mitarbeiter ins Leben gerufen:

„I figured, what if I could just extract the part that I really liked about Angular and build something really lightweight.“ - Evan You, Entwickler von Vue.js

Da die Einsatzgebiete von kleinen interaktiven Seitenbestandteilen bis hin zu vollwertigen Single-Page-Anwendungen reichen, kann Vue schrittweise in eine bestehende Anwendung integriert werden („inkrementelle Adaptierbarkeit“).

4.2.2. Funktionsweise

Die Reaktion auf Änderungen in der Datenquelle werden als Reaktivität bezeichnet. Um dem MVVM-Pattern gerecht zu werden, erkennt Vue diese Änderungen automatisch und schreibt diese ins Document Object Model (DOM). Das DOM ist eine Schnittstelle zwischen JavaScript und HTML. Alle Elemente, die Teil der Oberfläche sind, können dynamisch aufgerufen, verändert, hinzugefügt oder entfernt werden.

Da die naive Aktualisierung des DOMs nicht sonderlich performant wäre, verwendet Vue ein Virtual DOM. Dieses ist eine leichtgewichtige Repräsentation des tatsächlichen DOMs. Überschreibt Vue das virtuelle DOM, so kann es erkennen, welche Teile tatsächlich ausgetauscht werden müssen und welche nicht. Im echten DOM können unnötige Aktualisierungen zu unsichtbaren Updates führen, die dann die Berechnung der Oberfläche verlangsamen. Der Vorteil des virtuellen DOMs ist also, dass zusätzliche Schreibvorgänge kaum Performance kosten und auf DOM-Ebene vermieden werden können.

4.2.3. Template-Syntax

Ausdrücke

Ausdrücke sind dynamisch ins HTML eingebetteter Text und werden mit doppelten geschweiften Klammern formuliert:

<div id="app">
  <p>Du bist {{ age }} Jahre alt.</p>
  <p>Die Volljährigkeit gilt in deinem Land ab {{ legalAge + '.' }}</p>
  <p>Du hast: {{ hasMoneyLeft ? 'Guthaben frei' : 'KEIN Guthaben' }}</p>
  <p>Letzte Bestellung: {{ user.findLastOrder() }}</p>
</div>

Direktiven

Direktiven sind HTML-Attribute, denen ein v- vorangestellt wird. Sie bewirken, dass Seiteneffekte reaktiv auf ein Element angewendet werden, sobald sich der Wert des angegebenen Ausdrucks ändert:

<!-- Konditionales Hinzufuegen oder Entfernen: -->
<p v-if="userAge >= 18">Du bist voll geschäftsfähig!</p>
<p v-else-if="userAge >= 7">Du bist beschränkt geschäftsfähig.</p>
<p v-else>Du darfst noch nichts kaufen.</p>
<!-- Konditionales Ein- und Ausblenden eines Elements: -->
<div v-show="showEmail">{{ email }}</div>
<!-- Datenbindung in beide Richtungen: -->
<input type="password" v-model="password" />

Schleifen

Im Beispiel wird für jeden Eintrag im Array items genau ein <li>-Element erzeugt:

<ul>
  <li v-for="(item, index) in items" :key="index">{{ item }}</li>
</ul>

Attribut-Bindings

Ein Doppelpunkt vor einem Attribut sorgt dafür, dass ein Ausdruck ausgewertet wird, der bestimmt, welchen Wert das Attribut haben soll. Dieses Auswahlfeld für alkoholische Getränke kann nur von Volljährigen angeklickt werden:

<select :disabled="userAge < 18">
  <option v-for="(item, i) in alcoholicItems" :key="i">{{ item }}</option>
</select>

Erhöht sich das Nutzeralter auf 18+, so wird durch die Reaktivität von Vue das <select>-Element sofort wieder anklickbar.

Events

Wenn Ereignisse von Vue behandelt werden sollen, kann auf dem zugehörigen Element ein @-Handler hinzugefügt werden. Der übergebene Ausdruck muss einer Methode oder einem Funktionsaufruf entsprechen:

<button @click="cancelOrder">Bestellung stornieren</button>
<h1>Registrieren</h1>
<form @submit.prevent="register">...</form>

4.2.4. Anatomie einer Vue-Instanz

Dieses Beispiel zeigt eine Root-Instanz, also den äußersten Teil der Komponenten-Hierarchie von Vue:

new Vue({
  el: "#app",
  components: { ... },
  data() {
    return {
      firstName: "Max",
      lastName: "Mustermann"
      email: "max.mustermann@example.com",
    };
  },
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`;
    },
    antiSpamMail() {
      return this.email.replaceAll(".", " DOT ").replace("@", " AT ");
    }
  },
  watch: {
    firstName(value, oldValue) {
      console.log(`Vorname hat sich geändert von ${oldValue} zu ${value}.`);
    },
  },
  methods: {
    cancelOrder() {
      showModal("Sie haben die Bestellung abgebrochen.");
    },
    register(event) {
      event.preventDefault();
      completeRegistration(this.$data);
    },
  },
});
<form v-on:submit="register">
  <div>
    <label>Vorname: <input v-model="firstName" /></label>
    <label>Nachname: <input v-model="lastName" /></label>
    <label>E-Mail-Adresse: <input type="email" v-model="email" /></label>
  </div>
  <p>Voller Name: {{ fullName }}</p>
  <p>Anti Spam Mail: {{ antiSpamMail }}</p>
  <div>
    <button v-on:click="cancelOrder" type="button">Abbrechen</button>
    <button>Bestellung absenden</button>
  </div>
</form>

Um wiederverwendbare Komponenten zu definieren, kann fast dieselbe Syntax benutzt werden:

Vue.component("my-custom-input", {
  components: { OtherComponent },
  template: `
    <div>
      <OtherComponent></OtherComponent>
      <label>
        <slot />:
        <input v-model="value" :disabled="disabled" v-show="visible" />
      </label>
    </div>
  `,
  props: ["value", "disabled", "visible"],
  data() {
    return { ... };
  },
  computed: { ... },
  watch: { ... },
  methods: { ... },
});

4.2.5. Single-File-Components

Wenn ein JavaScript-Modulsystem zur Verfügung steht, kann eine Komponente in einer speziellen .vue-Datei beschrieben werden, sog. Single-File-Components (SFC). SFC ist ein kompiliertes Format, das nach JavaScript umgewandelt wird. Folgende Datei ist gleichbedeutend mit der oben definierten Komponente:

<template>
  <div>
    <OtherComponent></OtherComponent>
    <label>
      <slot />:
      <input v-model="value" :disabled="disabled" v-show="visible" />
    </label>
  </div>
</template>

<script>
import OtherComponent from "./OtherComponent";

export default {
  name: "MyCustomInput",
  components: { OtherComponent },
  props: ["value", "disabled", "visible"],
  data() {
    return { ... };
  },
  computed: { ... },
  watch: { ... },
  methods: { ... },
};
</script>

So haben wir eine vollständige Trennung der Komponenten, eine logischere Anordnung der Bestandteile sowie komplette Syntaxhervorhebung dazugewonnen. SFCs kommen in vielen Projekten zum Einsatz, in denen Vue die volle Kontrolle über die Seite hat und der Code nicht erst vom Server generiert werden muss.

4.3. Vergleich mit Svelte

Svelte ist ein Webframework, das von Rich Harris entwickelt und Ende 2016 als Nachfolger der Bibliothek Ractive.js veröffentlicht wurde. Die große Ähnlichkeit zwischen Vue und Svelte ist hauptsächlich darauf zurückzuführen, dass Ractive als Vorbild für die API und Single-File-Components von Vue diente.

Wegen der geringen Unterschiede haben wir den Vergleich mit der Template-Syntax von Vue hierher ausgelagert: Template-Syntax von Svelte

4.3.1. Verzicht auf VDOM und Runtime

Anders als Vue verzichtet Svelte auf ein Virtual DOM und kritisiert es als „Pure Overhead“. Stattdessen kompiliert Svelte den domänenspezifischen Code in reines JavaScript, worin keine Referenzen vom Framework mehr verbleiben. Sobald sich der Zustand der Anwendung verändert, schreibt Svelte die Änderung direkt ins DOM. Dadurch erhofft man sich eine Reduzierung der Dateigrößen und eine bessere Laufzeitperformance.

Streng genommen ist Svelte also keine Bibliothek, sondern ein Compiler. Die Frage, ob man es direkt in HTML verwendet oder Single-File-Components nutzt, stellt sich somit nicht. Es gibt nur letzteres.

4.3.2. Mehrere Root-Elemente

Bei Svelte darf es in einer Komponente mehrere Elemente auf der äußersten Ebene eines Templates geben. Das ist in Vue 2 aufgrund technischer Einschränkungen nicht möglich und führt zu einer Warnung. Behelfsweise muss man sich hier mit einem Wrapper-Element zu helfen wissen, hier das <div>:

<template>
  <div>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
<template>

4.3.3. Komponentenanatomie

In der folgenden .svelte-Datei wird eine App definiert, welche die persönlichen Daten eines Nutzers entgegennimmt. Standardmäßig sind diese auf Max Mustermann eingestellt. Sobald der Nutzer fertig mit der Eingabe ist, kann er eine Bestellung aufgeben oder diese stornieren.

Zur Notation:

  • data-Eigenschaften gibt es in Svelte nicht. Stattdessen kommen einfache Variablen zum Einsatz.

  • Anstelle von computed-Eigenschaften wird in Svelte reaktiven Daten ein $:-Zeichen vorangestellt, was eine Zweckentfremdung der Labels in JavaScript darstellt.

  • Im Formular wird bei einem Button-Klick eine Methode aufgerufen. Diese definiert man mittels naturbelassener JavaScript-Funktionen.

Hier das gesamte Beispiel: https://codesandbox.io/embed/svelte-demo-dva-praktikum-8lzdw?fontsize=14&hidenavigation=1&module=%2FApp.svelte&theme=dark

4.4. Neuerungen in Vue 3

4.4.1. Fragmente

Durch die Änderung des Rendering-Mechanismus in Vue 3 können Komponenten nun als Fragmente definiert werden. Diese besitzen mehrere Root-Elemente:

<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
<template>

4.4.2. Composition API

Die neu hinzugekommene Composition API erlaubt es, Eigenschaften mit einfachen Helper-Funktionen zu definieren. Dieser Code wird dann in eine einzige setup-Funktion geschrieben und hat daher den Vorteil, dass Einträge nach logischen Belangen angeordnet werden können.

  • ref() entspricht den data-Eigenschaften.

  • computed() entspricht den computed-Eigenschaften.

  • watchEffect() nimmt eine Funktion entgegen und überwacht alle darin verwendeten Variablen.

  • Methoden werden als einfache JavaScript-Funktionen notiert.

  • Alles, was dem Template zum Rendern zur Verfügung gestellt werden soll, muss im return-Statement angegeben werden.

Da in die setup-Funktion beliebige Teile eines anderen Moduls integriert werden können, erhöht sich die Wiederverwendbarkeit derselben Logik. Man schreibt gemeinsamen Code einfach in eine Funktion, die man dann z.B. am Anfang einer Komponente aufruft.

Hier haben wir unser Svelte-Beispiel in die kompositionale Syntax von Vue umgewandelt:

<script>
import { ref, computed, watchEffect } from "vue";

export default {
  setup() {
    const firstName = ref("Max");
    const lastName = ref("Mustermann");
    const email = ref("max.mustermann@example.com");

    const fullName = computed(() => `${firstName.value} ${lastName.value}`);
    const antiSpamMail = computed(() => email.value.replaceAll(".", " DOT ").replace("@", " AT "));
    watchEffect(() => console.log(`Vorname hat sich geändert zu ${firstName.value}.`));

    function cancelOrder() {
      showModal("Sie haben die Bestellung abgebrochen.");
    }

    function register() {
      completeRegistration({
        firstName: firstName.value,
        lastName: lastName.value,
        email: email.value,
      });
    }

    return {
      firstName,
      lastName,
      email,
      fullName,
      antiSpamMail,
      cancelOrder,
      register,
    };
  },
};
</script>

<template>...</template>

4.5. Ausblick

Durch <script setup> und syntaktischen Zucker für ref soll es in Zukunft möglich sein, Vue-Komponenten in einer abstrakten Syntax schreiben zu können, die an Svelte erinnert:

<script setup>
import { computed, watchEffect } from "vue";

ref: firstName = "Max";
ref: lastName = "Mustermann";
ref: email = "max.mustermann@example.com";

ref: fullName = computed(() => `${firstName} ${lastName}`);
ref: antiSpamMail = computed(() => email.replaceAll(".", " DOT ").replace("@", " AT "));
watchEffect(() => console.log(`Vorname hat sich geändert zu ${firstName}.`));

function cancelOrder() {
  showModal("Sie haben die Bestellung abgebrochen.");
}

function register() {
  completeRegistration({ firstName, lastName, email });
}
</script>

Mehr erfahren: